From 27e2572dedede92057ca64fd535f66e444be5418 Mon Sep 17 00:00:00 2001 From: Herald Date: Fri, 24 Apr 2026 16:51:48 +0200 Subject: [PATCH] updated to python v13.3 and added mockable tests --- .../README.md | 67 +++++++- .../service_factory.cpython-314.pyc | Bin 0 -> 1251 bytes .../app/__pycache__/settings.cpython-314.pyc | Bin 0 -> 1050 bytes .../__pycache__/crud_client.cpython-314.pyc | Bin 0 -> 5929 bytes .../mock_crud_client.cpython-314.pyc | Bin 0 -> 5980 bytes .../app/clients/mock_crud_client.py | 162 ++++++++++++++++++ .../domain/__pycache__/models.cpython-314.pyc | Bin 0 -> 4109 bytes .../__pycache__/fastapi_app.cpython-314.pyc | Bin 0 -> 1797 bytes .../app/entrypoints/azure_function.py | 6 +- .../app/entrypoints/fastapi_app.py | 11 +- .../app/service_factory.py | 18 ++ ...cation_preferences_service.cpython-314.pyc | Bin 0 -> 12177 bytes .../app/settings.py | 4 + .../requirements.txt | 4 +- ...ock_scenarios.cpython-314-pytest-8.3.4.pyc | Bin 0 -> 9860 bytes ...ransformation.cpython-314-pytest-8.3.4.pyc | Bin 0 -> 8127 bytes .../tests/test_mock_scenarios.py | 69 ++++++++ 17 files changed, 321 insertions(+), 20 deletions(-) create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/__pycache__/service_factory.cpython-314.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/__pycache__/settings.cpython-314.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/clients/__pycache__/crud_client.cpython-314.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/clients/__pycache__/mock_crud_client.cpython-314.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/clients/mock_crud_client.py create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/domain/__pycache__/models.cpython-314.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/__pycache__/fastapi_app.cpython-314.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/service_factory.py create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/app/services/__pycache__/notification_preferences_service.cpython-314.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/tests/__pycache__/test_mock_scenarios.cpython-314-pytest-8.3.4.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/tests/__pycache__/test_transformation.cpython-314-pytest-8.3.4.pyc create mode 100644 src/python/notificatiesbeheren/notification-preferences-example/tests/test_mock_scenarios.py diff --git a/src/python/notificatiesbeheren/notification-preferences-example/README.md b/src/python/notificatiesbeheren/notification-preferences-example/README.md index 07436ed..a2594dd 100644 --- a/src/python/notificatiesbeheren/notification-preferences-example/README.md +++ b/src/python/notificatiesbeheren/notification-preferences-example/README.md @@ -16,32 +16,81 @@ Reference implementation for the Service Engine `GET /customers/notificationpref ## Runtime options -### Container App / local FastAPI +### Local FastAPI with real CRUD APIs -```bash -pip install -r requirements.txt -export NOTIFICATIONS_CRUD_BASE_URL=https://your-notifications-crud-api.example.nl -export CUSTOMERS_CRUD_BASE_URL=https://your-customers-crud-api.example.nl -uvicorn app.entrypoints.fastapi_app:app --reload +```powershell +.\.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: -```bash -curl -H "X-HTM-CUSTOMER-PROFILE-ID-HEADER: 42" \ - "http://localhost:8000/customers/notificationpreferences?deviceId=5bedce29-af0c-4f3c-b182-2caa8a1f9377" +```powershell +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. + +```powershell +$env:USE_MOCK_DATA="true" +$env:MOCK_SCENARIO="mixed" +.\.venv\Scripts\python.exe -m uvicorn app.entrypoints.fastapi_app:app --reload +``` + +Open: + +```text +http://127.0.0.1:8000/docs +``` + +Health check: + +```text +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 + +```powershell +.\.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 diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/__pycache__/service_factory.cpython-314.pyc b/src/python/notificatiesbeheren/notification-preferences-example/app/__pycache__/service_factory.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f5b7a4e77aead1461a354033700c96ee03da0ff GIT binary patch literal 1251 zcmah|PiqrF6rag za3TR;2)@_^-A8+}jdfaI_ zwmh>%NsCb8+JtQrdgRyy&ghJA?zr!mjYa+9OCW-dBIwM4(MNC5UgGgbgqncOq1X5T zH*h2I5JPdwnTQQEN_dyLR0HR;s8wruv}185GA;y%>radwMkq552(>zfcjQ|=gHhY? zd;Gw24d4~lN!UJtAQHzsx8hHWuaNgv$8QtE@_hr63MOXDvbjflRlgTxO*3p1F-fKyYiw80y(SH`64pKaMj4kA!JTQ>VWP}^ixIO+ z+rdq{%edEp>zZI?oeDldt_v|87&EhJaZ6q|6_kUV1er}-i#i^i5!k(}`E8iYNy4Bo z{m7LKDmc@;P&tug(Wb=W#Ej=MW7A-U6F>_PEid{77=1KM<_F1n_#G$bhxy7E_uKAZ zb^By>`?UJzG{1AK@BGY^hx+YdX=zZ}I4Nxmi;IKe+DUQkud1Zd7mAwI&XKBWvX;$q zCP);;R{bwS@wkHQ_{{jx x%1e>VueVKq+VPs*HhD(p;gpy$b{EvSf-xSUg=5T3oeq-mO{mDVb?q_IVb(jRJ-5=#*UQNe;&?2Boa&1GW_FL&qeMq2S9 zR6)VNMEqx(x4Phi_$0nGrl60`rb!Lr!p_coH?!Z&cXwA#WYd7_)9w#HAOPP?&|it3 z)47RG6Slx558ysgBnRV=2iKYa*WM&c_EDa)Hmy7?^@^13Ix|p6d^d4{=*%4^`8rq1 z51&b|m()G1UeH-Vhau4>DB^;$wj5=jfe2}p<60`=+A8TfYG?zl6E`sdK}x0Eq#E7; zm3D`aETvO@hF9Y?rQ+hkLJ)Z@s6|p0mlqeW{vW<}PbcdBHm`Un0hn!;gWHVOd5C6( zG+_g_tmU@=6-3X&V^Sjvuuh)BEz2brF{kHSJ)aQt9G{4`$E_}|< zww%?r4TOB1nr=D8b^_gY8ph7HoLqav1Q{5cXgSkAvlw)+5I(|MVMM1W4O!g~r75aZ zX#~c{7G5-1(0g){(yA|{3jC0V5v5`n%Z2$1#E3B&Gcnm?&)x|7aggp)R5|I+H7liQyu?m3eONtm2& zKH5Kr=9d}k4T!OR*;>KTqw!9Kg@(T`uP|R{_gbAg#>NfSEi}wJwQ?lGs>oqzT9Q*} e4s1fm7r1d`!r<6J`{IT-SR*6VH}A7 literal 0 HcmV?d00001 diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/clients/__pycache__/crud_client.cpython-314.pyc b/src/python/notificatiesbeheren/notification-preferences-example/app/clients/__pycache__/crud_client.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8543126caca7f0c74f48398cb361a9efcfa6c0d1 GIT binary patch literal 5929 zcmc&&T}&L;6~41GvwtivLbMEild(L-$?m1dl8zu1k_R8PPS856QD^|*nULWK?j3E~Eov zfl<*BMq6Sxp`L{)GkwNVYg*Tfc*ZO96atj1Lt z>OsFAR6|e;V-5BY0XoX*uv!CjjMEXd7U()oN7Z_u8#rB~?sH=dFz5EEvkr@T4-LPQ9cIO^mVrX!I4s$SJdVW?BYg zDTnG7o3rU6KfaQ+=gqWY=N-kKgV`p71$D;yl}T91U)#2_1?6qazQ~jX`(o6wm9(DC z@^3LaPt%5CSecw{S`Jfm*hJpCNc9}=Putn7kw(TUmcD2(Wscg5ya#s}P1_diC+)yC z(*iIUfA|F92b>o+dw{r1rpblC^FJpf18p7TjIbc6f*Mdo_(`gq@??whcAG)2=A4=K zWUI~U%+T^Q>x#^wZeu#n9DC8AY^S5%F&7OuzQzpLOoqAgT-Mf|>j;ye$0mlm>UQms zAI(fLm^`y!P|vV-8!>2J15}>5I9n zF{9^lGaj4SOd5o&d6oWL!HsE}k5fa_+yhk*=&$UngZ;55IH$HqSWazV@tu+=$?z@( zNvBc@Ar&SEx;7#}ZNgZRmaz06ggN{on{NRD3>PNKq9~7mw@Qxrt<%KX?w<}kaICM2 zguMQwgM$l>E_heqxk8u`P>>Y>pU>FaP}z5Cc&_rRU*ftw?%-6yfB zYrU)gPvH*Q1e3OaMSWu_gjAU9OO_%?MM<=EqXsB`@+ySj!QVrKoCe8vlP*|v2AaHQ zVq3!H76OL&^@a(Q)UHP*&-tSV1Wn8b@}d?v=8riAiv1R__mXf~JS|5_PMVe`Lc11# z>gx$LP+sK}xP_`Y^0YjG?B4a1ztKgYANjxQDfj1LPhei)@0gF(Q&6w`LKjJp5IGGS zJN-QTA__T5_!u~?xAPE*lEeP`^KjY@Vg|ZMCvXk_5cvaQCozR*L{mtL$pGzu!EVU6 zMdQ~6`WQ4MgRZ>bIJrx1%@Er95pL?aX1LLHD@sE!G>cm!vG))UI?d$l|H!Xu(qF;rU)%euE7^3S+|8S3RLl_pqzZZe}qIozZ z---&uV)#}~0_px*XudTd49me=gF&EgivqOYmIb6k!Qq3E+qJ@QL*4DVDAH{KEZZZ{ zTcy^BQ?Awu5chF7QYw%j*JV$g<7I!M<)}SpW)02ExFWbgimNO)WliSnR^|sPZ4XRk z?R*zoBq$GVVDY6`!dG!3!>gWlR_ffgcD@fYJ?)$(d)Z}Zt(*s=@lgSj)7+(|z@R(1&BunZqf&CYt^wKSTg2qS^)G?^pYICvda@v>v(9c zIriIz1)yH8-HxolHlP0i@J5kr+v14ahXGXIjh~kARebpZuilGp^s4V+r1S+4KDvcD zust1MXA^Ll`2ltrZ1S{_5>&CmCX$d8?RviaHM$r z46oh?bXQ>dqncCzTA%`G;tvH{h4Yt}!>Nq1WWrsS;uXXqpxy!ZL6u)n zm2yaz%0Ze0Qa*6e;Q1k@E!vqpT!J*rT)mUmvwly# zrp=j<-^oG((z2mDq-hyDt!cCkR`LS(7mz}&rl{5QRV=2UaQAEcYHOx7-GL0zY~C># z%tRx~mBUMUV2XfZfxefepm?8b1Y|k9RR`&=ttQB5+--#m)ZGS%Pwzst?`|`g)2)WG z-g#@`3dt}YWiAgH7dQ$<>*NNVLJojHC8$um0MQ;SaJbttUYT)3i{)R_tGDQe2R-(@ tNZ*8MpfiM_A1#T3ApC`Ne@m1({Tng>K1Kil literal 0 HcmV?d00001 diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/clients/__pycache__/mock_crud_client.cpython-314.pyc b/src/python/notificatiesbeheren/notification-preferences-example/app/clients/__pycache__/mock_crud_client.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca8fd0f73b7200dda0f1dcd230b9673d09a6b5ea GIT binary patch literal 5980 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 Ih1re%A6f`IG5`Po literal 0 HcmV?d00001 diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/clients/mock_crud_client.py b/src/python/notificatiesbeheren/notification-preferences-example/app/clients/mock_crud_client.py new file mode 100644 index 0000000..253d00e --- /dev/null +++ b/src/python/notificatiesbeheren/notification-preferences-example/app/clients/mock_crud_client.py @@ -0,0 +1,162 @@ +from __future__ import annotations +from copy import deepcopy +from typing import Any, Optional + + +class MockCrudClient: + """Deterministic in-memory CRUD client for local demos and tests. + + It implements the same methods as CrudApiClient, so the service layer does + not know whether data comes from real CRUD APIs or from mock fixtures. + """ + + def __init__(self, scenario: str = "mixed"): + self.scenario = scenario + + async def get_notification_subscriptions(self, customer_profile_id: int) -> list[dict[str, Any]]: + subscriptions = deepcopy(MOCK_SUBSCRIPTIONS.get(self.scenario, MOCK_SUBSCRIPTIONS["mixed"])) + for subscription in subscriptions: + subscription["customerProfileId"] = customer_profile_id + return subscriptions + + async def get_notification_category_defaults(self, notification_category_id: int) -> Optional[dict[str, Any]]: + return deepcopy(MOCK_CATEGORIES.get(notification_category_id)) + + async def get_ovpay_token(self, ovpay_token_id: Optional[int]) -> Optional[dict[str, Any]]: + if ovpay_token_id is None: + return None + return deepcopy(MOCK_OVPAY_TOKENS.get(ovpay_token_id, {"ovPayTokenId": ovpay_token_id, "alias": None})) + + async def get_devices(self, customer_profile_id: int) -> list[dict[str, Any]]: + return deepcopy(MOCK_DEVICES) + + async def get_persons(self, customer_profile_id: int) -> list[dict[str, Any]]: + return deepcopy(MOCK_CUSTOMERS) + + +MOCK_CATEGORIES: dict[int, dict[str, Any]] = { + 1: { + "notificationCategoryId": 1, + "name": "Reizen", + "groupName": "Mijn passen", + "eventTypes": [ + { + "eventTypeId": 10, + "name": "OVPAY_CHECK_IN", + "subName": "Check-in", + "prettyName": "Check-in bevestiging", + "eventTypeChannels": [ + { + "eventTypeChannelId": "etc-push-default-on", + "channel": { + "channelId": 1, + "name": "Push", + "resourceName": {"resourceNameId": 8, "name": "devices"}, + }, + "isDefault": True, + "isMandatory": False, + }, + { + "eventTypeChannelId": "etc-email-default-off", + "channel": { + "channelId": 2, + "name": "E-mail", + "resourceName": {"resourceNameId": 4, "name": "customers"}, + }, + "isDefault": False, + "isMandatory": False, + }, + ], + }, + { + "eventTypeId": 20, + "name": "SERVICE_MESSAGE", + "subName": "Service", + "prettyName": "Servicebericht", + "eventTypeChannels": [ + { + "eventTypeChannelId": "etc-email-mandatory", + "channel": { + "channelId": 2, + "name": "E-mail", + "resourceName": {"resourceNameId": 4, "name": "customers"}, + }, + "isDefault": True, + "isMandatory": True, + } + ], + }, + ], + } +} + +MOCK_SUBSCRIPTIONS: dict[str, list[dict[str, Any]]] = { + # Mixed scenario demonstrates both default behavior and customer deviations. + "mixed": [ + { + "notificationSubscriptionId": "sub-mixed-1", + "notificationCategoryId": 1, + "notificationCategory": {"notificationCategoryId": 1, "name": "Reizen", "groupName": "Mijn passen"}, + "customerProfileId": 42, + "ovPayTokenId": 112, + "subscriptionActivities": [{"timestamp": "2026-04-01T10:00:00Z", "isActive": True}], + "notificationPreferences": [ + # Default push=true: this row opts out only device-2. + {"notificationPreferenceId": "pref-push-device-2-off", "eventTypeChannelId": "etc-push-default-on", "resourceIdentifier": "device-2"}, + # Default email=false: this row opts in the customer's email resource. + {"notificationPreferenceId": "pref-email-on", "eventTypeChannelId": "etc-email-default-off", "resourceIdentifier": "42"}, + ], + } + ], + # No preferences: output should be entirely based on category defaults. + "defaults_only": [ + { + "notificationSubscriptionId": "sub-defaults-1", + "notificationCategoryId": 1, + "notificationCategory": {"notificationCategoryId": 1, "name": "Reizen", "groupName": "Mijn passen"}, + "customerProfileId": 42, + "ovPayTokenId": 112, + "subscriptionActivities": [{"timestamp": "2026-04-01T10:00:00Z", "isActive": True}], + "notificationPreferences": [], + } + ], + # Global preference: null resourceIdentifier applies the deviation to all resources. + "global_override": [ + { + "notificationSubscriptionId": "sub-global-1", + "notificationCategoryId": 1, + "notificationCategory": {"notificationCategoryId": 1, "name": "Reizen", "groupName": "Mijn passen"}, + "customerProfileId": 42, + "ovPayTokenId": 112, + "subscriptionActivities": [{"timestamp": "2026-04-01T10:00:00Z", "isActive": True}], + "notificationPreferences": [ + {"notificationPreferenceId": "pref-all-push-off", "eventTypeChannelId": "etc-push-default-on", "resourceIdentifier": None} + ], + } + ], + # Subscription inactive while preferences are still written out for the frontend. + "inactive_subscription": [ + { + "notificationSubscriptionId": "sub-inactive-1", + "notificationCategoryId": 1, + "notificationCategory": {"notificationCategoryId": 1, "name": "Reizen", "groupName": "Mijn passen"}, + "customerProfileId": 42, + "ovPayTokenId": 112, + "subscriptionActivities": [{"timestamp": "2026-04-01T10:00:00Z", "isActive": False}], + "notificationPreferences": [], + } + ], +} + +MOCK_OVPAY_TOKENS = { + 112: {"ovPayTokenId": 112, "alias": "Mijn betaalpas"}, +} + +MOCK_DEVICES = [ + {"deviceId": "device-1", "alias": "Mijn iPhone"}, + {"deviceId": "device-2", "alias": "Werktelefoon"}, +] + +MOCK_CUSTOMERS = [ + {"customerProfileId": 42, "person": {"emailAddress": "klant@example.nl"}}, +] diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/domain/__pycache__/models.cpython-314.pyc b/src/python/notificatiesbeheren/notification-preferences-example/app/domain/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1110ec0bb193842bd435ea07c2385cd25893c15 GIT binary patch literal 4109 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 ad2sl%*Mt5 literal 0 HcmV?d00001 diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/azure_function.py b/src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/azure_function.py index 9822074..25d9a57 100644 --- a/src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/azure_function.py +++ b/src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/azure_function.py @@ -1,9 +1,7 @@ from __future__ import annotations import azure.functions as func -from app.clients.crud_client import NotificationsCrudClient -from app.services.notification_preferences_service import NotificationPreferencesService -from app.settings import settings +from app.service_factory import create_notification_preferences_service async def main(req: func.HttpRequest) -> func.HttpResponse: @@ -11,7 +9,7 @@ async def main(req: func.HttpRequest) -> func.HttpResponse: if not customer_profile_id: return func.HttpResponse("Missing X-HTM-CUSTOMER-PROFILE-ID-HEADER", status_code=400) - service = NotificationPreferencesService(NotificationsCrudClient(settings.notifications_crud_base_url, settings.customers_crud_base_url)) + service = create_notification_preferences_service() result = await service.get_customer_notification_preferences( customer_profile_id=int(customer_profile_id), device_id=req.params.get("deviceId"), diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/fastapi_app.py b/src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/fastapi_app.py index 567dc8d..909e3ca 100644 --- a/src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/fastapi_app.py +++ b/src/python/notificatiesbeheren/notification-preferences-example/app/entrypoints/fastapi_app.py @@ -1,16 +1,16 @@ from __future__ import annotations from fastapi import FastAPI, Header, Query -from app.clients.crud_client import NotificationsCrudClient from app.domain.models import CustomerNotificationPreferencesResponse -from app.services.notification_preferences_service import NotificationPreferencesService +from app.service_factory import create_notification_preferences_service from app.settings import settings app = FastAPI(title="SE Notifications example") -def get_service() -> NotificationPreferencesService: - return NotificationPreferencesService(NotificationsCrudClient(settings.notifications_crud_base_url, settings.customers_crud_base_url)) +@app.get("/health") +async def health(): + return {"status": "ok", "useMockData": settings.use_mock_data, "mockScenario": settings.mock_scenario} @app.get("/customers/notificationpreferences", response_model=CustomerNotificationPreferencesResponse) @@ -18,7 +18,8 @@ async def get_customer_notification_preferences( x_htm_customer_profile_id_header: int = Header(..., alias="X-HTM-CUSTOMER-PROFILE-ID-HEADER"), device_id: str | None = Query(default=None, alias="deviceId"), ): - return await get_service().get_customer_notification_preferences( + service = create_notification_preferences_service() + return await service.get_customer_notification_preferences( customer_profile_id=x_htm_customer_profile_id_header, device_id=device_id, ) diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/service_factory.py b/src/python/notificatiesbeheren/notification-preferences-example/app/service_factory.py new file mode 100644 index 0000000..4cbe60b --- /dev/null +++ b/src/python/notificatiesbeheren/notification-preferences-example/app/service_factory.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from app.clients.crud_client import CrudApiClient +from app.clients.mock_crud_client import MockCrudClient +from app.services.notification_preferences_service import NotificationPreferencesService +from app.settings import settings + + +def create_notification_preferences_service() -> NotificationPreferencesService: + if settings.use_mock_data: + return NotificationPreferencesService(MockCrudClient(settings.mock_scenario)) + + return NotificationPreferencesService( + CrudApiClient( + notifications_base_url=settings.notifications_crud_base_url, + customers_base_url=settings.customers_crud_base_url, + ) + ) diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/services/__pycache__/notification_preferences_service.cpython-314.pyc b/src/python/notificatiesbeheren/notification-preferences-example/app/services/__pycache__/notification_preferences_service.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b970be837f1f53d3c2e1f88d9353136bdd7d642c GIT binary patch literal 12177 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 X+rUBjxE@WBkDZ`xHtOIQp!5De1U3p^ literal 0 HcmV?d00001 diff --git a/src/python/notificatiesbeheren/notification-preferences-example/app/settings.py b/src/python/notificatiesbeheren/notification-preferences-example/app/settings.py index da3cd45..a347ded 100644 --- a/src/python/notificatiesbeheren/notification-preferences-example/app/settings.py +++ b/src/python/notificatiesbeheren/notification-preferences-example/app/settings.py @@ -5,5 +5,9 @@ class Settings(BaseSettings): notifications_crud_base_url: str = "http://localhost:8001" customers_crud_base_url: str = "http://localhost:8002" + # local development aid: set USE_MOCK_DATA=true to run without CRUD APIs + use_mock_data: bool = False + mock_scenario: str = "mixed" + settings = Settings() diff --git a/src/python/notificatiesbeheren/notification-preferences-example/requirements.txt b/src/python/notificatiesbeheren/notification-preferences-example/requirements.txt index 238304d..d64d8e4 100644 --- a/src/python/notificatiesbeheren/notification-preferences-example/requirements.txt +++ b/src/python/notificatiesbeheren/notification-preferences-example/requirements.txt @@ -1,8 +1,8 @@ fastapi==0.115.6 uvicorn[standard]==0.34.0 httpx==0.28.1 -pydantic==2.10.4 -pydantic-settings==2.7.1 +pydantic>=2.12.0 +pydantic-settings>=2.7.1 azure-functions==1.21.3 pytest==8.3.4 pytest-asyncio==0.25.2 diff --git a/src/python/notificatiesbeheren/notification-preferences-example/tests/__pycache__/test_mock_scenarios.cpython-314-pytest-8.3.4.pyc b/src/python/notificatiesbeheren/notification-preferences-example/tests/__pycache__/test_mock_scenarios.cpython-314-pytest-8.3.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..557f6b0d9bfe2e4f7480f097cf07f160471c36ba GIT binary patch literal 9860 zcmeHNT~Hg>6~6nU-xUHQFgA{jjO`?f4K@e^No;Bi=EulH-C)r=iZw#4j2a;=cO_!T zWJ;aRG!O0Mt&^F)q%ZA|x@{iXndGGtx1NS}Ivoi?M0VO~(rF*kJ{053WbCJ&yMI~< zIfOJ1os2K5yXXF$-Mv5OeCHgFyK9^X(ob&uI}zj%ijjhzXpOjgl|pC>0aK$Ba5goEc{e;@KvY;hwh!_vx%l_h560)+yf7 zgu1js#=VPiR@1oJU5vAt#?|d&oZU2T&o0I}OylZ#>Im{VKPKNh?xSS)rF86t0i2y4 zNG60-25JtCr!$FoA{Nag(y1U8;sO>@F+qA(!1IZi5QEh*k_{*Zes}*2*%At&pwUz! zuA&pBSr6*#T`0l(nCiO66p{fv{vgkR0pk0hzZ)a zw}zN0W~?3|2Rektj;Z%7BQhd=1o*Zfl};cZg~^xtEb`uzX+j2~8R1$QCj?2}Z?1nf zJ0-<%LL`$W$xdM&CUaszgl>BbdSE7+N(o6xuCJ^bo%XS^6$?@}nUP(RiI>JIk^x?_ zEv9iGv-7Ce>y_sQo(f-<1T2MT1RR|Wr{~4!LRiAFu(*(!NvFc4cNC;40Xhlhx^${d zEUj>xa6LLJCWUZDkTQ~zM`mGrM5LIIisD3CY8Mw|M?`-MTnn#3wtzGP*%I3D>@Rvw zx}v=`Z*P5Lc-4OVqbC0ys5);jJP2Nvf>Y5pL9yPj}*~Li)=y_vI=#YNl?BT*)p5B zE=-T#_XwF-o0yem+NOng6eM9=IwdpWLI=sZ?z^KB2)c~-*yG|tw@)IK-S->G7-2S= zNR|f0@%zpHRL^ z!kxf}h%M>cAh16MasChYiJMp79xk*E7Tgz>hBrLTMbEjs=iIyX1L8``R3PCC81d zgjdFaS{cuCSBk856_h5mxNf4?NJpcWnShq9M&!Og-P^Jw*9hEP$TdpclTN}y-0%&1 zsGm{8R`%wjK;5eM4m;Reo#&51;dYaE*v;PVIbR2b_Z&{hV=%->4xnCv-(4QG!2hpy z27LgAhN78a8_)TxX=c#Cuq_FE6j_ns;SV(MM989O;CO`wHgtH<(8b1_Ake_>kQFp= z?c@cM24=&hQC0PJh}Cr-Z`rZ0p{*-4u#LC!wveqt1KYzM{ryaw8}z!;9W=0Ae+&Jt zNq!bIumBp^zLN(20<-M^E&Fh;VHqms+G(C^K$KN5lW66cxgUlc7t!3C#x8wL!HK2) zR(+S&g`BWU_e>_f;4bA%d#U=4HgF%ry>1qi73+!s<;jUICOKHBbpkYTk@GlKJG6r@mqZAZbFV|?OLIPNkTo}j1 zcA3CLnqeZoFtIE!ITpAHvY|x@9VHpK zCJ=sP$%KDge&|c1c15(qI}wC%;iLmgSlLZ=h%src%GXmvf5Q(Nt0S!H_>aPqtVMuK+9H*3L~w^ zHAdZ&xxl@@a~JPPXJ9AX@C6`MMh#P}fsRY|H;9ap4k#akN& zawc6NQeT`AQijPLfe+N23Xy~qNoA8s+z*c+@?UxjGDFJd&RiaPqtVMuK+7GQSvp8W_1*9dJPlG|pbMk|P#_fs zx`|ZS1BKspTOhA~_8UO0d^u;Ir+B&txRC@dU*>bVd|5N}-T@F2cW+gr5@Hnw2v^^L z0lt2((>7IU6+1<#dY z&6P4wSMRx8e`vjTve+A4?Tr?=seEsAo1X~ka_nM`o622V$i){49be3Ii$zwu3QDV5 zTsP5cN>8JknSds8j-LZB5oq}sc!`vAe0*^q3fQ6=}||&&;(SkeT5f;NtsAtVD$a(7ua#tR{ac*zbCH=a&i&bIFZ~R8)y^ zM9eYl@gZVZpTZ}OoYale^`V<(~M(|v6zKnZ-Cm7?W zhzF{An!I8dd&rYHGTA?qK~T1KS7D} z2`Mp^6yQ5Dk=ZDI0o0P-7xVDsKKMy)bEGAA=Gj8qM8SQzDtP>iSpSDyQ<3xLId6{j zy>UU!IA4*~t^(`JabBZn5U-vw$W_*-m)=n6=DF4!+xkA&T4c+v z0^3SBYIUon()(;{Zd-4t+;S)gRU#Mw0v`uS;G>zn3Gt1C)NjuYuyFf5-M=4hK96kI zcLyFLi?~0c#`mO>7%SmhNsgf14sj{o3-M{BPP3B}#HEIksa*{*#=@RNJHYhgJ_owyd@(lB#Yx zk`~Emk^u$-1en2`yf+W9SjamUEieaT_Y!0e>%BEcQONDf`mlS*E(Tz1U_9|$=Dlh* z*`ztPGqYH1fqea{>eWxzt5@%P)kgv?J_gdSKm8x&cL9b;V#Z2*g?aV}m{n$m5!rD@ zW9gH-=*V-sn2WsT+-26dG#AuxZ(+%?wvEn*V#WbJ^Opi0ob_?EVJ(nGCbW&dIE%%qhu)rerfQBG1W0&Lm~^icIb) zNxAObD|2R^WuS2a-u@Q6xyWeTj7xKLF|6W>B} z(Q=pNJy;C$k)JVx@Vv9uy*v?L3Yz&|FOnFIdM zdV!o^S79bI+{jNDCIzM2nJIQ2=5>X=$@Ih9Kjgkad80jD*dOw{d1*e-U6ogmdJ zc0sNia$EWx5w71Afh9lcP}uNRqo=ki+02#vtePZB4maL-YQ?GMXZxZ?hh1b0-t~5t zEE`?TO2<p`kx}FP2!o zn!PK-*hUwOU7gFBq^8_cG(}boucj=3NLo&8BuYgx};U0i@Zj z3T#y-FFYw3t+Q{i(#PNnFUrdc2_(%0Pul)%6{RXy8*~NtV)^3+t59r}DUqru{dpxgSOX@Pw52>Zrz54!< zQtQqKj#BHPwM+WwWqsu z%Vn?gt+sNDza>z11X_J%CgAmbr}8j|Ps2ypzx-SXa|GX}USSfPjC2pe;Xu4l4N?L; zh#yGhG@$EBz*8_J2xJS^bONE~w++n?NMY(KeUetFsk6TwP(bZ(6&TC@!1AaCn)Wxt zLC!6DL~qUd(DJAhwZ0h-NObgbV1b=rf!$zzJlN<~Duu0k&_-u?;I>tywwO|2qs|E1 z&sVH;JHeBK;QR?XA<~Tm-^uV*_o-?KRUYieapAqoh8wmK+MdCUjT=7L+M2eE`IX=# zo}}sCv3WUpw@(3kthTM}ca`+wAiHr8cha;0!&_m{%&8&xEkmHA1nfDGahoOqMysf2 z<+(&Ytz9()Mg1U=NhLHmM9!0a*uT>>60db8sN?_?gH{jW6y5?-XDxeP+WyrIkupb7 zjp*wv=A|Zdi+l-Vq18O`?|YCa`P+VRvE=W*zoX>e@}y(;gJ`Lv=l;1;$Bz42siRll zGgRt0_+{{cFPd)FamyuEzoWx}x7r>lsbI_oFhyTxZX4@Hq4ySK5AXt)sL( zyml1KWou74!0d>wAE~u6gHfRw31L) zTImTsK6#;cHj%jtC%IiuMu0k8Ny7siEVE8!K~$No-q_hhKeqa#Fsh0}qcTdZ+wXs} z_Q|7<^ouw2oj3JhT=&E)r;AtkD^3^Uf)U>E1R22@RUJ5wCkT*#yh0x@A0XAm-UIDw zi)}77xN;siCpc@aV&X2N;y=*OR6I$Pj6vfS({CyO?WL7W`o z2g{)dxZala^)y-QN*#SGY)v8L>3ZK^f@06Vm+*yw5&T0w%A1(SR zd@nrg=&{W@MfO_;DK0xcUtV`~IdqewIa}*Ka~O_T4xv$HwH;(DR2zPJ?wm=b;M`JI zs@U^%1^Y!ZuUy_7K+SAMHJUANrH;-YXF|7eXy1d`wTOQFvfdiggHyU^s@q5s2kL);jwzQo)}2J!{+#R zSR(%!EwFX^Zl(qAw6|VYi)Wi@A?Fgg#lN5SO)!i9TwU{@ztNhvObf8)fe-Keg=_Ay ztxR+MxU3Zs-LIAPS+f0(dQBjno%S^7$NxR_yb{28k!Zd959OBM4y#!BHj<$i_L>p1X5=CJf;)LK z%&xSdd^D`~o;*pL4u@5^S>Y6*D+5?J6jptIQ(<~d84!uGnH+l|*n1Jn#&!1*BDik%-|t4ObWt^-2i>XLm9I zCpM9DB$-{vC5UWzB{hMUL^)yv&d}w8`28FqSz@$H=7ohMFXhsS482kcw-_!M0MK|P ze2_GVbBzEZdg#}TY*m9Lh5+MArUC}+nIU^-(9D?Q9JObT8A0k{ZJpYBtcaT358K;~ zsu&gb+LIY4570%((AWj87gT^aG^$YH8YWd!ioBGR=^chj;5cO7gTFcf4g|gMf3`<| z6WJJiZ=-#z*sc`X75z?nqkW;+vaou-6zVR9B85<7ZE-UcEru=?LYE$&*$hn-vqGo1ysX#FNfl#m;DejBvX%x>EFuTZcmWoLfNpMKKSTkIMvbPcXYH@l7&yWTBy zy}Nq3B=p>mZVEeeK2j786odo7tnD+25Gg__{cZ3OT{vL!o5=4l`LAr{z_0DAwy-l_ z^O2`PCXivjd+K5Q1K+88VNE|haFJ!#*bAfl!!td=e(C7AaDxA```lj0{ObuP&?Jae z6DsvH_&@va{{@$ii0&9#B_weBj{nbYe{oM!$M1-T%=t6Sw8J_qbNme3#I67Fja*DC zb?c#ew+_y_>MmjkMjO)Z>J9cN)ZE_PFj7+|9IpZtYu=VKOl@muzrA5N$nIudZvO#` zmzp-W)wJC-XN_0q;+o4=*HpB-&gQZ;XybkD9BR(f$5C6^vaQwD+ zZocx~szzI0D_aFE-$$bj0LL!|#Nf2Q0pPU6-&{*A){Lzu036hg7ydo~IKL4c6yR|0 zpQo4X?c_SFFPQ)W7{qIa-f%-Qi8yA!aHQcr9JoPp6UE{{Fgv;uv_YRJfD^cLTaHG_ z4V1ctWCn?ZVqRrVcOK-1G;ZzX>=x>8Rr3<6gY?b+ZsLK8p`!I7~gLDvJ z2sZu-`H>oL^#bLOrp)373av{*XHhs<5Du=fo5JCuFkBFZ*QYjxV>*AWC`=TD31A-I zp`;*OD?%y#ZSdE0VZ!7$kw0egU)jome|)Ff!p?lnUwi7t<@}CbL4A5?e2D!Ac6@;U z^_TUIxFAAa@OSLS4=z6}VZ4iO?Nn-VdU;Hus|Egn6fQ ziu@AFU}rHZo+o7o%d-E$Fh5~FYxytc+H=<^#MIw1NXtts!+CzX?Z?}SY)66Z_?Gjq b{m&c>*I7oc+|^Lav)5T#{ggwNUeWytStTqP literal 0 HcmV?d00001 diff --git a/src/python/notificatiesbeheren/notification-preferences-example/tests/test_mock_scenarios.py b/src/python/notificatiesbeheren/notification-preferences-example/tests/test_mock_scenarios.py new file mode 100644 index 0000000..15da9dc --- /dev/null +++ b/src/python/notificatiesbeheren/notification-preferences-example/tests/test_mock_scenarios.py @@ -0,0 +1,69 @@ +import pytest + +from app.clients.mock_crud_client import MockCrudClient +from app.services.notification_preferences_service import NotificationPreferencesService + + +def _channels(result): + subscription = result.notificationCategories[0].notificationSubscriptions[0] + return { + channel.eventTypeChannelId: channel + for event_type in subscription.eventTypes + for channel in event_type.eventTypeChannels + } + + +@pytest.mark.asyncio +async def test_mixed_scenario_applies_default_on_and_default_off_deviations(): + service = NotificationPreferencesService(MockCrudClient("mixed")) + result = await service.get_customer_notification_preferences(42) + channels = _channels(result) + + push_resources = channels["etc-push-default-on"].resources + assert [(r.resourceIdentifier, r.isActive) for r in push_resources] == [ + ("device-1", True), + ("device-2", False), + ] + + email_resources = channels["etc-email-default-off"].resources + assert [(r.resourceIdentifier, r.isActive) for r in email_resources] == [("42", True)] + + +@pytest.mark.asyncio +async def test_defaults_only_scenario_writes_all_resources_from_defaults(): + service = NotificationPreferencesService(MockCrudClient("defaults_only")) + result = await service.get_customer_notification_preferences(42) + channels = _channels(result) + + assert [r.isActive for r in channels["etc-push-default-on"].resources] == [True, True] + assert [r.isActive for r in channels["etc-email-default-off"].resources] == [False] + assert channels["etc-email-mandatory"].isMandatory is True + assert [r.isActive for r in channels["etc-email-mandatory"].resources] == [True] + + +@pytest.mark.asyncio +async def test_global_override_applies_to_all_resources_when_resource_identifier_is_null(): + service = NotificationPreferencesService(MockCrudClient("global_override")) + result = await service.get_customer_notification_preferences(42) + channels = _channels(result) + + assert [r.isActive for r in channels["etc-push-default-on"].resources] == [False, False] + + +@pytest.mark.asyncio +async def test_device_filter_returns_only_requested_device_for_device_channels(): + service = NotificationPreferencesService(MockCrudClient("mixed")) + result = await service.get_customer_notification_preferences(42, device_id="device-1") + channels = _channels(result) + + assert [(r.resourceIdentifier, r.isActive) for r in channels["etc-push-default-on"].resources] == [("device-1", True)] + + +@pytest.mark.asyncio +async def test_inactive_subscription_is_visible_but_marked_inactive(): + service = NotificationPreferencesService(MockCrudClient("inactive_subscription")) + result = await service.get_customer_notification_preferences(42) + subscription = result.notificationCategories[0].notificationSubscriptions[0] + + assert subscription.isActive is False + assert subscription.eventTypes