Skip to content

feat(background-workers,platform-api): booking notification relay parity (TEC-81)#28

Merged
imKXNNY merged 2 commits into
mainfrom
feature/tec-81-notification-relay
Apr 15, 2026
Merged

feat(background-workers,platform-api): booking notification relay parity (TEC-81)#28
imKXNNY merged 2 commits into
mainfrom
feature/tec-81-notification-relay

Conversation

@imKXNNY

@imKXNNY imKXNNY commented Apr 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • TEC-81: Booking created/completed notification relay parity
    • Shared domain contracts for booking.created / booking.completed events in packages/domain
    • Worker envelopes for both event types in services/background-workers
    • Booking write paths publish domain events through relay
    • Retry metadata, DLQ markers, notification payloads wired
    • Relay breadcrumbs in domain-event.publisher + relay-domain-event.publisher

Test plan

  • CI validate passes
  • CodeRabbit findings reviewed and resolved
  • booking-created and booking-completed worker tests pass
  • Integration tests for relay accept path pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • System now emits booking.created and booking.completed events and relays them through new background workflows that queue email/push notifications with retry and dead‑letter handling.
  • Tests

    • Added comprehensive tests for booking-created and booking-completed workers covering retry/backoff, DLQ behavior, notification payloads, correlation logging, and deterministic retry scheduling.

…fication relay parity (TEC-81)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d459fdeb-9567-41e5-ab9c-314bbf06cd7f

📥 Commits

Reviewing files that changed from the base of the PR and between fae82c4 and f8b8478.

📒 Files selected for processing (1)
  • services/platform-api/src/bookings/bookings.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/platform-api/src/bookings/bookings.service.ts

📝 Walkthrough

Walkthrough

Adds booking.created and booking.completed domain events, types, notification payloads, background-worker implementations with retry/backoff and DLQ support, test coverage, and integrates publication and relay logic into the bookings service and platform event publishers.

Changes

Cohort / File(s) Summary
Domain Types
packages/domain/src/index.ts
Added BookingCreatedDomainEvent, BookingCompletedDomainEvent, retry/backoff metadata, DLQ marker types, worker envelope types, and notification payload types for both events.
Booking Created Worker
services/background-workers/src/workers/booking-created.worker.ts, services/background-workers/src/workers/booking-created.worker.test.ts
New worker implementation: envelope builder, DLQ marker, email/push payload builders, and consumeBookingCreatedAttempt with retry/DLQ logic; comprehensive Vitest suite covering backoff, DLQ, state transitions, and breadcrumb logging.
Booking Completed Worker
services/background-workers/src/workers/booking-completed.worker.ts, services/background-workers/src/workers/booking-completed.worker.test.ts
New worker implementation mirroring created-worker APIs and behavior plus tests validating envelope creation, retry scheduling, DLQ marking, notifications, and breadcrumb logging.
Worker Exports / Index
services/background-workers/src/workers/index.ts, services/background-workers/src/index.ts
Re-exported new worker functions (build*WorkerEnvelope, consume*Attempt, mark*Dlq) and added pipeline identifiers booking-created-orchestration and booking-completed-orchestration.
Bookings Service
services/platform-api/src/bookings/bookings.service.ts
Publishes booking.created after create; publishes booking.completed in both replay and normal completion paths; adds providerUserId presence checks (throws if missing); removed fallback to session.userId in a capture path.
Domain Event Publisher Interfaces
services/platform-api/src/orchestration/domain-event.publisher.ts, services/platform-api/src/orchestration/logging-domain-event.publisher.ts
Interface and logging publisher extended with publishBookingCreated and publishBookingCompleted methods and corresponding structured breadcrumb emits.
Relay Publisher
services/platform-api/src/orchestration/relay-domain-event.publisher.ts, services/platform-api/src/orchestration/relay-attempt-policy.ts
Added publishBookingCreated and publishBookingCompleted relay loops that call background-worker attempts with retry/DLQ handling and breadcrumb logging; expanded relay attempt context event union to include created/completed events.
Tests / Integrations
services/platform-api/src/bookings/*.test.ts, services/platform-api/src/bookings/booking-accept-relay.integration.test.ts
Extended test helpers and mocks to capture created/completed domain events; updated assertions and deterministic retry timestamp expectations in relay integration test.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant BookingsService
    participant DomainEventPublisher
    participant RelayPublisher
    participant BackgroundWorker
    participant NotificationQueue

    Client->>BookingsService: createBooking(...)
    BookingsService->>BookingsService: persist booking
    BookingsService->>DomainEventPublisher: publishBookingCreated(event)
    DomainEventPublisher->>RelayPublisher: relay booking.created

    loop attempt 1..N
        RelayPublisher->>BackgroundWorker: consumeBookingCreatedAttempt(input)
        BackgroundWorker->>BackgroundWorker: build envelope & log "started"
        alt processed
            BackgroundWorker->>NotificationQueue: enqueue email notification
            BackgroundWorker->>NotificationQueue: enqueue push notification
            BackgroundWorker-->>RelayPublisher: { status: "processed" }
        else retry-scheduled
            BackgroundWorker-->>RelayPublisher: { status: "retry-scheduled", nextAttemptAt }
        else dead-letter
            BackgroundWorker-->>RelayPublisher: { status: "dead-letter", dlq }
        end
        RelayPublisher->>RelayPublisher: log breadcrumb per attempt
    end
Loading
sequenceDiagram
    participant Client
    participant BookingsService
    participant DomainEventPublisher
    participant RelayPublisher
    participant BackgroundWorker
    participant NotificationQueue

    Client->>BookingsService: completeBooking(...)
    alt replay path
        BookingsService->>DomainEventPublisher: publishBookingCompleted(replayed: true, occurredAt)
    else normal path
        BookingsService->>DomainEventPublisher: publishBookingCompleted(replayed: false, occurredAt)
    end

    DomainEventPublisher->>RelayPublisher: relay booking.completed

    loop attempt 1..N
        RelayPublisher->>BackgroundWorker: consumeBookingCompletedAttempt(input)
        BackgroundWorker->>BackgroundWorker: build envelope & log "started"
        alt processed
            BackgroundWorker->>NotificationQueue: enqueue email notification
            BackgroundWorker->>NotificationQueue: enqueue push notification
            BackgroundWorker-->>RelayPublisher: { status: "processed" }
        else retry-scheduled
            BackgroundWorker-->>RelayPublisher: { status: "retry-scheduled", nextAttemptAt }
        else dead-letter
            BackgroundWorker-->>RelayPublisher: { status: "dead-letter", dlq }
        end
        RelayPublisher->>RelayPublisher: log breadcrumb per attempt
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Two events hop in—created, completed, hooray!
Envelopes retry, then backoff away;
DLQs mark the ones that can’t land,
Notifications queued by a careful hand.
Thump-thump, the rabbit logs each relay play.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: implementing booking notification relay parity for two new domain events (booking.created and booking.completed) across background-workers and platform-api.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tec-81-notification-relay

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
services/platform-api/src/bookings/booking-accept-relay.integration.test.ts (1)

327-329: Isolate this assertion from booking.created relay side effects.

The nextAttemptAt expectations now depend on prior clock consumption by booking.created retries, not only booking.accepted. Consider scoping shouldFailAttempt to event.eventName === 'booking.accepted' so this test stays focused and stable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/bookings/booking-accept-relay.integration.test.ts`
around lines 327 - 329, The test's nextAttemptAt assertions are affected by
clock consumption from booking.created retries; update the shouldFailAttempt
logic to only apply to booking.accepted events (e.g., change the predicate to
check event.eventName === 'booking.accepted' before marking shouldFailAttempt
true) so the test isolates booking.accepted behavior from booking.created side
effects and remains stable; locate and modify the shouldFailAttempt usage in the
test handling loop that processes relay events (reference the shouldFailAttempt
variable and the event.eventName checks).
packages/domain/src/index.ts (1)

105-149: Reduce duplicated envelope/metadata contracts to avoid drift.

BookingCreated* and BookingCompleted* retry/DLQ/envelope/notification types are structurally identical except event/queue literals. A shared generic base would make future policy/shape updates safer.

Also applies to: 197-225

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/domain/src/index.ts` around lines 105 - 149, Replace the duplicated
retry/DLQ/envelope contracts with shared generic types: create a single
RetryBackoffMetadata (used by both BookingCreatedRetryBackoffMetadata and
BookingCompletedRetryBackoffMetadata), a generic DlqMarker<QueueName> type to
cover BookingCreatedDlqMarker and BookingCompletedDlqMarker, and a generic
WorkerEnvelope<EventName, EventType, RetryMetadata, DlqMarker?> to cover
BookingCreatedWorkerEnvelope and BookingCompletedWorkerEnvelope; then replace
the duplicated types by aliasing the new generics with the appropriate literal
parameters (e.g., 'booking.created'/'booking.completed' and their corresponding
DLQ queue names) so future changes to backoff shape or dlq shape only need one
edit.
services/platform-api/src/orchestration/relay-attempt-policy.ts (1)

13-18: Consider renaming the policy context to match broadened scope.

BookingAcceptedRelayAttemptContext now covers multiple event families. A neutral name (e.g., BookingRelayAttemptContext) would better reflect usage and reduce future confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/orchestration/relay-attempt-policy.ts` around lines
13 - 18, The context type BookingAcceptedRelayAttemptContext is misnamed given
it now covers multiple event families; rename it to a neutral name like
BookingRelayAttemptContext across the file (and any imports/exports) so the type
accurately reflects its broadened scope, update all references (e.g., in the
RelayAttemptPolicy declaration, function parameters, type aliases and tests) to
use BookingRelayAttemptContext, and ensure the union type for event remains
unchanged and correctly typed where the new name is used.
services/platform-api/src/orchestration/relay-domain-event.publisher.ts (1)

59-128: Consider extracting the common relay loop into a generic helper.

Both publishBookingCreated and publishBookingCompleted (and similarly publishBookingDeclined and publishPaymentCaptured) share nearly identical structure:

  1. Call logging publisher
  2. Initial consume attempt
  3. Log breadcrumb
  4. Retry loop while status is 'retry-scheduled'
  5. Final breadcrumb

This pattern could be extracted into a reusable generic function parameterized by event type and consume function, reducing ~140 lines of duplication.

♻️ Sketch of a generic relay helper
// Example approach (outside changed line ranges)
function relayDomainEvent<TEvent extends { correlationId: string; eventId: string }, TResult extends { status: string; correlationId: string; envelope: { retry: unknown; dlq?: unknown } }>(
  eventName: string,
  consume: (input: { event: TEvent; attempt: number; maxAttempts: number; baseBackoffMs: number; shouldFail: boolean; now: Date }) => TResult,
  event: TEvent,
  policy: BookingAcceptedRelayAttemptPolicy,
  clock: BookingAcceptedRelayClock,
): TResult {
  let result = consume({ event, attempt: 1, maxAttempts: relayMaxAttempts, baseBackoffMs: relayBaseBackoffMs, shouldFail: policy.shouldFailAttempt({ event, attempt: 1, maxAttempts: relayMaxAttempts }), now: clock.now() });
  logStructuredBreadcrumb({ event: `${eventName}.domain-event.relay.attempt`, ... });
  
  for (let attempt = 2; attempt <= relayMaxAttempts && result.status === 'retry-scheduled'; attempt++) {
    result = consume({ ... });
    logStructuredBreadcrumb({ ... });
  }
  
  logStructuredBreadcrumb({ event: `${eventName}.domain-event.relay`, ... });
  return result;
}

Also applies to: 277-346

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/orchestration/relay-domain-event.publisher.ts`
around lines 59 - 128, The publishBookingCreated method duplicates a relay/retry
loop present in publishBookingCompleted (and others); extract that pattern into
a generic helper (e.g., relayDomainEvent) that accepts the eventName string, the
consume function (e.g., consumeBookingCreatedAttempt /
consumeBookingCompletedAttempt), the event, the attempt policy
(relayAttemptPolicy), the clock (relayClock), and the shared constants
relayMaxAttempts and relayBaseBackoffMs, and returns the final worker result;
the helper should perform the initial consume call, log per-attempt breadcrumbs
via logStructuredBreadcrumb using mapRelayAttemptStatus, loop retries while
status === 'retry-scheduled', log the final breadcrumb, and then replace the
duplicated blocks inside publishBookingCreated, publishBookingCompleted (and
publishBookingDeclined / publishPaymentCaptured) to call this helper and forward
the result.
services/background-workers/src/workers/booking-completed.worker.ts (1)

30-48: Consider consolidating duplicated worker logic with booking-created.worker.ts.

The structure is nearly identical to booking-created.worker.ts:

  • Same backoff formula
  • Same DLQ marking pattern
  • Same consume flow with state machine
  • Only differences are event names, DLQ queue names, and notification copy

A generic worker factory could parameterize these differences and eliminate ~150 lines of duplication across both files.

Also applies to: 50-62, 64-91, 93-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/background-workers/src/workers/booking-completed.worker.ts` around
lines 30 - 48, The buildBookingCompletedWorkerEnvelope and the
booking-created.worker.ts logic are duplicated; create a generic worker factory
(e.g., buildBookingWorkerFactory or createBookingWorker) that encapsulates the
shared backoff formula, retry object construction (use retryStrategy), DLQ
marking logic, and consume/state-machine flow, and parameterize it with
eventName, DLQ queue name, notification copy/content, and any small per-event
transforms; replace buildBookingCompletedWorkerEnvelope and the booking-created
equivalents to call the factory with their specific eventName
('booking.completed' / 'booking.created'), correlation mapping, and notification
text, leaving types like
BookingCompletedWorkerEnvelope/BookingCreatedWorkerEnvelope or a common
BookingWorkerEnvelope to satisfy typings. Ensure the factory exposes helper(s)
used in both files (e.g., computeBackoffMs, buildEnvelope, consumeHandler) so
tests and imports require minimal changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/platform-api/src/bookings/bookings.service.ts`:
- Around line 489-490: Replace the non-authoritative fallback to session.userId
when building booking.completed events: use the persisted provider ID from the
booking/replayed payment instead of session.userId (e.g., use
bookingToComplete.providerUserId or replayedPayment.providerUserId where
available) and throw an error if that persisted providerUserId is missing so we
fail fast; update the two sites currently using
"bookingToComplete.providerUserId ?? session.userId" (and the analogous use at
the replay/completion branch) to require the persisted ID and remove the session
fallback.

---

Nitpick comments:
In `@packages/domain/src/index.ts`:
- Around line 105-149: Replace the duplicated retry/DLQ/envelope contracts with
shared generic types: create a single RetryBackoffMetadata (used by both
BookingCreatedRetryBackoffMetadata and BookingCompletedRetryBackoffMetadata), a
generic DlqMarker<QueueName> type to cover BookingCreatedDlqMarker and
BookingCompletedDlqMarker, and a generic WorkerEnvelope<EventName, EventType,
RetryMetadata, DlqMarker?> to cover BookingCreatedWorkerEnvelope and
BookingCompletedWorkerEnvelope; then replace the duplicated types by aliasing
the new generics with the appropriate literal parameters (e.g.,
'booking.created'/'booking.completed' and their corresponding DLQ queue names)
so future changes to backoff shape or dlq shape only need one edit.

In `@services/background-workers/src/workers/booking-completed.worker.ts`:
- Around line 30-48: The buildBookingCompletedWorkerEnvelope and the
booking-created.worker.ts logic are duplicated; create a generic worker factory
(e.g., buildBookingWorkerFactory or createBookingWorker) that encapsulates the
shared backoff formula, retry object construction (use retryStrategy), DLQ
marking logic, and consume/state-machine flow, and parameterize it with
eventName, DLQ queue name, notification copy/content, and any small per-event
transforms; replace buildBookingCompletedWorkerEnvelope and the booking-created
equivalents to call the factory with their specific eventName
('booking.completed' / 'booking.created'), correlation mapping, and notification
text, leaving types like
BookingCompletedWorkerEnvelope/BookingCreatedWorkerEnvelope or a common
BookingWorkerEnvelope to satisfy typings. Ensure the factory exposes helper(s)
used in both files (e.g., computeBackoffMs, buildEnvelope, consumeHandler) so
tests and imports require minimal changes.

In `@services/platform-api/src/bookings/booking-accept-relay.integration.test.ts`:
- Around line 327-329: The test's nextAttemptAt assertions are affected by clock
consumption from booking.created retries; update the shouldFailAttempt logic to
only apply to booking.accepted events (e.g., change the predicate to check
event.eventName === 'booking.accepted' before marking shouldFailAttempt true) so
the test isolates booking.accepted behavior from booking.created side effects
and remains stable; locate and modify the shouldFailAttempt usage in the test
handling loop that processes relay events (reference the shouldFailAttempt
variable and the event.eventName checks).

In `@services/platform-api/src/orchestration/relay-attempt-policy.ts`:
- Around line 13-18: The context type BookingAcceptedRelayAttemptContext is
misnamed given it now covers multiple event families; rename it to a neutral
name like BookingRelayAttemptContext across the file (and any imports/exports)
so the type accurately reflects its broadened scope, update all references
(e.g., in the RelayAttemptPolicy declaration, function parameters, type aliases
and tests) to use BookingRelayAttemptContext, and ensure the union type for
event remains unchanged and correctly typed where the new name is used.

In `@services/platform-api/src/orchestration/relay-domain-event.publisher.ts`:
- Around line 59-128: The publishBookingCreated method duplicates a relay/retry
loop present in publishBookingCompleted (and others); extract that pattern into
a generic helper (e.g., relayDomainEvent) that accepts the eventName string, the
consume function (e.g., consumeBookingCreatedAttempt /
consumeBookingCompletedAttempt), the event, the attempt policy
(relayAttemptPolicy), the clock (relayClock), and the shared constants
relayMaxAttempts and relayBaseBackoffMs, and returns the final worker result;
the helper should perform the initial consume call, log per-attempt breadcrumbs
via logStructuredBreadcrumb using mapRelayAttemptStatus, loop retries while
status === 'retry-scheduled', log the final breadcrumb, and then replace the
duplicated blocks inside publishBookingCreated, publishBookingCompleted (and
publishBookingDeclined / publishPaymentCaptured) to call this helper and forward
the result.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bebe86a4-08cb-4044-843d-af02fefd3cf4

📥 Commits

Reviewing files that changed from the base of the PR and between 17b03a3 and fae82c4.

📒 Files selected for processing (15)
  • packages/domain/src/index.ts
  • services/background-workers/src/index.ts
  • services/background-workers/src/workers/booking-completed.worker.test.ts
  • services/background-workers/src/workers/booking-completed.worker.ts
  • services/background-workers/src/workers/booking-created.worker.test.ts
  • services/background-workers/src/workers/booking-created.worker.ts
  • services/background-workers/src/workers/index.ts
  • services/platform-api/src/bookings/booking-accept-relay.integration.test.ts
  • services/platform-api/src/bookings/bookings.service.complete.test.ts
  • services/platform-api/src/bookings/bookings.service.test.ts
  • services/platform-api/src/bookings/bookings.service.ts
  • services/platform-api/src/orchestration/domain-event.publisher.ts
  • services/platform-api/src/orchestration/logging-domain-event.publisher.ts
  • services/platform-api/src/orchestration/relay-attempt-policy.ts
  • services/platform-api/src/orchestration/relay-domain-event.publisher.ts

Comment thread services/platform-api/src/bookings/bookings.service.ts Outdated
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • services/platform-api/src/bookings/bookings.service.ts

Commit: f8b84787bef3e47e7a6d437b8b8de8a37a39f73a

The changes have been pushed to the feature/tec-81-notification-relay branch.

Time taken: 5m 13s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@imKXNNY imKXNNY merged commit ad3b3d8 into main Apr 15, 2026
2 checks passed
@imKXNNY imKXNNY deleted the feature/tec-81-notification-relay branch April 15, 2026 21:42
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