557 lines
16 KiB
YAML
557 lines
16 KiB
YAML
asyncapi: 3.1.0
|
|
info:
|
|
title: Notification Service Intent Contract
|
|
version: 1.0.0
|
|
description: |
|
|
Stable Redis Streams contract for normalized notification intents
|
|
published by upstream services toward Notification Service.
|
|
channels:
|
|
intents:
|
|
address: notification:intents
|
|
messages:
|
|
notificationIntent:
|
|
$ref: '#/components/messages/NotificationIntent'
|
|
operations:
|
|
publishNotificationIntent:
|
|
action: send
|
|
summary: Publish one normalized notification intent.
|
|
channel:
|
|
$ref: '#/channels/intents'
|
|
messages:
|
|
- $ref: '#/channels/intents/messages/notificationIntent'
|
|
components:
|
|
messages:
|
|
NotificationIntent:
|
|
name: NotificationIntent
|
|
title: Notification intent
|
|
summary: One normalized notification request published into Notification Service.
|
|
payload:
|
|
$ref: '#/components/schemas/NotificationIntentEnvelope'
|
|
examples:
|
|
- name: gameTurnReady
|
|
summary: User-targeted game-turn notification.
|
|
payload:
|
|
notification_type: game.turn.ready
|
|
producer: game_master
|
|
audience_kind: user
|
|
recipient_user_ids_json: '["user-1","user-2"]'
|
|
idempotency_key: game-master:game-123:turn-54
|
|
occurred_at_ms: "1775121700000"
|
|
request_id: request-123
|
|
trace_id: trace-123
|
|
payload_json: '{"game_id":"game-123","game_name":"Nebula Clash","turn_number":54}'
|
|
- name: geoReviewRecommended
|
|
summary: Administrator email notification.
|
|
payload:
|
|
notification_type: geo.review_recommended
|
|
producer: geoprofile
|
|
audience_kind: admin_email
|
|
idempotency_key: geoprofile:user-123:review-true:1775121700001
|
|
occurred_at_ms: "1775121700001"
|
|
payload_json: '{"user_id":"user-123","user_email":"pilot@example.com","observed_country":"DE","usual_connection_country":"PL","review_reason":"country_mismatch"}'
|
|
- name: lobbyApplicationSubmittedPublic
|
|
summary: Public-game application notification sent to configured admins.
|
|
payload:
|
|
notification_type: lobby.application.submitted
|
|
producer: game_lobby
|
|
audience_kind: admin_email
|
|
idempotency_key: game-lobby:game-456:application-submitted:user-42
|
|
occurred_at_ms: "1775121700002"
|
|
payload_json: '{"game_id":"game-456","game_name":"Orion Front","applicant_user_id":"user-42","applicant_name":"Nova Pilot"}'
|
|
schemas:
|
|
NotificationIntentEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
description: |
|
|
Stable producer-to-notification envelope for one normalized
|
|
notification intent.
|
|
|
|
Duplicate handling is scoped by `(producer, idempotency_key)`.
|
|
A replay with the same normalized content is a successful duplicate.
|
|
A replay with different normalized content is a conflict.
|
|
|
|
`request_id` and `trace_id` are observability-only metadata and do not
|
|
participate in idempotency fingerprinting.
|
|
required:
|
|
- notification_type
|
|
- producer
|
|
- audience_kind
|
|
- idempotency_key
|
|
- occurred_at_ms
|
|
- payload_json
|
|
properties:
|
|
notification_type:
|
|
type: string
|
|
enum:
|
|
- geo.review_recommended
|
|
- game.turn.ready
|
|
- game.finished
|
|
- game.generation_failed
|
|
- lobby.runtime_paused_after_start
|
|
- lobby.application.submitted
|
|
- lobby.membership.approved
|
|
- lobby.membership.rejected
|
|
- lobby.invite.created
|
|
- lobby.invite.redeemed
|
|
- lobby.invite.expired
|
|
description: |
|
|
Exact v1 notification type catalog. `lobby.invite.revoked`
|
|
deliberately remains outside the supported catalog because it
|
|
produces no notification.
|
|
producer:
|
|
type: string
|
|
enum:
|
|
- geoprofile
|
|
- game_master
|
|
- game_lobby
|
|
description: |
|
|
Stable producer identifier. The exact producer value is frozen per
|
|
`notification_type` by the v1 catalog.
|
|
audience_kind:
|
|
type: string
|
|
enum:
|
|
- user
|
|
- admin_email
|
|
description: |
|
|
Delivery audience selector.
|
|
`user` targets concrete `user_id` values from the producer.
|
|
`admin_email` targets configured administrator email lists.
|
|
recipient_user_ids_json:
|
|
type: string
|
|
description: |
|
|
JSON-encoded array of unique stable `user_id` values.
|
|
|
|
Required for `audience_kind=user`. Forbidden for
|
|
`audience_kind=admin_email`.
|
|
|
|
`Notification Service` treats the recipient set as unordered for
|
|
idempotency purposes: duplicate `user_id` values are invalid and
|
|
element order does not change normalized content.
|
|
contentMediaType: application/json
|
|
contentSchema:
|
|
type: array
|
|
minItems: 1
|
|
uniqueItems: true
|
|
items:
|
|
type: string
|
|
minLength: 1
|
|
idempotency_key:
|
|
type: string
|
|
minLength: 1
|
|
description: |
|
|
Producer-owned idempotency key scoped together with `producer`.
|
|
occurred_at_ms:
|
|
type: string
|
|
pattern: '^[0-9]+$'
|
|
description: Milliseconds since Unix epoch as a base-10 string.
|
|
request_id:
|
|
type: string
|
|
description: Optional observability request identifier.
|
|
trace_id:
|
|
type: string
|
|
description: Optional observability trace identifier.
|
|
payload_json:
|
|
type: string
|
|
description: |
|
|
JSON-encoded type-specific payload. Payload normalization ignores
|
|
insignificant whitespace and object key order, while array order
|
|
remains significant. Required payload fields are frozen per
|
|
`notification_type`.
|
|
contentMediaType: application/json
|
|
contentSchema:
|
|
type: object
|
|
additionalProperties: true
|
|
allOf:
|
|
- if:
|
|
properties:
|
|
audience_kind:
|
|
const: user
|
|
required:
|
|
- audience_kind
|
|
then:
|
|
required:
|
|
- recipient_user_ids_json
|
|
- if:
|
|
properties:
|
|
audience_kind:
|
|
const: admin_email
|
|
required:
|
|
- audience_kind
|
|
then:
|
|
not:
|
|
required:
|
|
- recipient_user_ids_json
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: geo.review_recommended
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: geoprofile
|
|
audience_kind:
|
|
const: admin_email
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/GeoReviewRecommendedPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: game.turn.ready
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_master
|
|
audience_kind:
|
|
const: user
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/GameTurnReadyPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: game.finished
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_master
|
|
audience_kind:
|
|
const: user
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/GameFinishedPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: game.generation_failed
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_master
|
|
audience_kind:
|
|
const: admin_email
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/GameGenerationFailedPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: lobby.runtime_paused_after_start
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_lobby
|
|
audience_kind:
|
|
const: admin_email
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/LobbyRuntimePausedAfterStartPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: lobby.application.submitted
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_lobby
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/LobbyApplicationSubmittedPayload'
|
|
oneOf:
|
|
- properties:
|
|
audience_kind:
|
|
const: user
|
|
required:
|
|
- audience_kind
|
|
- properties:
|
|
audience_kind:
|
|
const: admin_email
|
|
required:
|
|
- audience_kind
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: lobby.membership.approved
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_lobby
|
|
audience_kind:
|
|
const: user
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/LobbyMembershipApprovedPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: lobby.membership.rejected
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_lobby
|
|
audience_kind:
|
|
const: user
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/LobbyMembershipRejectedPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: lobby.invite.created
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_lobby
|
|
audience_kind:
|
|
const: user
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/LobbyInviteCreatedPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: lobby.invite.redeemed
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_lobby
|
|
audience_kind:
|
|
const: user
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/LobbyInviteRedeemedPayload'
|
|
- if:
|
|
properties:
|
|
notification_type:
|
|
const: lobby.invite.expired
|
|
required:
|
|
- notification_type
|
|
then:
|
|
properties:
|
|
producer:
|
|
const: game_lobby
|
|
audience_kind:
|
|
const: user
|
|
payload_json:
|
|
contentSchema:
|
|
$ref: '#/components/schemas/LobbyInviteExpiredPayload'
|
|
GeoReviewRecommendedPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- user_id
|
|
- user_email
|
|
- observed_country
|
|
- usual_connection_country
|
|
- review_reason
|
|
properties:
|
|
user_id:
|
|
type: string
|
|
minLength: 1
|
|
user_email:
|
|
type: string
|
|
minLength: 1
|
|
observed_country:
|
|
type: string
|
|
minLength: 1
|
|
usual_connection_country:
|
|
type: string
|
|
minLength: 1
|
|
review_reason:
|
|
type: string
|
|
minLength: 1
|
|
GameTurnReadyPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
- turn_number
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
turn_number:
|
|
type: integer
|
|
minimum: 1
|
|
GameFinishedPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
- final_turn_number
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
final_turn_number:
|
|
type: integer
|
|
minimum: 1
|
|
GameGenerationFailedPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
- failure_reason
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
failure_reason:
|
|
type: string
|
|
minLength: 1
|
|
LobbyRuntimePausedAfterStartPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
LobbyApplicationSubmittedPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
- applicant_user_id
|
|
- applicant_name
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
applicant_user_id:
|
|
type: string
|
|
minLength: 1
|
|
applicant_name:
|
|
type: string
|
|
minLength: 1
|
|
LobbyMembershipApprovedPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
LobbyMembershipRejectedPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
LobbyInviteCreatedPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
- inviter_user_id
|
|
- inviter_name
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
inviter_user_id:
|
|
type: string
|
|
minLength: 1
|
|
inviter_name:
|
|
type: string
|
|
minLength: 1
|
|
LobbyInviteRedeemedPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
- invitee_user_id
|
|
- invitee_name
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
invitee_user_id:
|
|
type: string
|
|
minLength: 1
|
|
invitee_name:
|
|
type: string
|
|
minLength: 1
|
|
LobbyInviteExpiredPayload:
|
|
type: object
|
|
additionalProperties: true
|
|
required:
|
|
- game_id
|
|
- game_name
|
|
- invitee_user_id
|
|
- invitee_name
|
|
properties:
|
|
game_id:
|
|
type: string
|
|
minLength: 1
|
|
game_name:
|
|
type: string
|
|
minLength: 1
|
|
invitee_user_id:
|
|
type: string
|
|
minLength: 1
|
|
invitee_name:
|
|
type: string
|
|
minLength: 1
|