DEV Community

PrestonCole1111
PrestonCole1111

Posted on

Gaming Receipts: Malformed Event Payloads, Email/SMS Phone Checks, and Template Variables

Short answer: keep template ownership in a versioned application registry, validate the settled-payment event against a channel-specific JSON Schema, and only then render an email or SMS order receipt. The receipt should be replayable from an immutable notification command, not rebuilt from a mutable game event during a retry.

This is a small boundary with a large blast radius. A malformed receipt can expose the wrong player handle, send a message to an invalid phone number, or turn a payment retry into duplicate mail. The useful design question is not which channel is easier to call. It is who owns the contract at each step, and where a bad value becomes impossible to send.

How should a gaming event notification handle malformed email and SMS payloads?

Start with the event contract. A payment-settled event needs a stable order ID, player reference, currency, amount, and settlement timestamp. It does not need to look like an email payload. Keep that distinction explicit: the domain event records what happened; a notification command records what may be sent.

The command should contain a channel, recipient, template ID, template version, and a map of variables. JSON Schema is useful here because it can describe required properties, types, formats, and additional-property rules in a tool-independent way. The validator should reject unknown template variables as well as missing ones. A typo such as order_total in a template that expects total_amount is a construction error, not a delivery error.

Email and SMS need different recipient checks. An email format check can reject obvious malformed input, but it cannot prove that a mailbox exists, that the player consented, or that a message will be delivered. A phone number should be normalized to an agreed E.164 representation before it enters the command. That still does not establish subscriber consent or carrier reachability.

The critical path is deliberately boring:

import json
import re
from dataclasses import dataclass
from typing import Mapping


EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
E164 = re.compile(r"^\+[1-9]\d{7,14}$")


@dataclass(frozen=True)
class ReceiptCommand:
    channel: str
    recipient: str
    template_id: str
    template_version: int
    variables: Mapping[str, str]


def validate_receipt(
    command: ReceiptCommand,
    required_variables: set[str],
) -> str:
    if command.channel not in {"email", "sms"}:
        raise ValueError("channel must be email or sms")

    pattern = EMAIL if command.channel == "email" else E164
    if pattern.fullmatch(command.recipient) is None:
        expected = "an email address" if command.channel == "email" else "E.164"
        raise ValueError(f"recipient must be {expected}")

    supplied = set(command.variables)
    missing = required_variables - supplied
    unexpected = supplied - required_variables
    if missing or unexpected:
        raise ValueError(
            f"template variables differ from schema: "
            f"missing={sorted(missing)}, unexpected={sorted(unexpected)}"
        )

    if any(not isinstance(value, str) or not value.strip()
           for value in command.variables.values()):
        raise ValueError("template variables must be non-empty strings")

    return json.dumps({
        "channel": command.channel,
        "recipient": command.recipient,
        "template_id": command.template_id,
        "template_version": command.template_version,
        "variables": dict(command.variables),
    }, separators=(",", ":"), ensure_ascii=True)
Enter fullscreen mode Exit fullscreen mode

The Python example expresses the same contract a Node.js service can enforce with its JSON Schema library. The regular expressions are intentionally narrow. They are boundary checks, not mailbox verification, phone-number intelligence, or consent management. Your mileage may vary for internationalized email addresses; document the product policy and test that policy at the boundary instead of silently widening the pattern.

Receipts are evidence.

Consider a settled order for 1,200 in-game credits. The payment event arrives once, the email template is version 7, and the first transport attempt times out after the receiver has accepted the request but before the application sees a response. If the worker rebuilds a message from the latest template and generates a new idempotency key, the second attempt can produce a different receipt or a duplicate. The safer worker stores the validated command with its order ID, notification ID, recipient policy result, template version, and rendered-content hash before transport. A timeout then replays that same command with the same key. If validation rejected the phone number before persistence, the worker does not retry it as though it were a network failure. If a content owner publishes version 8 while version 7 is in flight, the old command remains version 7 and support can explain exactly which contract was used. That is the practical payoff of separating the event from the notification payload: retries preserve meaning instead of reconstructing it.

Validate once.

Who should own receipt templates and their variables?

For a gaming receipt, template ownership should sit with the team accountable for the player-facing message, while the application team owns the variable contract and send decision. That split prevents a copy edit from changing the meaning of a settled payment, but it still lets a content owner improve wording without editing payment code.

Store the template ID, version, locale, channel, required variables, and review state together. A new variable is a contract change. It should pass a fixture containing a settled order, a zero-discount order, a large amount, and a non-ASCII player display name. Do not use the live event stream as the test fixture; event replay must be deterministic and must not send real messages.

Previewing a rendered template catches a missing placeholder and an awkward line break. It does not prove the recipient is valid, the sender identity is authenticated, or the player has permission to receive the notification. Email sender requirements include authentication and behavior expectations that remain operational controls outside the template registry; Google's sender guidance is a useful reference for that work.

SMS deserves a second review path. A receipt may be transactional, but its legal and product treatment still depends on jurisdiction, consent records, sender registration, and message content. Keep country policy and suppression decisions outside the renderer. The renderer should receive an already-authorized command and have one job: produce the approved channel content.

Option comparison: where should the template contract live?

The right choice depends on who changes copy, who must approve it, and how quickly a bad version can be disabled. No option removes the need for an application-owned validation boundary.

Ownership model Good fit Trade-off
Source-controlled templates Engineering-led copy with release review A small wording change follows a deployment path
Content system with immutable versions Operations or localization teams need controlled edits The application must pin versions and verify schema compatibility
Provider-managed templates A channel team already owns delivery and review Domain events become coupled to an external template contract
Inline message construction One fixed internal message with no reusable variables It becomes difficult to audit, localize, and test as channels grow

The catch is that provider-managed templates are not suitable when payment records require a reproducible audit trail independent of the delivery system. Stick with source-controlled or application-stored versions when a receipt must be reconstructed exactly. Inline construction is reasonable for a tiny internal tool, but it is a poor fit for player-facing receipts with localization, retries, and support investigations.

Template ownership also affects incident response. A disabled template version should fail closed with a clear internal reason; it should not fall back to an unreviewed string. The command can be retried after an operator selects an approved version, but the original order ID and notification idempotency key must remain stable.

How do malformed payloads, invalid recipients, and retries stay diagnosable?

Use separate error classes for event parsing, schema validation, recipient policy, template rendering, and transport. They imply different actions. A malformed JSON document goes to the producer or dead-letter queue. An invalid phone number goes to data correction or suppression handling. A missing template variable blocks the deployment or template version. A transient transport response may be retried under a bounded policy.

Log the notification ID, order ID, channel, template version, validation rule, and delivery state. Redact the full email address, phone number, receipt body, and any authentication code. A short recipient fingerprint can help correlate repeated failures without turning logs into a second customer database. This matters for both debugging and compliance.

Retry the immutable command, not the original event. Use a stable idempotency key derived from the notification ID, and keep the same key through transport retries. Backoff should be bounded, and a retryable response should not be confused with a permanent schema or recipient failure. A queue makes that distinction visible: accepted commands can wait; rejected commands need a reason.

One failure deserves special treatment: a receipt must not quietly become an authentication message. NIST's digital identity guidance treats authentication mechanisms as a separate security design. If the product later adds an OTP, create a separate threat model, enrollment policy, and audit path rather than reusing the receipt renderer as a shortcut.

Measure the boundary, not just the final delivery rate. Track validation rejection by rule, render rejection by template version, retry age, duplicate suppression, and delivery outcomes by channel and country policy. A high delivery rate can hide a serious problem if malformed events are being discarded before they reach the transport metrics.

The decision record

For gaming order receipts, the recommended invariant is simple: a settled payment produces one versioned notification command per authorized channel, and every command is checked against the application contract before any external send. Template content can have a separate owner, but variable names, recipient policy, audit identity, and retry behavior stay explicit in the application boundary.

The rejected option is “send the raw payment event and let the channel validate it.” It saves a local schema at first, then couples payment data to message fields and turns a typo into a provider-specific failure. It is not suitable for reusable receipts, mixed email and SMS delivery, or support teams that need to explain what was sent.

There is no universal answer for template storage. I'm not sure every team should move copy into a content system; the deciding evidence is the review workflow and the required audit lifetime. What should not vary is the contract: validate before rendering, pin the version, redact diagnostics, and retry only an immutable command.

References

Further reading

Top comments (0)