ovpay/src/python/notificatiesbeheren/notification-preferences-example/README.md

4.4 KiB

Notification preferences transformation service - Python example

Reference implementation for the Service Engine GET /customers/notificationpreferences endpoint. It composes raw CRUD API data into the frontend-friendly response model from SE-notifications.yaml.

Supplied contracts used

  • notifications-crud.yaml
    • GET /notificationsubscriptions?customerProfileId={id}&expand=eventTypeChannel
    • GET /notificationcategories?notificationCategoryId={id}&expand=eventTypeChannel
  • customers-crud-v2.yaml
    • GET /ovpaytokens?ovPayTokenId={id}
    • GET /devices?customerProfileId={id}
    • GET /customers?customerProfileId={id} for person/email resource data
  • SE-notifications.yaml
    • GET /customers/notificationpreferences

Runtime options

Local FastAPI with real CRUD APIs

.\.venv\Scripts\python.exe -m pip install -r requirements.txt
$env:NOTIFICATIONS_CRUD_BASE_URL="https://your-notifications-crud-api.example.nl"
$env:CUSTOMERS_CRUD_BASE_URL="https://your-customers-crud-api.example.nl"
.\.venv\Scripts\python.exe -m uvicorn app.entrypoints.fastapi_app:app --reload

Call:

Invoke-RestMethod `
  -Uri "http://127.0.0.1:8000/customers/notificationpreferences?deviceId=device-1" `
  -Headers @{"X-HTM-CUSTOMER-PROFILE-ID-HEADER"="42"}

Local FastAPI with mock data

Use mock mode when you want to demo or test the transformation without live CRUD APIs.

$env:USE_MOCK_DATA="true"
$env:MOCK_SCENARIO="mixed"
.\.venv\Scripts\python.exe -m uvicorn app.entrypoints.fastapi_app:app --reload

Open:

http://127.0.0.1:8000/docs

Health check:

http://127.0.0.1:8000/health

Available mock scenarios:

Scenario Purpose
mixed Default-on push, default-off email, plus customer deviations
defaults_only No customer preferences, so only defaults are applied
global_override Preference with resourceIdentifier = null applies to all resources
inactive_subscription Subscription is returned but marked inactive

Azure Function

Use app/entrypoints/azure_function.py as the HTTP-trigger body. Keep the app/services and app/domain modules unchanged.

Running automated tests

.\.venv\Scripts\python.exe -m pytest

The tests cover:

  • default-on channel without preference -> active
  • default-on channel with preference -> inactive deviation
  • default-off channel without preference -> inactive
  • default-off channel with preference -> active deviation
  • global preference where resourceIdentifier = null
  • device filtering via deviceId
  • inactive subscription still returned as inactive

Design choice

The implementation deliberately separates:

  • HTTP entrypoints: request/response only
  • CRUD client: raw REST calls only
  • Mock CRUD client: deterministic local/demo data
  • Service layer: composition and transformation logic
  • Domain models: frontend/SE response shape

This makes the same transformation reusable in Azure Functions, Container Apps, and tests.

Business rule interpretation

For each customer subscription:

  1. Load customer NotificationSubscriptions with NotificationPreferences expanded to EventTypeChannel.
  2. Load the default category definition with all EventTypes and EventTypeChannels.
  3. Load customer resources for each channel resource type:
    • resourceNameId = 8 -> GET /devices?customerProfileId={id}
    • resourceNameId = 4 -> GET /customers?customerProfileId={id} and use the embedded person email data
  4. For every default EventTypeChannel, write out resources explicitly.
  5. If the channel is default-on and no preference exists, resource is active.
  6. If the channel is default-off and no preference exists, resource is inactive.
  7. A NotificationPreference row is interpreted as a deviation:
    • default-on + preference -> inactive unless explicit isActive says otherwise
    • default-off + preference -> active unless explicit isActive says otherwise
  8. A null resourceIdentifier applies to all resources for that EventTypeChannel.

Production hardening points

  • Add authentication propagation from the SE API to both CRUD APIs.
  • Decide whether category defaults should be cached across requests.
  • Decide whether missing category defaults should become 502 Bad Gateway, partial data, or an empty category.
  • Confirm whether the real NotificationPreference has explicit isActive; this example supports it, but also works when preference existence alone means deviation.