Moving new casino launches to provider B does not make B the owner of rounds opened with provider A. A late settlement needs the original round mapping, even when the launch router has already changed. The useful cutover test delivers A's event after the switch and checks the recorded owner and resulting credit effect together.
This article was prepared with AI-generated research, prose and code. Its execution results describe a local synthetic model, with no live provider, player account or wallet transaction.
The fixture uses fictional providers and integer credit amounts. It exposes a current-route lookup that rejects an old provider's legitimate event and can admit the replacement provider against the wrong round. The corrected receiver resolves ownership from the round record. A separate held-event path covers the original route being unavailable without treating that outage as permission to use another provider.
Cover: original synthetic diagram constructed for this article with AI assistance.
Store round ownership before changing the launch route
A launch route chooses the destination for new work. A round owner records where particular work began. Put those decisions in different records. If the current configuration is the only place the receiver can find a provider, the integration has lost the distinction it needs for late settlements.
The model's ownership record retains the local round ID, provider ID and provider-side round ID. It also retains the player, currency and route revision used when the round opened. Those values make the settlement join inspectable. The current route can advance while the old ownership record stays unchanged.
For example, local round local-1 belongs to provider A, provider round round-17, player alice and currency EUR under revision 1. New launches then move to B under revision 2. A new local round local-2 may use the same provider-side string, round-17, without becoming the same round. Provider-side identifiers need their declared namespace.
These are fictional identifier conventions, not a vendor API. A production adapter might need a provider account, environment or additional session scope in its ownership key. Include the fields that establish identity in that contract. A string unique in one provider account should not silently become globally unique when several integrations share a gateway.
The recorded revision explains the launch decision. It does not replace the actual provider and round mapping. Revision 1 could include many rounds and configuration changes unrelated to a particular callback. Keep enough identifiers to locate the original work directly, then use the revision as supporting provenance.
Reassigning an existing round would be another operation. This model does not implement it. A default-route change should not silently edit historical ownership to make a late callback agree with today's configuration. If a supplier agreement permits transferring outstanding obligations, represent that authority and its required mapping as a separate contract.
Make the old event the discriminating test
Open a round with A, switch new launches to B and then submit A's settlement for the old round. Leave the original mapping in place. A test that creates every round after the switch only shows that the new launch path can work; it says nothing about the outstanding queue.
The deliberately broken receiver compares the event's provider with the current launch provider. After cutover, it sees B as the expected owner and rejects A. Nothing about the old round's business identity changed. The defect is the source of authority used for the comparison.
Test the reverse error too. A B-labeled event carrying the old A round reference must not become acceptable merely because B is current. Checking only the rejection of A could miss this more dangerous consequence. The old callback path needs to reject the wrong owner while preserving the right one.
Keep the rest of the event stable while exercising this boundary. Changing the player, currency and amount at the same time would make a rejection ambiguous. The ownership regression uses the same retained round record and a valid synthetic event. Separate tests exercise the other join conditions.
No signature or network authentication runs here. The provider label is a trusted test input, not proof that a provider sent a real message. Before applying this decision procedure in an adapter, the actual admission layer must establish the sender and bind the normalized event to that identity.
Join the event to the whole original scope
The local round ID selects the ownership record. Provider and provider-round ID must match it, followed by player and currency. Only then can this model consider a credit. A known round alone does not authorize every message that names it.
A mismatched currency must not be repaired by relabeling the amount. Five hundred minor units in the fixture's EUR scope means EUR5.00. The model does not perform foreign exchange or define units for every currency. An event carrying another currency is an exception to investigate, even when its numeric amount looks plausible.
Player identity has the same containment role. An adapter should not attach a settlement to whichever account happens to own the active browser session. This example compares the event's player with the player retained on the round. It does not test login sessions, account authorization or a real wallet's account-selection logic.
An unknown local round is quarantined with no credit effect. That disposition does not establish whether the provider is wrong, a mapping is missing or historical data has not arrived. Retain the observation and its context so an authorized operator can investigate the join. A successful new-provider launch cannot fill in an unknown old-round mapping.
The fixture keeps a callback decision journal separate from its credit-effect list. A QUARANTINED entry means the receiver observed a message and refused its proposed effect. Do not count every journal row as a settled event. The disposition and resulting effect must tell the same story.
There is a further boundary in the sample: only positive integer credit amounts enter the settlement method. Invalid values fail before the callback journal. That is an input-domain restriction, not a complete malformed-message policy. Production normalization needs its own validation and error-response checks.
Deduplicate an event without suppressing another win
The model's duplicate key combines provider and event ID. It does not use the local round alone. Distinct settlement events can refer to the same round, so a round-level already processed flag would erase useful distinctions. The test demonstrates this using two different event IDs and amounts of 500 and 250 minor units.
Malcolm Featonby writes in the Amazon Builders' Library article Making retries safe with idempotent APIs:
It’s possible that the caller actually wants two identical EC2 instances.
The example concerns EC2 requests. Its useful distinction here is intent: similar content does not necessarily identify the same operation. Our settlement contract explicitly defines event identity. It does not infer that two events are duplicates because they share a round or happen to carry equal amounts.
Delivering the identical provider event again must leave one accepted credit effect. Then deliver a distinct event on that same round. Under this model's declared positive-credit contract, that second event is separate work: 500 plus 250 gives 750 minor units. The model records two effects, rather than suppressing the second one through an overbroad round key.
This is not a claim that every provider permits multiple wins per round. Some interfaces have their own transaction types, finality flags or single-settlement rules. The adapter must express those semantics before choosing its event-to-effect mapping. The local fixture deliberately tests a contract that admits distinct credit events without closing the round.
Provider-scoped identity matters too. A's event-1 and B's event-1 are different keys in this single-account-per-provider example. The new-round test allows both while preserving their separate owners. If the real integration's event IDs are scoped more narrowly, extend the key to that declared boundary instead of relying on the example's shorter namespace.
A duplicate response also needs a precise meaning. Here, DUPLICATE means that the same event has already been seen outside the held path. The first observation may have been applied or quarantined, so inspect its original disposition before calling it settled. A pending old-route event retains UNRESOLVED_OLD_ROUTE instead of becoming completed through redelivery.
Treat changed event content as a conflict
After accepting event-1 for 500, submit the same provider event ID with a different amount. The receiver must retain the first event and record EVENT_CONFLICT. A repeated identifier cannot silently acquire a new economic meaning because a later delivery says something different.
Compare the semantic fields required by the declared contract. The immutable settlement record in this model includes the owner references and amount. A production event may also carry delivery-specific fields, such as another request trace or receipt timestamp, that should not redefine its business meaning. Establish that normalization before relying on equality of arbitrary JSON objects.
The test inspects the retained event and accepted effects after the conflict. Checking only the returned string would be too weak: a receiver could report a conflict after changing the credit underneath. The original 500-minor-unit effect must remain the only accepted one for that event identity.
Do not overwrite the initial event merely to make the journal agree with the latest delivery. Preserve both the first meaning and the conflicting observation. Whether the conflict represents a supplier correction, a mapping defect or an invalid message is an investigation question outside this fixture.
A legitimate correction needs its own contract. It may require a separate transaction type, a reference to the original event and a defined authority. The model does not authorize compensation or negative credits. Reusing an identifier with a new amount is not a substitute for specifying those operations.
Original synthetic diagram constructed with AI assistance. Its sequence matches the local recovery test; it is not a provider or wallet screenshot.
Keep an unavailable old route unresolved
The original owner can be known while its integration is unavailable. Those are different facts. In the fixture, A still owns local-1 after the launch route moves to B. Setting A unavailable prevents the proposed credit and retains the event as UNRESOLVED_OLD_ROUTE.
B being available does not resolve that condition. Its adapter may use a different case namespace, wallet contract or provider agreement. A fallback that chooses B would be changing ownership, not simply delivering the original event by another path. The model never treats replacement-provider availability as settlement authority.
Retain the exact held event together with its original owner reference. This gives the recovery action something definite to resume. A queue row that only says retry later leaves unanswered which provider, round and amount must be used when someone returns to the exception.
An identical delivery while the event is held remains unresolved. It creates no credit and does not change the owner. Recovery happens through an explicit retry_held call after A is available again. That call consumes the retained original event rather than asking the current launch router where new work should go.
This is a deliberately visible control in a teaching model. It has no operator authentication, approval workflow or scheduled retry policy. A deployed recovery action needs those surrounding decisions, including who may request reprocessing and what evidence establishes that the original path is usable. The method name does not supply that authority.
The recovery test checks the full sequence: unavailable A, retained event, no effect, restored A, explicit retry and one original-owner effect. A subsequent ordinary redelivery is DUPLICATE. The result supports this sequential path only; it does not show that two recovery workers cannot race or that a process restart preserves the held event.
Separate the route log, decisions and credit effects
The launch route log explains configuration changes. The round registry identifies owners. The callback journal records receiver dispositions, and the credit-effect list records the simulated effects actually accepted. None of those records should be used as a shorthand for all the others.
A route switch alone creates no credit. A quarantined callback creates no credit either. An unresolved callback is evidence of outstanding work, not proof that the settlement failed at the provider. Those distinctions make the cutover report usable when the new route looks healthy but the old queue still needs attention.
The effect list is not a double-entry ledger. It has no debit counterpart, opening balance, reserved amount or external wallet receipt. Its job is to make duplicate effects countable and attributable inside the fixture. Calling it a production accounting system would add guarantees the implementation does not contain.
Keep currency and player on each accepted effect so an aggregate cannot hide a scope mismatch. Summing all amounts into one gateway total would not establish that the right account received the right currency. Even a correct numeric total can conceal an incorrectly attributed event.
Likewise, this model does not mark a round closed. Accepting one credit is not evidence that no later event is permitted or that the provider has completed all its obligations. Round finality, correction windows and the disposition of outstanding bets belong to the actual provider contract and need their own state transitions.
Run the same contract against both receivers
Executed locally on November 5, 2026 with Python 3.14.7: nine tests produced three failures against BuggyRouter and passed against Router. The failures show rejection of late A, acceptance of B against A's old round and loss of the named unresolved disposition when A is unavailable. These are local synthetic results, not provider or wallet certification.
MODEL_VARIANT=BuggyRouter python3 -B -m unittest -v test_settlement # Ran 9 tests; FAILED (failures=3); exit 1 MODEL_VARIANT=Router python3 -B -m unittest -v test_settlement # Ran 9 tests; OK; exit 0 | Observed condition | Receiver decision | Accepted simulated effect |
|---|---|---|
| Late A event after launch default becomes B | APPLIED through retained owner A | One original-round credit |
| Identical event after application | DUPLICATE | No additional credit |
| Distinct valid event on the same round | APPLIED | Separate credit, 500 + 250 = 750 |
| Same event ID with changed amount | EVENT_CONFLICT | First event retained |
| Unknown round or mismatched owner/player/currency | QUARANTINED | No credit |
| Original A route unavailable | UNRESOLVED_OLD_ROUTE | Retained held event, no credit |
| A restored, ordinary redelivery of held event | UNRESOLVED_OLD_ROUTE | Still no credit |
| Explicit retry of retained held event after A restored | APPLIED through A | One original-round credit |
Keep the runtime and test file in one directory. The implementation selector changes the receiver under test while the observable assertions stay the same. A failing run against the intentional defect establishes that the suite can detect a current-route ownership mistake. The passing run then shows the corrected model satisfying that same bounded contract.
settlement.py
"""Synthetic sequential positive credits; no real ledger or authentication.""" from dataclasses import dataclass @dataclass(frozen=True) class RoundOwner: local_round_id: str provider: str provider_round_id: str player: str currency: str route_revision: int @dataclass(frozen=True) class Settlement: provider: str provider_round_id: str local_round_id: str event_id: str player: str currency: str amount_minor: int @dataclass(frozen=True) class Credit: owner: RoundOwner settlement: Settlement @dataclass(frozen=True) class CallbackRecord: settlement: Settlement disposition: str class BuggyRouter: def __init__(self, provider): self.provider, self.revision = provider, 1 self.route_journal = [(self.revision, provider)] self.available = {provider: True} self.owners = {} self.events = {} self.held = {} self.effects = [] self.callback_journal = [] def switch(self, provider): self.provider = provider self.revision += 1 self.route_journal.append((self.revision, provider)) self.available.setdefault(provider, True) def create(self, local_round_id, provider_round_id, player, currency): if local_round_id in self.owners: raise ValueError('local round already assigned') owner = RoundOwner(local_round_id, self.provider, provider_round_id, player, currency, self.revision) self.owners[local_round_id] = owner return owner def provider_matches(self, owner, event): # Deliberate defect: use today's default as the historical owner. return event.provider == self.provider def record(self, event, disposition): self.callback_journal.append(CallbackRecord(event, disposition)) return disposition def apply(self, event): owner = self.owners.get(event.local_round_id) if (owner is None or not self.provider_matches(owner, event) or (event.provider_round_id, event.local_round_id, event.player, event.currency) != (owner.provider_round_id, owner.local_round_id, owner.player, owner.currency)): return self.record(event, 'QUARANTINED') key = (event.provider, event.event_id) if not self.available.get(owner.provider, False): self.held[key] = event return self.record(event, 'UNRESOLVED_OLD_ROUTE') self.effects.append(Credit(owner, event)) self.held.pop(key, None) return self.record(event, 'APPLIED') def callback(self, event): if type(event.amount_minor) is not int or event.amount_minor <= 0: raise ValueError('positive integer minor units required') key = (event.provider, event.event_id) if key in self.events: if self.events[key] != event: disposition = 'EVENT_CONFLICT' elif key in self.held: disposition = 'UNRESOLVED_OLD_ROUTE' else: disposition = 'DUPLICATE' return self.record(event, disposition) self.events[key] = event return self.apply(event) def retry_held(self, provider, event_id): # Explicit retry accepts an event key, never a replacement payload. event = self.held[(provider, event_id)] return self.apply(event) class Router(BuggyRouter): def provider_matches(self, owner, event): return event.provider == owner.provider test_settlement.py
"""Nine observable contracts for a synthetic positive-credit model.""" import os import unittest from dataclasses import replace from settlement import Settlement class SettlementContract(unittest.TestCase): def setUp(self): import settlement variant = os.environ.get('MODEL_VARIANT', 'Router') self.model = getattr(settlement, variant)('A') self.owner = self.model.create('local-1', 'round-17', 'alice', 'EUR') self.event = Settlement('A', 'round-17', 'local-1', 'event-1', 'alice', 'EUR', 500) def total(self): return sum(effect.settlement.amount_minor for effect in self.model.effects) def test_old_event_after_switch_credits_retained_owner(self): self.model.switch('B') self.assertEqual(self.model.callback(self.event), 'APPLIED') self.assertEqual(self.model.owners['local-1'], self.owner) self.assertEqual(self.model.effects[0].owner, self.owner) self.assertEqual(self.model.effects[0].settlement, self.event) self.assertEqual((len(self.model.effects), self.total()), (1, 500)) self.assertEqual(self.model.callback_journal[-1].disposition, 'APPLIED') def test_new_round_uses_B_and_provider_scoped_ids_coexist(self): self.model.callback(self.event) self.model.switch('B') new = self.model.create('local-2', 'round-17', 'alice', 'EUR') self.assertEqual((new.provider, new.route_revision), ('B', 2)) self.assertEqual(self.model.route_journal, [(1, 'A'), (2, 'B')]) self.assertEqual(new.provider_round_id, self.owner.provider_round_id) event_b = replace(self.event, provider='B', local_round_id='local-2') self.assertEqual(self.model.callback(event_b), 'APPLIED') self.assertEqual([effect.owner for effect in self.model.effects], [self.owner, new]) self.assertEqual((len(self.model.effects), self.total()), (2, 1000)) self.assertEqual(self.model.events[('A', 'event-1')], self.event) self.assertEqual(self.model.events[('B', 'event-1')], event_b) def test_duplicate_once_but_distinct_event_adds_credit(self): self.assertEqual(self.model.callback(self.event), 'APPLIED') self.assertEqual(self.model.callback(self.event), 'DUPLICATE') self.assertEqual((len(self.modelSource:https://dev.to/gambit_stream/casino-provider-replacement-route-late-settlements-to-the-original-round-10n6