Skip to content

feat(compliance): games filter by country availability and bulk geo restrict - #218

Open
okapitula wants to merge 4 commits into
devfrom
feat/games-country-filter
Open

okapitula wants to merge 4 commits into
devfrom
feat/games-country-filter

Conversation

@okapitula

@okapitula okapitula commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The admin games list can filter to games available in a country, and a country's game geo rule can be restricted or unrestricted for many games in one atomic, audited call.

  • GET /backoffice/gaming/games gains geoAvailableCountries (up to 50, compliance:view). A game matches when neither it nor its provider has a geo rule for any listed country (NOT EXISTS anti-joins, same semantics as ComplianceService.checkGame). If a listed country is blocked platform-wide, the page is empty. Rejected with geoBlocked=true or when it shares a code with geoBlockedCountries.
  • GET /compliance/blocked-countries (compliance:view) returns the platform-wide block set (igaming.blockedCountries plus country_rule rows with action block), so the backoffice can explain an empty result.
  • POST /compliance/game-geo-rules/bulk/restrict and .../bulk/unrestrict (compliance:manage-geo) take { gameIds?, providerIds?, countryCode, reason }, with the same target caps as the gaming bulk routes. providerIds expands to every game of the provider. One transaction, 5,000-game cap checked before any write, idempotent (unchanged games are skipped, no NOT_FOUND), unknown ids reported in notFound. Game rules only; provider rules are never touched. Unrestrict also returns stillBlockedByProvider and globallyBlocked.
  • Audit: each bulk call that changes anything emits one compliance.game-geo-rules.bulk_updated event after commit (operation, countryCode, reason, target, notFound, and rules: the full rule rows the call added or removed). Audit records it as one row, as it records gaming.games.bulk_updated, with resourceType: 'game-geo-rule' and resourceId: null.
    • Restrict: before.rules is empty and after.rules holds the new rules.
    • Unrestrict: before.rules holds the removed rules, with their ids, original reasons and timestamps; after.rules is empty.
    • A call that changes nothing emits nothing.
    • This replaces the per-game compliance.game-geo-rule.upserted / .deleted events the single-game routes emit. The single-game routes are unchanged.

Why

The backoffice needs "Available in X" alongside the existing "Restricted in X" (geoBlockedCountries), and bulk-bar Restrict / Unrestrict for one country. Until now, geo rules could only be written one game at a time.

Alternatives considered

  • Per-(game, country) advisory keys for bulk, as the single-game writes use: up to 5,000 locks in one transaction, which can exhaust Postgres' shared lock table (max_locks_per_transaction x connections) and fail unrelated transactions. Instead, bulk writes take one exclusive per-country key, and single-game writes take it shared ahead of their per-(game, country) keys. Single-game writes for a country still run concurrently; a bulk write and single-game writes for the same country take turns.
  • One game-geo-rule.upserted / .deleted event per changed game, as the single-game routes emit: up to 5,000 fire-and-forget audit writes, each taking the global audit_log lock in its own transaction, would starve the connection pool. Writing them in one batch inside the bulk transaction would need a new method on the sealed audit port.
  • The summary event through the transactional outbox (emitInTransaction): the audit record would then commit with the rules instead of being published best-effort after commit. That would require the outbox to be enabled (OUTBOX_ENABLED), and no core path uses emitInTransaction today. The single-game geo routes and the gaming bulk routes also audit after commit.
  • Gaming reads country_rule and the igaming config itself for the global-block check: this would put the definition of "globally blocked" in two places. It is a port method instead, which also backs the new read.

Risks

  • GameGeoCheckPort gains a required method, listGloballyBlockedCountries(). Only compliance binds GAME_GEO_CHECK in core; a consumer that binds its own adapter must implement it. An adapter that returns an empty list makes geoAvailableCountries show globally-blocked markets as available. The play-time checkGame gate is unaffected.
  • A bulk change is not found by searching the audit log for a rule id or game id (resourceId / q), because the bulk row has resourceId: null. It is found by filtering on action=compliance.game-geo-rules.bulk_updated and reading before.rules / after.rules, as with gaming bulk.
  • As with the single-game routes, the bulk audit record is published after commit, so a crash between commit and publish loses it. With up to 5,000 games per call, that is up to 5,000 rule changes without an audit record.
  • Single-game game geo-rule writes now also take a shared per-country advisory lock, so they wait behind an in-flight bulk write for the same country.
  • A bulk call's providerIds scope is a snapshot at call time: a game moved onto the provider concurrently is not covered. Re-running the call is idempotent and closes the gap.
  • The bulk reason is capped at 500 characters. The single-game routes are unchanged.
  • The hands-on QA walkthrough was not completed; coverage is the integration and E2E suites.

…estrict

- listAdminGames gains geoAvailableCountries: games with no game or provider
  geo rule for any listed country; empty page when a listed country is
  blocked platform-wide. Guarded by compliance:view like the other geo filters.
- GET /compliance/blocked-countries lists the platform-wide block set.
- POST /compliance/game-geo-rules/bulk/{restrict,unrestrict} write one
  country's game rule across gameIds and every game of providerIds in one
  transaction, with a 5,000-game cap, idempotent, unknown ids in notFound, never
  touches provider rules. Guarded by compliance:manage-geo.
- Bulk writes take one exclusive per-country advisory lock; single-game
  writes take it shared ahead of their per-(game, country) keys.
- Bulk audit rows are appended inside the transaction in one batch via the
  new AuditWritePort.recordEventsInTransaction; the post-commit events carry
  auditRecorded so the subscriber does not write them again.
- GameGeoCheckPort gains a required listGloballyBlockedCountries().
Bulk restrict and unrestrict emit one compliance.game-geo-rules.bulk_updated
event per call, carrying the rules the call added or removed, and audit
records it as one row with before and after state, as gaming bulk does.
This drops the batched in-transaction audit writer and its sealed port
method. The shared-lock helper folds into withAdvisoryXactLocks as a mode
argument.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant