Keep SMS polling separate from delivery
Prepared with AI assistance for Portproof. All events and counts below are fictional. The example is an offline teaching model, not a Portproof API client; it sends no messages, reads no real inbox and makes no purchase.
A status screen can lose useful information when it uses one “success” flag for everything. A number can be reported active while a message fetch fails. A message fetch can succeed with an empty snapshot. An operation can have an uncertain outcome while the interface is still able to refresh an existing record.
Keep those facts separate. In particular, a failed poll must not overwrite a previously successful reading with zero messages or imply that another purchase should be made.
Give each fact its own history
A small read model can retain three independent streams:
| Stream | What its value means | What it does not establish |
|---|---|---|
| Operation outcome | A reported confirmed or unknown result for the same acquisition operation. |
An active resource, a delivered message or permission to make a second purchase. |
| Resource snapshot | The last successfully read state, such as active or expired. |
Whether a separate message refresh succeeded. |
| Message snapshot | The count in the last successfully read message snapshot. | All-time message count, future delivery, account acceptance or continued access after expiry. |
For resource and message reads, retain both the last successful observation time and the latest attempt time/result. If a refresh fails after a successful read, the previous value is still a historical fact. Show it with its age and failed-refresh indicator, rather than presenting it as a new current reading.
An empty successful snapshot and a failed fetch are different. So are a reported expiry and an unreadable status. Do not infer any of them from another stream.
An offline reducer with explicit limits
Save the complete example as polling_model.py. It uses Python 3.10 or newer and only the standard library. The names are this article's teaching format, not a claim about any service's endpoint, event schema or complete lifecycle.
The at field orders observations within each stream. Supply a trustworthy observation timestamp with an explicit offset. Do not stamp a cached old result with a new clock value and treat it as a fresh source fact. An actual integration needs the service's revision/timestamp rules, or serialised reads with suitable stale-response protection; this reducer cannot create an authoritative ordering for overlapping responses.
"""A fictional local read model; it sends no requests or messages."""
from dataclasses import dataclass, field, replace
from datetime import datetime, timezone
import json
def instant(value):
try:
date = datetime.fromisoformat(value.replace("Z", "+00:00"))
if date.tzinfo is None or date.utcoffset() is None:
raise ValueError
return date.astimezone(timezone.utc)
except (AttributeError, TypeError, ValueError):
raise ValueError("Use a timestamp with an explicit UTC offset") from None
@dataclass(frozen=True)
class Snapshot:
value: object = None
succeeded_at: str | None = None
attempted_at: str | None = None
latest_attempt_ok: bool | None = None
@dataclass(frozen=True)
class State:
operation: Snapshot = field(default_factory=Snapshot)
resource: Snapshot = field(default_factory=Snapshot)
messages: Snapshot = field(default_factory=Snapshot)
def apply_event(state, event):
kind, at = event["kind"], instant(event["at"])
if kind in ("operation_confirmed", "operation_unknown"):
stream, ok, value = "operation", True, kind.removeprefix("operation_")
elif kind in ("resource_ok", "messages_ok", "resource_error", "messages_error"):
stream, outcome = kind.split("_")
ok = outcome == "ok"
value = event.get("value") if ok else None
if ok and stream == "resource" and value not in ("pending", "active", "expired", "failed"):
raise ValueError("Unrecognised resource state in this teaching model")
if ok and stream == "messages" and (type(value) is not int or value < 0):
raise ValueError("Use a non-negative snapshot count")
else:
raise ValueError("Unrecognised event")
previous = getattr(state, stream)
if stream == "operation" and previous.value == "confirmed" and value == "unknown":
return state
if previous.attempted_at:
earlier = instant(previous.attempted_at)
if at < earlier:
return state
if at == earlier:
if previous.latest_attempt_ok == ok and (not ok or previous.value == value):
return state
raise ValueError("Conflicting events at the same stream timestamp")
stamp = at.isoformat()
if ok:
current = Snapshot(value, stamp, stamp, True)
else:
current = replace(previous, attempted_at=stamp, latest_attempt_ok=False)
return replace(state, **{stream: current})
def view(state):
def show(snapshot):
return {"last_successful_value": snapshot.value,
"last_successful_at": snapshot.succeeded_at,
"last_attempt_at": snapshot.attempted_at,
"latest_attempt_succeeded": snapshot.latest_attempt_ok}
return {"operation": {"reported_outcome": state.operation.value,
"reported_at": state.operation.attempted_at},
"resource": show(state.resource),
"message_snapshot_count": show(state.messages)}
if __name__ == "__main__":
# Fictional events only. An empty snapshot does not authorise a resend.
events = [
{"kind": "operation_confirmed", "at": "2026-10-23T09:00:00Z"},
{"kind": "resource_ok", "value": "active", "at": "2026-10-23T09:01:00Z"},
{"kind": "messages_ok", "value": 0, "at": "2026-10-23T09:02:00Z"},
{"kind": "messages_error", "at": "2026-10-23T09:03:00Z"},
]
state = State()
for event in events:
state = apply_event(state, event)
print(json.dumps(view(state), indent=2))
Run it locally:
python3 polling_model.py
The fictional sequence records an acknowledged operation, an active resource, an empty successful message snapshot and then a failed message refresh. The output retains the zero from the earlier successful snapshot, its original timestamp, and latest_attempt_succeeded: false for the later attempt. It does not claim that the failed attempt found no messages.
The model intentionally performs no polling, retries, expiry calculation, sending or financial action. It replaces snapshot counts rather than adding them, so replaying the same snapshot does not manufacture more messages.
Handle order, duplicates and uncertainty deliberately
A repeated identical observation at the same stream timestamp leaves the state unchanged. An older observation cannot replace a newer one in that stream. Conflicting resource/message observations at the same timestamp raise an error for review instead of choosing whichever value is more convenient. The resource and message watermarks are independent: a newer message snapshot does not make an earlier resource observation irrelevant.
For the same acquisition operation, a later ambiguous report does not erase a definite earlier confirmation in this model. A confirmation still establishes neither resource activation nor message arrival. If the initial operation is unknown, keep its reference and use its documented recovery/status process. Do not turn a missing response into a second purchase.
This distinction matters beyond SMS. HTTP semantics separates idempotent methods from operations that cannot safely be retried merely because the connection failed. A read-model reducer does not make a side-effecting request idempotent. Any real polling loop also needs the service's access rules, bounded attempts and its rate-limit guidance; this example supplies none of those policies.
Make the screen say what was observed
Useful labels include “Last successful message check”, “Latest refresh failed” and “Reported expiry”. Avoid an unqualified “No messages” based on an error, or “Current” beside a reading whose freshness is unresolved.
Keep message contents and verification codes out of ordinary logs. The example stores only a fictional snapshot count and timestamps; it does not need a phone number, a code or a customer identifier. An actual application still needs authorised access, appropriate retention and the receiving service's rules.
A reported expired resource can coexist with a retained historical message snapshot. That does not promise the inbox remains accessible, that a later message will arrive, or that the same number can be kept. Product term and account-recovery requirements belong in the buyer's decision before purchase.
What was checked
On 23 October 2026, fourteen offline unittest cases passed with Python 3.12.14. They cover unknown initial values, independent resource/message facts, empty successful snapshots, failed-refresh retention, later arrivals, duplicate/older events, same-time conflicts, independent watermarks, uncertain/confirmed operations, expiry, invalid values and timezone normalisation. The standalone example was run, and the displayed Python block was compared with that executed file.
The tests establish the behaviour of this local model. They do not test real delivery, receiving-service acceptance, API polling limits, a supplier integration or customer outcomes.
For the product decision, activation versus rental explains how the access terms differ. Number expiry and account recovery covers dependencies that can remain after the paid term. Check the actual supported service and duration in the SMS catalogue; a previous delivered message is not a guarantee of another one.
Comments
Join the discussionNo comments yet. Be the first to comment.