feat(backend): notification service with in-app + email fan-out on appointment lifecycle events - #47
Conversation
…pointment lifecycle events Wires up the previously-empty NotificationService/NotificationServiceImpl against the existing Notification model and repository, and hooks every appointment lifecycle transition (booked, accepted, declined, updated, cancelled, deleted) in AppointmentServiceImpl and SlotReservationService to produce a persisted notification for the client and/or worker plus a best-effort fan-out email via the existing MailService. - Notification persistence happens inside the same transaction as the appointment write; the email send is deferred to after that transaction commits and runs on a dedicated async executor, so a slow or failing mail provider never blocks or rolls back the appointment operation. - New GET/PUT endpoints under /api/v1/notifications for paginated listing, unread count, and mark-as-read (single/bulk), all scoped to the caller's email so a user can only see and mark their own notifications. - Fixes a pre-existing gap where bookAppointment never resolved a managed Client entity, which notifications (and correct persistence) depend on. - Adds unit/integration tests, including a mail-provider-down case and ownership-scoping coverage at the HTTP layer. See backend-api/docs/NOTIFICATION_SERVICE.md for the full write-up.
Approve pending a few small, targeted changes and documentation/ops follow-ups. I've reviewed the PR description, test notes, and the high-level changes you listed. This implements a long-awaited Notification Service with persisted in-app notifications, transactional email fan-out (deferred to after commit), new controller endpoints, and tests. Below is a concise, actionable review: summary first, then core/other changes, merge-readiness, specific actionable suggestions, and a few follow-ups I can take on for you. Actionable file-scoped suggestions
Possible improvements (non-blocking suggestions)
Summary recommendation
|
|
thank you for the review, i am currently making the necessary changes |
… ops notes Addresses maintainer review on workman-labs#47: - Cap GET /api/v1/notifications page size at 100 via spring.data.web.pageable.max-page-size; Spring Data's own default (2000) was too high to call safe for a public list endpoint. - Bump the mail-provider-failure log in NotificationEmailDispatcher to ERROR (was WARN) so it's actionable for log-based alerting, and document why a metrics counter isn't added here (no Micrometer/Actuator on the classpath yet — a new dependency deserving its own PR). - Document why Notification intentionally has no @Version: every mutation after insert is idempotent and monotonic, and already scoped by recipientEmail first, so optimistic locking would only add a spurious exception on a harmless race. - Add NotificationEmailFanOutIntegrationTest: an end-to-end proof (real AppointmentService, real transaction, real @async executor, mocked MailService throwing) that a down mail provider still lets bookAppointment commit and leaves both notifications persisted as emailStatus=FAILED, complementing the existing dispatcher-level unit test. - docs/NOTIFICATION_SERVICE.md: new Pagination, Indexing & retention, Observability, and Concurrency sections covering the above plus the already-present composite indexes and the deliberate 404-vs-403 ownership design. The other points raised (transactional deferral, bulk-update scoping, cross-user access tests, JWT-derived recipient) were already covered by the existing implementation and tests; no code change needed there.
|
Thanks for the thorough review — addressed in 880c1ff. Summary of what changed vs. what was already covered: Changed:
Already covered by the existing implementation (no change needed, called out explicitly per your ask):
|
CI (test.yml) failed on NotificationControllerTest with "sorry, too many clients already": this suite has ~20 @SpringBootTest classes, each distinct properties/@MockBean combination gets its own cached ApplicationContext (and its own HikariCP pool) that stays open for the rest of the test JVM's life, and with Spring's default context-cache size (32) enough of them can be alive at once to exceed the CI Postgres container's default max_connections (100). Adding NotificationEmailFanOutIntegrationTest's context was what tipped it over; first commit on this branch (before that test existed) passed CI fine. Capping HikariCP's own per-context pool size was tried first and reverted: SlotReservationIntegrationTest and EscrowOrchestrationIntegrationTest deliberately drive 8-12 concurrent threads against a single context to exercise real locking/idempotency behaviour, and shrinking that context's pool starves them well before Hikari's default (10) would -- it broke EscrowOrchestrationIntegrationTest's 8-thread test locally. The actual lever is how many separate contexts (and pools) are alive simultaneously, not how big any one pool is: spring.test.context.cache.maxSize bounds that directly via LRU eviction (which closes the evicted context's DataSource), capping worst-case connections at maxSize * Hikari's default pool size regardless of how large this test suite grows. Verified locally against a real Postgres with the same max_connections=100 default as CI: ./mvnw test and ./mvnw verify both pass, 196/196, including the two concurrency-heavy integration tests.
…nced with development for workman-labs#48) into local CI fix # Conflicts: # backend-api/pom.xml
meshackyaro
left a comment
There was a problem hiding this comment.
Excellent response to review feedback — approved.
@daniella-techie, this follow-up commit demonstrates exactly the kind of thoughtfulness we're after. You didn't just rush changes; you addressed each point deliberately and with depth:
Page size cap — You're right that 2000 was too permissive. Capping at 100 with a clear application property (spring.data.web.pageable.max-page-size) is the right call for a public endpoint, and the comment explaining why is exactly the kind of thing that saves the next person 30 minutes of wondering.
Pagination and observability docs — The new sections in NOTIFICATION_SERVICE.md fill real gaps: explicitly calling out the index matching the default sort, the trade-offs on email error observability (why there's no counter yet, and what log-based alerting buys us in the interim), and the honest note about retention being a follow-up. That's the kind of transparency that helps the team plan and scale.
The integration test — This is the piece I was most eager to see. NotificationEmailFanOutIntegrationTest running the full stack (real AppointmentService, real transaction, real async executor, mocked mail provider throwing) is the only way to actually prove the guarantee: down mail provider → booking succeeds → notifications land as FAILED. Unit test + integration test together = confidence.
Concurrency reasoning — Your note on why @Version is deliberately omitted is exactly right. Two mark-read calls racing to the same state don't need optimistic locking; they need idempotent, scoped writes — which you already have. Nice catch on avoiding the spurious exception.
Mail error logging — Bumping to ERROR with the full exception (not just the message) is the move. An operator who needs to debug this later will thank you.
This is how iteration works on production code. Ship, listen, respond thoughtfully. Ship this.
Approved & ready to merge.
Summary
Implements the Notification Service issue: appointment lifecycle changes now
produce a persisted, readable in-app notification and a corresponding
transactional email, instead of silently changing state that nobody is told
about.
NotificationService/NotificationServiceImplagainst the existing
Notificationmodel andNotificationRepository.updated, cancelled, deleted — in
AppointmentServiceImplandSlotReservationService(the two-step booking flow) so both the client andthe worker are notified as appropriate.
/api/v1/notifications: paginated list, unread count,mark-as-read (single and bulk) — all scoped to the caller's email so a user
can only see and mark their own notifications.
MailService, but the send isdeferred until after the appointment transaction commits and runs on a
dedicated async executor — a slow or failing mail provider can never block
or roll back the appointment operation. A failed send is recorded on the
notification (
emailStatus), never rethrown.bookAppointmentnever resolved a managedCliententity (only the worker), which notifications — and correctpersistence — depend on.
NotificationNotFoundException→ 404, wired into the existing RFC 7807GlobalExceptionHandler; endpoints documented via springdoc.No new dependencies and no migration file — see
backend-api/docs/NOTIFICATION_SERVICE.mdfor the full architecturalwrite-up, including why the primary key column keeps its old name under this
codebase's
ddl-auto=updateschema strategy.Test plan
NotificationServiceImplTest— recipient selection per lifecycle event,content, pagination delegation, ownership-scoped read/mark-read
NotificationEmailDispatcherTest— the mail-provider-down case: athrown exception is caught, marks the notification
FAILED, neverpropagates
AppointmentServiceImplTest— each lifecycle method notifies with thecorrect
NotificationTypeNotificationControllerTest— HTTP-level ownership scoping against areal JWT and database
MailServiceso theydon't make real calls to the mail provider now that booking triggers a
notification fan-out
./mvnw testand./mvnw verifyboth pass locallytest.yml) already caches Maven dependencies (cache: maven) — nochange needed
closes #44