Skip to content

feat(phase2-slice1): booking decline + provider profile CRUD#5

Merged
imKXNNY merged 3 commits into
mainfrom
feature/phase2-provider-profile-booking-decline
Mar 28, 2026
Merged

feat(phase2-slice1): booking decline + provider profile CRUD#5
imKXNNY merged 3 commits into
mainfrom
feature/phase2-provider-profile-booking-decline

Conversation

@imKXNNY

@imKXNNY imKXNNY commented Mar 28, 2026

Copy link
Copy Markdown
Collaborator

Phase 2 — Slice 1: Marketplace MVP Kickoff

Phase 1 exit criteria are met (PR #4 merged). This PR kicks off Phase 2 with the first two foundational features of the marketplace loop.


What's in this PR

🔻 Booking Decline

Providers can now decline a submitted booking. Mirrors the accept flow with idempotency, audit trail, and domain event emission.

  • BookingStatus extended: 'submitted' | 'accepted' | 'declined'
  • POST /api/v1/bookings/:bookingId/decline (provider-only)
  • Optional declineReason stored on the record
  • booking.declined domain event emitted (same structured pattern as booking.accepted)
  • Postgres repository updated to maintain interface parity
  • 5 new service tests; app-level action tests

👤 Provider Profile CRUD

Providers can create/update a public-facing profile (display name, bio, trade categories, service area, visibility).

  • ProviderProfileRepository interface + in-memory implementation (upsert, get, list public)
  • PUT /api/v1/providers/me/profile + GET /api/v1/providers/me/profile (provider-only)
  • Registered in ProvidersModule
  • 7 service tests + 6 repository tests; app state + action tests

⚙️ Worker: booking.declined

  • booking-declined.worker.ts — same retry/DLQ pattern as booking-accepted
  • vitest.config.ts added to background-workers (fixes pre-existing stale dist/ pickup)
  • 7 worker tests

📦 Package updates

  • @quickwerk/domain: BookingDeclinedDomainEvent, worker envelope types
  • @quickwerk/api-client: bookingApiRoutes.decline, providerApiRoutes.myProfile, typed request builders

📋 Planning doc

.agent/plans/phase2-marketplace-mvp-kickoff-2026-03-28.md — full Phase 2 scope, milestones, API surface, testing strategy, risks.


Test totals

Package Tests
platform-api 117/119 pass (2 Postgres integration skipped)
product-app 90/90
background-workers 11/11
admin-web 10/10

All typechecks pass: platform-api, product-app, admin-web, api-client, domain.


Phase 1 behaviour preserved

  • No changes to auth flow, booking create/accept, or provider verification
  • Repository pattern maintained; in-memory is still the active persistence strategy

What comes next (Slice 2)

  • GET /api/v1/providers — public provider list/filter for customer discovery
  • Booking detail enrichment with provider display name
  • booking.declined relay pipeline (mirrors booking.accepted relay)
  • Customer booking list filter by status
  • Phase 2 combined smoke test

Summary by CodeRabbit

  • New Features

    • Providers can decline booking requests with an optional reason.
    • Providers can create, edit, and publish their public profiles.
    • Booking status flow now includes "declined" alongside "submitted" and "accepted".
  • Tests

    • Added comprehensive tests covering booking declines, decline worker behavior, provider profile CRUD, and related API flows.
  • Chores

    • Database migration to support declined status and persist decline reasons.

Phase 2 Slice 1 — Marketplace MVP kickoff:

## Booking Decline
- Extend BookingStatus with 'declined' (submitted → declined transition)
- DeclineSubmittedBooking input/result types in booking.repository domain
- InMemoryBookingRepository.declineSubmittedBooking with idempotency guard
- PostgresBookingRepository.declineSubmittedBooking (Postgres path parity)
- BookingsService.declineBooking — provider-only, emits booking.declined event
- POST /api/v1/bookings/:bookingId/decline endpoint
- declineReason stored on BookingRecord

## Provider Profile
- ProviderProfileRepository interface (upsert, getByProviderId, listPublic)
- InMemoryProviderProfileRepository with partial-update merge semantics
- ProvidersService.upsertProfile + getMyProfile (provider-only)
- PUT /api/v1/providers/me/profile + GET /api/v1/providers/me/profile endpoints
- Registered in ProvidersModule

## Domain Events
- BookingDeclinedDomainEvent + worker envelope types in @quickwerk/domain
- BookingDomainEventPublisher.publishBookingDeclined added to interface
- LoggingBookingDomainEventPublisher + RelayBookingDomainEventPublisher updated
- booking-declined.worker.ts with same retry/DLQ pattern as booking-accepted

## App Layer (@quickwerk/product-app)
- bookingApiRoutes.decline + createDeclineBookingRequest in api-client
- providerApiRoutes.myProfile + createUpsertProviderProfileRequest in api-client
- declineBookingRequest action in product-app booking feature
- ProviderProfileScreenState types + state factories in provider-state.ts
- loadMyProviderProfile + saveMyProviderProfile actions in provider-screen-actions.ts

## Tests
- 5 new decline tests in bookings.service.test.ts
- InMemoryProviderProfileRepository tests (6 cases)
- ProvidersService profile tests (7 cases)
- booking-declined.worker.test.ts (7 cases)
- booking-screen-actions-decline.test.ts (4 cases)
- provider-screen-actions-profile.test.ts (8 cases)
- provider-profile-state.test.ts (6 cases)
- Fix: vitest.config.ts added to background-workers to exclude stale dist/ files

## Planning
- .agent/plans/phase2-marketplace-mvp-kickoff-2026-03-28.md with full Phase 2 scope,
  milestones, technical approach, API changes, testing strategy, and risk log

Test totals: platform-api 117/119, product-app 90/90, background-workers 11/11, admin-web 10/10
All typechecks pass: platform-api, product-app, admin-web, api-client, domain
@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@imKXNNY has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 0 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 0 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3222249-f7b7-4c32-8492-2f6d190add4a

📥 Commits

Reviewing files that changed from the base of the PR and between aabf141 and 9f08fab.

📒 Files selected for processing (3)
  • services/background-workers/src/main.ts
  • services/platform-api/src/orchestration/relay-attempt-policy.ts
  • services/platform-api/src/orchestration/relay-domain-event.publisher.ts
📝 Walkthrough

Walkthrough

Adds provider profile CRUD and booking-decline support across client, API, domain, repositories, event publishing, and background worker plumbing, plus tests, API-client request builders, DB migration, and a Phase 2 plan document.

Changes

Cohort / File(s) Summary
Planning
\.agent/plans/phase2-marketplace-mvp-kickoff-2026-03-28.md
New Phase 2 kickoff plan describing scope, slices, completed slice 1 items, and remaining Phase 2 work.
Client — Booking
apps/product-app/src/features/booking/booking-screen-actions.ts, apps/product-app/src/features/booking/booking-screen-actions-decline.test.ts, apps/product-app/src/features/booking/booking-state.ts, apps/product-app/src/features/booking/booking-state.test.ts
Added declineBookingRequest (fetch integration, error handling), introduced knownBookingStatuses parser, tightened submit validation, added declined status and optional declineReason to booking state, and tests for decline flows.
Client — Provider Profile
apps/product-app/src/features/provider/provider-screen-actions.ts, apps/product-app/src/features/provider/provider-screen-actions-profile.test.ts, apps/product-app/src/features/provider/provider-profile-state.test.ts
Added loadMyProviderProfile and saveMyProviderProfile, payload parsing/coercion, and tests for success/error/not-set scenarios; added provider state tests.
API Client
packages/api-client/src/index.ts
Added decline booking route and myProfile provider routes plus request builders: createDeclineBookingRequest, createGetMyProviderProfileRequest, createUpsertProviderProfileRequest.
Domain Types & Events
packages/domain/src/index.ts
Added 'declined' to bookingStatuses and new domain event types/worker envelope types for booking.declined including retry/DLQ metadata.
Platform API — Bookings
services/platform-api/src/bookings/bookings.controller.ts, .../bookings.service.ts, .../bookings.service.test.ts
New POST /bookings/:bookingId/decline controller; BookingsService.declineBooking enforcing provider role, calling repository decline, emitting BookingDeclinedDomainEvent, returning serialized booking; comprehensive tests (auth, not-found, conflict, idempotency).
Platform API — Booking Repo
services/platform-api/src/bookings/domain/booking.repository.ts, .../in-memory-booking.repository.ts, .../postgres-booking.repository.ts
Extended BookingStatus and BookingRecord (declineReason), added declineSubmittedBooking contract and implementations with transition checks, idempotency, status history updates, and DB row locking; DB reads now map decline_reason.
Platform API — Domain Event Publishing
services/platform-api/src/orchestration/domain-event.publisher.ts, .../logging-domain-event.publisher.ts, .../relay-domain-event.publisher.ts
Added publishBookingDeclined to publisher interface and implementations; unified logging helper; relay publisher runs declined-event relay attempts using background worker utilities and logs attempt breadcrumbs.
Background Workers
services/background-workers/src/workers/booking-declined.worker.ts, .../booking-declined.worker.test.ts, services/background-workers/src/workers/index.ts, services/background-workers/src/index.ts, services/background-workers/vitest.config.ts
New booking-declined worker: envelope builder, deterministic exponential retry metadata, DLQ marker, consume attempt logic; tests for envelope, backoff, DLQ, and consumption outcomes; new exports and pipeline identifier.
Providers Domain & Repo
services/platform-api/src/providers/domain/provider-profile.repository.ts, .../infrastructure/in-memory-provider-profile.repository.ts, .../infrastructure/in-memory-provider-profile.repository.test.ts, .../infrastructure/provider-profile-repository.provider.ts
New ProviderProfile types and repository interface; in-memory implementation with normalization, preservation semantics, list filtering, and tests; provider DI provider added.
Platform API — Providers Controller & Service
services/platform-api/src/providers/providers.controller.ts, services/platform-api/src/providers/providers.service.ts, .../providers.service.profile.test.ts, .../providers.service.test.ts, services/platform-api/src/providers/providers.module.ts
GET/PUT /providers/me/profile endpoints and ProvidersService methods added with role checks, validation, timestamped persistence via profiles repo, serialization helpers, tests, and module provider registration.
Database Migration
services/platform-api/migrations/0006_booking_declined_support.sql
Migration to permit declined status, require provider_user_id for accepted/declined, add decline_reason column, and adjust booking_status_history constraints.

Sequence Diagram(s)

sequenceDiagram
    actor Provider
    participant Client as Client App
    participant API as Platform API
    participant Repo as Booking Repo
    participant Publisher as Event Publisher
    participant Worker as Background Worker

    Provider->>Client: Trigger decline
    Client->>API: POST /api/v1/bookings/{id}/decline
    API->>API: Resolve session & auth (401/403 checks)
    API->>Repo: declineSubmittedBooking(input)
    Repo->>Repo: Validate transition, update status→'declined', persist declineReason
    Repo-->>API: Return updated BookingRecord
    API->>Publisher: publishBookingDeclined(event)
    Publisher->>Worker: Relay/publish declined event (attempt loop)
    Worker->>Worker: Build envelope, compute retry backoff or DLQ
    API-->>Client: 200 { booking: { status: 'declined', declineReason } }
Loading
sequenceDiagram
    actor Provider
    participant Client as Client App
    participant API as Platform API
    participant Repo as Profile Repo

    Provider->>Client: Submit profile (PUT)
    Client->>API: PUT /api/v1/providers/me/profile
    API->>API: Resolve session & validate displayName (401/400)
    API->>Repo: upsertProfile(input with now)
    Repo->>Repo: Normalize fields, preserve createdAt, set updatedAt
    Repo-->>API: Return ProviderProfile
    API-->>Client: 200 { profile }
    Provider->>Client: Request profile (GET)
    Client->>API: GET /api/v1/providers/me/profile
    API->>Repo: getProfileByProviderId(...)
    Repo-->>API: ProviderProfile | null
    API-->>Client: 200 { profile | { status: 'not-set' } }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nudge a booking from submitted to shy,
Profiles planted under a springtime sky.
Events hop out, workers hum and retry,
States tidy up, DBs learn to sigh.
Phase two wiggles its nose — hooray, we try!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% 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 summarizes the main changes in the PR: adding booking decline functionality and provider profile CRUD operations, which are the two core features of Phase 2 Slice 1.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/phase2-provider-profile-booking-decline

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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts (1)

20-27: ⚠️ Potential issue | 🟠 Major

BookingRow and loadBookingRecord also need to include decline_reason.

To maintain parity with the in-memory implementation, the BookingRow type and the SELECT queries in loadBookingRecord/loadBookingById should include the decline_reason column, and mapBookingRecord should map it to declineReason.

🔧 Proposed additions for reading declineReason

Add to BookingRow:

 type BookingRow = {
   id: string;
   customer_user_id: string;
   provider_user_id: string | null;
   requested_service: string;
   status: BookingStatus;
   created_at: Date | string;
+  decline_reason: string | null;
 };

Update SELECT in loadBookingRecord (and similarly in loadBookingById):

       SELECT id::text,
              customer_user_id::text,
              provider_user_id::text,
              requested_service,
              status,
-             created_at
+             created_at,
+             decline_reason
        FROM bookings

Update mapBookingRecord:

   return {
     bookingId: booking.id,
     createdAt: toIsoString(booking.created_at),
     customerUserId: booking.customer_user_id,
     providerUserId: booking.provider_user_id ?? undefined,
     requestedService: booking.requested_service,
     status: booking.status,
+    declineReason: booking.decline_reason ?? undefined,
     statusHistory,
   };

Also applies to: 294-329

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

In
`@services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts`
around lines 20 - 27, The BookingRow type and the SQL read paths are missing the
decline_reason field; update the BookingRow type to include decline_reason:
string | null, modify the SELECT in loadBookingRecord and loadBookingById to
include decline_reason, and update mapBookingRecord to map row.decline_reason to
the domain property declineReason (preserving null handling/typing consistent
with other fields).
🧹 Nitpick comments (6)
services/platform-api/src/orchestration/logging-domain-event.publisher.ts (1)

23-35: Consider extracting shared event-log payload mapping.

publishBookingAccepted and publishBookingDeclined now have near-identical logging blocks; a tiny helper would reduce divergence risk as fields evolve.

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

In `@services/platform-api/src/orchestration/logging-domain-event.publisher.ts`
around lines 23 - 35, Both publishBookingAccepted and publishBookingDeclined
duplicate the same structured logging payload; extract a small helper (e.g.,
buildEventLogPayload or mapDomainEventToLog) that accepts the domain event
(BookingAcceptedDomainEvent | BookingDeclinedDomainEvent) and returns the common
breadcrumb payload (event, correlationId, status, details with eventName,
eventId, replayed, bookingId). Replace the duplicated logStructuredBreadcrumb
calls in publishBookingAccepted and publishBookingDeclined to call this helper
so future field changes are made in one place.
apps/product-app/src/features/booking/booking-state.ts (1)

4-4: Tighten status type to preserve narrowing.

On Line 4, | string makes the union effectively just string, which weakens compile-time guarantees for decline/accept handling.

🔧 Proposed fix
-  status: 'submitted' | 'accepted' | 'declined' | string;
+  status: 'submitted' | 'accepted' | 'declined';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/product-app/src/features/booking/booking-state.ts` at line 4, The status
field's union currently includes "| string" which collapses type narrowing;
update the status declaration in booking-state.ts (the status property on the
Booking/booking state type) to remove "| string" and use only the explicit
literals "submitted" | "accepted" | "declined" (or add any specific allowed
literals you actually support) so TypeScript can narrow checks for
accepted/declined branches correctly.
apps/product-app/src/features/provider/provider-profile-state.test.ts (1)

39-48: Add a serviceArea mapping assertion in the loaded-state test.

Line 40 uses a profile with serviceArea, but the loaded-state expectations never verify it reaches the form. This leaves a quiet regression gap.

Suggested test addition
   if (state.status === 'loaded') {
     expect(state.profile.displayName).toBe('Max Muster');
     expect(state.form.displayName).toBe('Max Muster');
     expect(state.form.bio).toBe('Expert plumber');
     expect(state.form.tradeCategories).toEqual(['plumbing']);
+    expect(state.form.serviceArea).toBe('Vienna');
     expect(state.form.isPublic).toBe(true);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/product-app/src/features/provider/provider-profile-state.test.ts` around
lines 39 - 48, The test for createLoadedProfileState omits verifying that
profile.serviceArea is mapped into the form; update the test for
createLoadedProfileState (using sampleProfile and the returned state) to assert
that when state.status === 'loaded' the profile.serviceArea equals the
form.serviceArea (e.g.,
expect(state.form.serviceArea).toEqual(state.profile.serviceArea or the expected
array/value from sampleProfile)); keep this check alongside the existing
displayName/bio/tradeCategories/isPublic assertions to prevent regressions in
the serviceArea mapping.
services/platform-api/src/bookings/bookings.service.test.ts (1)

367-385: Strengthen replay idempotency assertion with history length.

In Line 377 replay flow, also assert that statusHistory does not grow (e.g., remains length 2) to catch accidental duplicate decline transitions.

Suggested assertion
   expect(replay.ok).toBe(true);
   if (replay.ok) {
     expect(replay.booking.status).toBe('declined');
+    expect(replay.booking.statusHistory).toHaveLength(2);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/bookings/bookings.service.test.ts` around lines 367
- 385, Add an assertion in the declineBooking replay test to ensure the
booking's statusHistory does not grow on replay: after calling
service.declineBooking(provider, created.booking.bookingId, {}) and inspecting
replay.booking.status, also assert that replay.booking.statusHistory.length
remains the expected length (e.g., 2) to ensure idempotent transitions; locate
this in the 'declineBooking — is idempotent for the same provider' test where
service.declineBooking and replay are used.
services/platform-api/src/bookings/bookings.service.ts (1)

332-345: Consider including declineReason in the domain event payload.

The BookingDeclinedDomainEvent doesn't include declineReason, which could be useful for notification workers to compose messages like "Provider declined: [reason]". If notification workers need this, they'd have to fetch the booking separately.

💡 Optional enhancement to include declineReason in event
     const declinedEvent: BookingDeclinedDomainEvent = {
       eventName: 'booking.declined',
       eventId: randomUUID(),
       occurredAt: new Date().toISOString(),
       correlationId,
       replayed: declined.replayed,
       booking: {
         bookingId: declined.booking.bookingId,
         customerUserId: declined.booking.customerUserId,
         providerUserId: declined.booking.providerUserId ?? session.userId,
         requestedService: declined.booking.requestedService,
         status: 'declined',
+        declineReason: declined.booking.declineReason,
       },
     };

This would require updating BookingDeclinedDomainEvent in packages/domain as well.

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

In `@services/platform-api/src/bookings/bookings.service.ts` around lines 332 -
345, The booking.declined domain event currently omits the declineReason; update
the declinedEvent payload built in bookings.service.ts (variable declinedEvent)
to include declineReason (e.g., booking.declineReason or declined.declineReason)
under booking so notification workers can use it, and bump the
BookingDeclinedDomainEvent type in the domain package to include declineReason
on the booking shape; ensure serialization uses the same string (or nullable)
type as the source booking to avoid type errors.
services/platform-api/src/providers/providers.service.ts (1)

311-314: Log displayName validation failures for observability parity.

The 400 branch returns early without a breadcrumb, which makes bad-request tracing harder than other failure paths.

Proposed refinement
     const displayName = input.displayName?.trim();
     if (!displayName) {
+      logStructuredBreadcrumb({
+        event: 'provider.profile.upsert',
+        correlationId,
+        status: 'failed',
+        details: { reason: 'invalid-display-name', actorUserId: session.userId },
+      });
       return { ok: false, statusCode: 400, error: 'displayName is required.' };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/providers/providers.service.ts` around lines 311 -
314, The early return when displayName is missing should record a breadcrumb for
observability before returning; in the block that computes const displayName =
input.displayName?.trim() and returns { ok: false, statusCode: 400, error:
'displayName is required.' }, add a breadcrumb/log entry using the module's
tracing/logger (the same logger/tracing used elsewhere in providers.service.ts)
that records the validation failure, the incoming input.displayName value (or
that it was undefined/empty), and context like method name so bad-request traces
match other failure paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/product-app/src/features/provider/provider-screen-actions.ts`:
- Around line 184-196: The profile builder in provider-screen-actions (the
object with keys profile -> providerUserId, displayName, bio, tradeCategories,
serviceArea, isPublic, createdAt, updatedAt) currently coerces missing
createdAt/updatedAt to empty strings which accepts malformed payloads; change
parsing so createdAt and updatedAt are required and the parser fails when they
are absent or not valid strings (do not default to ''), and refactor to reuse a
single parser/validator for both load and save paths (e.g., extract a
validateProviderProfile function or schema used by the code that constructs the
profile and the code at the 229-241 block) so both paths enforce the same
required timestamp rules.

In `@apps/product-app/src/features/provider/provider-state.ts`:
- Around line 47-61: createNotSetProfileState and createLoadedProfileState are
returning form objects/arrays by reference, risking shared mutable state; update
both factories to return fresh copies instead of passing references: in
createNotSetProfileState clone initialProviderProfileFormState into a new object
for the form, and in createLoadedProfileState build a new form object by copying
primitive fields and shallow-cloning arrays (e.g., tradeCategories) and any
nested objects so no reference to profile or initial objects is retained. Ensure
the returned ProviderProfileScreenState contains these new cloned structures
rather than original references.

In `@packages/api-client/src/index.ts`:
- Around line 162-168: The client-side type UpsertProviderProfileBody currently
requires displayName which conflicts with the server's partial-update contract;
update the UpsertProviderProfileBody type in packages/api-client/src/index.ts to
make displayName optional (displayName?: string) to match the API controller,
and scan for and update any callers/tests that assumed the field was required so
partial-profile PATCH/PUT requests compile and behave consistently.

In `@packages/domain/src/index.ts`:
- Around line 58-71: The domain introduces BookingDeclinedDomainEvent with
booking.status 'declined' but the canonical bookingStatuses constant/union omits
'declined'; update the bookingStatuses definition to include 'declined' so the
canonical source of truth matches BookingDeclinedDomainEvent and any derived
unions/validation (locate bookingStatuses and the BookingDeclinedDomainEvent
type in packages/domain/src/index.ts and add 'declined' to the statuses list).

In `@services/platform-api/src/bookings/bookings.controller.ts`:
- Around line 200-202: The controller currently dereferences body.declineReason
directly when calling bookingsService.declineBooking, which can throw if body is
null/undefined; first read declineReason safely (e.g. const declineReason =
body?.declineReason) and validate it before calling declineBooking,
returning/throwing a controlled HTTP 400 (BadRequest) if the payload is missing
or invalid; then call this.bookingsService.declineBooking(session, bookingId, {
declineReason }, { correlationId }) so the service always receives a
well-defined value.

In `@services/platform-api/src/bookings/domain/booking.repository.ts`:
- Line 19: BookingRecord now has a new field declineReason but the Postgres
mapper mapBookingRecord in
services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts
does not map it, causing loss of data on reads; update mapBookingRecord to read
the decline_reason (or appropriate DB column) from the DB row and assign it to
BookingRecord.declineReason, and also ensure any reverse mapper (e.g., when
building DB params for insert/update) sets the decline_reason column from the
Booking entity's declineReason so create/update and read mappers are in parity.

In
`@services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts`:
- Around line 191-197: The code references a 'declined' status and persists a
declineReason but the DB schema lacks that support; add a migration that alters
the bookings CHECK constraint to include 'declined', adds a new decline_reason
TEXT column to bookings, and updates any CHECK/foreign-key constraints on
booking_status_history to allow transitions involving 'declined' (so
booking_status_history and bookings constraints accept 'declined' as a valid
status); ensure the migration is idempotent and used by the code that writes
declineReason (declineSubmittedBooking) and any repository methods that update
bookings or booking_status_history.

In `@services/platform-api/src/orchestration/relay-domain-event.publisher.ts`:
- Around line 124-126: publishBookingDeclined currently only calls
loggingPublisher.publishBookingDeclined and thus bypasses the relay retry/DLQ
pipeline; change publishBookingDeclined to mirror the accepted-event path by
delegating the event to the relay pipeline before/after logging (i.e., call the
same relayAttemptExecutor method used for accepted events such as the
relayAttemptExecutor.execute/handle/publish method used by
publishBookingAccepted) so declined events go through retry/DLQ semantics while
still invoking loggingPublisher.publishBookingDeclined.

In
`@services/platform-api/src/providers/infrastructure/in-memory-provider-profile.repository.ts`:
- Around line 19-24: The update logic for bio and serviceArea uses truthy
fallback (input.bio?.trim() || existing?.bio) which prevents explicitly clearing
them with an empty string; change those assignments to check for undefined/null
instead of falsy so an empty trimmed string is accepted (e.g., set bio to
input.bio !== undefined ? input.bio.trim() : existing?.bio and similarly for
serviceArea), leaving tradeCategories and isPublic logic unchanged; update the
assignments in the in-memory-provider-profile.repository.ts where bio and
serviceArea are computed.

---

Outside diff comments:
In
`@services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts`:
- Around line 20-27: The BookingRow type and the SQL read paths are missing the
decline_reason field; update the BookingRow type to include decline_reason:
string | null, modify the SELECT in loadBookingRecord and loadBookingById to
include decline_reason, and update mapBookingRecord to map row.decline_reason to
the domain property declineReason (preserving null handling/typing consistent
with other fields).

---

Nitpick comments:
In `@apps/product-app/src/features/booking/booking-state.ts`:
- Line 4: The status field's union currently includes "| string" which collapses
type narrowing; update the status declaration in booking-state.ts (the status
property on the Booking/booking state type) to remove "| string" and use only
the explicit literals "submitted" | "accepted" | "declined" (or add any specific
allowed literals you actually support) so TypeScript can narrow checks for
accepted/declined branches correctly.

In `@apps/product-app/src/features/provider/provider-profile-state.test.ts`:
- Around line 39-48: The test for createLoadedProfileState omits verifying that
profile.serviceArea is mapped into the form; update the test for
createLoadedProfileState (using sampleProfile and the returned state) to assert
that when state.status === 'loaded' the profile.serviceArea equals the
form.serviceArea (e.g.,
expect(state.form.serviceArea).toEqual(state.profile.serviceArea or the expected
array/value from sampleProfile)); keep this check alongside the existing
displayName/bio/tradeCategories/isPublic assertions to prevent regressions in
the serviceArea mapping.

In `@services/platform-api/src/bookings/bookings.service.test.ts`:
- Around line 367-385: Add an assertion in the declineBooking replay test to
ensure the booking's statusHistory does not grow on replay: after calling
service.declineBooking(provider, created.booking.bookingId, {}) and inspecting
replay.booking.status, also assert that replay.booking.statusHistory.length
remains the expected length (e.g., 2) to ensure idempotent transitions; locate
this in the 'declineBooking — is idempotent for the same provider' test where
service.declineBooking and replay are used.

In `@services/platform-api/src/bookings/bookings.service.ts`:
- Around line 332-345: The booking.declined domain event currently omits the
declineReason; update the declinedEvent payload built in bookings.service.ts
(variable declinedEvent) to include declineReason (e.g., booking.declineReason
or declined.declineReason) under booking so notification workers can use it, and
bump the BookingDeclinedDomainEvent type in the domain package to include
declineReason on the booking shape; ensure serialization uses the same string
(or nullable) type as the source booking to avoid type errors.

In `@services/platform-api/src/orchestration/logging-domain-event.publisher.ts`:
- Around line 23-35: Both publishBookingAccepted and publishBookingDeclined
duplicate the same structured logging payload; extract a small helper (e.g.,
buildEventLogPayload or mapDomainEventToLog) that accepts the domain event
(BookingAcceptedDomainEvent | BookingDeclinedDomainEvent) and returns the common
breadcrumb payload (event, correlationId, status, details with eventName,
eventId, replayed, bookingId). Replace the duplicated logStructuredBreadcrumb
calls in publishBookingAccepted and publishBookingDeclined to call this helper
so future field changes are made in one place.

In `@services/platform-api/src/providers/providers.service.ts`:
- Around line 311-314: The early return when displayName is missing should
record a breadcrumb for observability before returning; in the block that
computes const displayName = input.displayName?.trim() and returns { ok: false,
statusCode: 400, error: 'displayName is required.' }, add a breadcrumb/log entry
using the module's tracing/logger (the same logger/tracing used elsewhere in
providers.service.ts) that records the validation failure, the incoming
input.displayName value (or that it was undefined/empty), and context like
method name so bad-request traces match other failure paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af937faf-a910-4f7b-bf20-c3d668c5e227

📥 Commits

Reviewing files that changed from the base of the PR and between 47ebe3a and 98c5cc6.

📒 Files selected for processing (31)
  • .agent/plans/phase2-marketplace-mvp-kickoff-2026-03-28.md
  • apps/product-app/src/features/booking/booking-screen-actions-decline.test.ts
  • apps/product-app/src/features/booking/booking-screen-actions.ts
  • apps/product-app/src/features/booking/booking-state.ts
  • apps/product-app/src/features/provider/provider-profile-state.test.ts
  • apps/product-app/src/features/provider/provider-screen-actions-profile.test.ts
  • apps/product-app/src/features/provider/provider-screen-actions.ts
  • apps/product-app/src/features/provider/provider-state.ts
  • packages/api-client/src/index.ts
  • packages/domain/src/index.ts
  • services/background-workers/src/workers/booking-declined.worker.test.ts
  • services/background-workers/src/workers/booking-declined.worker.ts
  • services/background-workers/vitest.config.ts
  • services/platform-api/src/bookings/bookings.controller.ts
  • services/platform-api/src/bookings/bookings.service.test.ts
  • services/platform-api/src/bookings/bookings.service.ts
  • services/platform-api/src/bookings/domain/booking.repository.ts
  • services/platform-api/src/bookings/infrastructure/in-memory-booking.repository.ts
  • services/platform-api/src/bookings/infrastructure/postgres-booking.repository.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-domain-event.publisher.ts
  • services/platform-api/src/providers/domain/provider-profile.repository.ts
  • services/platform-api/src/providers/infrastructure/in-memory-provider-profile.repository.test.ts
  • services/platform-api/src/providers/infrastructure/in-memory-provider-profile.repository.ts
  • services/platform-api/src/providers/infrastructure/provider-profile-repository.provider.ts
  • services/platform-api/src/providers/providers.controller.ts
  • services/platform-api/src/providers/providers.module.ts
  • services/platform-api/src/providers/providers.service.profile.test.ts
  • services/platform-api/src/providers/providers.service.test.ts
  • services/platform-api/src/providers/providers.service.ts

Comment thread apps/product-app/src/features/provider/provider-screen-actions.ts Outdated
Comment on lines +47 to +61
export const createNotSetProfileState = (form: ProviderProfileFormState = initialProviderProfileFormState): ProviderProfileScreenState => ({
status: 'not-set',
form,
});

export const createLoadedProfileState = (profile: ProviderProfile): ProviderProfileScreenState => ({
status: 'loaded',
profile,
form: {
displayName: profile.displayName,
bio: profile.bio ?? '',
serviceArea: profile.serviceArea ?? '',
tradeCategories: profile.tradeCategories,
isPublic: profile.isPublic,
},

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.

⚠️ Potential issue | 🟡 Minor

Avoid shared mutable references in form state factories.

createNotSetProfileState and createLoadedProfileState currently pass through object/array references. If form state mutates in place anywhere, state can bleed across screens.

🧩 Proposed fix
 export const createNotSetProfileState = (form: ProviderProfileFormState = initialProviderProfileFormState): ProviderProfileScreenState => ({
   status: 'not-set',
-  form,
+  form: {
+    ...form,
+    tradeCategories: [...form.tradeCategories],
+  },
 });
@@
     displayName: profile.displayName,
     bio: profile.bio ?? '',
     serviceArea: profile.serviceArea ?? '',
-    tradeCategories: profile.tradeCategories,
+    tradeCategories: [...profile.tradeCategories],
     isPublic: profile.isPublic,
   },
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/product-app/src/features/provider/provider-state.ts` around lines 47 -
61, createNotSetProfileState and createLoadedProfileState are returning form
objects/arrays by reference, risking shared mutable state; update both factories
to return fresh copies instead of passing references: in
createNotSetProfileState clone initialProviderProfileFormState into a new object
for the form, and in createLoadedProfileState build a new form object by copying
primitive fields and shallow-cloning arrays (e.g., tradeCategories) and any
nested objects so no reference to profile or initial objects is retained. Ensure
the returned ProviderProfileScreenState contains these new cloned structures
rather than original references.

Comment thread packages/api-client/src/index.ts
Comment thread packages/domain/src/index.ts
Comment on lines +200 to +202
const result = await this.bookingsService.declineBooking(session, bookingId, { declineReason: body.declineReason }, {
correlationId,
});

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.

⚠️ Potential issue | 🟡 Minor

Guard declineReason extraction against null-ish body payloads.

Line 200 dereferences body.declineReason directly. If request body is null/missing, this can throw before your controlled HTTP error handling.

Proposed defensive fix
-    const result = await this.bookingsService.declineBooking(session, bookingId, { declineReason: body.declineReason }, {
+    const result = await this.bookingsService.declineBooking(
+      session,
+      bookingId,
+      { declineReason: body?.declineReason },
+      {
       correlationId,
-    });
+      },
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = await this.bookingsService.declineBooking(session, bookingId, { declineReason: body.declineReason }, {
correlationId,
});
const result = await this.bookingsService.declineBooking(
session,
bookingId,
{ declineReason: body?.declineReason },
{
correlationId,
},
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/bookings/bookings.controller.ts` around lines 200 -
202, The controller currently dereferences body.declineReason directly when
calling bookingsService.declineBooking, which can throw if body is
null/undefined; first read declineReason safely (e.g. const declineReason =
body?.declineReason) and validate it before calling declineBooking,
returning/throwing a controlled HTTP 400 (BadRequest) if the payload is missing
or invalid; then call this.bookingsService.declineBooking(session, bookingId, {
declineReason }, { correlationId }) so the service always receives a
well-defined value.

Comment thread services/platform-api/src/bookings/domain/booking.repository.ts
Comment on lines +191 to +197
await client.query(
`UPDATE bookings
SET status = 'declined',
provider_user_id = $2::uuid
WHERE id = $1::uuid`,
[input.bookingId, input.providerUserId],
);

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the bookings table schema/migration to confirm decline_reason column exists
rg -n "decline_reason|declineReason" --type sql
rg -n "decline_reason|declineReason" -g '*.ts' services/platform-api/src/

Repository: MarkoAtc/QuickWerk

Length of output: 1218


🏁 Script executed:

# Find migration files and database schema definitions
find . -type f \( -name "*migration*" -o -name "*schema*" -o -name "*.sql" \) | head -20

Repository: MarkoAtc/QuickWerk

Length of output: 423


🏁 Script executed:

# Search for bookings table definition in migrations or schema files
rg -l "CREATE TABLE.*bookings|bookings.*TABLE" --type sql
rg "bookings" -g '*.ts' | grep -i "migration\|schema\|database" | head -20

Repository: MarkoAtc/QuickWerk

Length of output: 125


🏁 Script executed:

# Look for TypeORM entities or database migrations patterns
find . -path "*/migrations/*" -o -path "*/database/*" | head -30

Repository: MarkoAtc/QuickWerk

Length of output: 468


🏁 Script executed:

# Check the initial migration for bookings table schema
cat -n ./services/platform-api/migrations/0001_initial_auth_bookings.sql

Repository: MarkoAtc/QuickWerk

Length of output: 2730


🏁 Script executed:

# Search all migration files for decline_reason
rg -n "decline_reason|declineReason" ./services/platform-api/migrations/

Repository: MarkoAtc/QuickWerk

Length of output: 44


🏁 Script executed:

# Check all migration files for bookings table modifications
for file in ./services/platform-api/migrations/*.sql; do
  echo "=== $(basename $file) ===";
  cat -n "$file" | head -50;
done

Repository: MarkoAtc/QuickWerk

Length of output: 6222


🏁 Script executed:

# Search for "declined" status in all migrations
rg -n "declined|DECLINED" ./services/platform-api/migrations/

Repository: MarkoAtc/QuickWerk

Length of output: 44


🏁 Script executed:

# Read the declineSubmittedBooking method in postgres-booking.repository.ts to understand the context
sed -n '155,225p' ./services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts

Repository: MarkoAtc/QuickWerk

Length of output: 2324


🏁 Script executed:

# Check if there are any pending/draft migrations
ls -la ./services/platform-api/migrations/ | grep -i draft
find . -name "*decline*" -o -name "*pending*" 2>/dev/null | head -20

Repository: MarkoAtc/QuickWerk

Length of output: 283


🏁 Script executed:

# Verify the bookings table structure with comment about which PR added 'declined' status
git log --oneline --all | grep -i "decline\|status" | head -10

Repository: MarkoAtc/QuickWerk

Length of output: 130


🏁 Script executed:

# Check the git diff for commit 98c5cc6 to see if migration was included
git show --name-only 98c5cc6 | head -30

Repository: MarkoAtc/QuickWerk

Length of output: 1552


🏁 Script executed:

# Check booking_status_history constraint - does it need to allow 'declined'?
rg -n "to_status|from_status" ./services/platform-api/migrations/0001_initial_auth_bookings.sql

Repository: MarkoAtc/QuickWerk

Length of output: 303


🏁 Script executed:

# Check if there's a pending migration file that hasn't been committed
find ./services/platform-api/migrations -type f -newer ./services/platform-api/migrations/0005_operator_role_support.sql 2>/dev/null

Repository: MarkoAtc/QuickWerk

Length of output: 44


Missing database migration to support 'declined' status and declineReason persistence.

The declineSubmittedBooking implementation references a 'declined' status and attempts to persist declineReason, but the required database schema changes were never created. The bookings table schema only allows status IN ('submitted', 'accepted') and lacks a decline_reason column. Attempting to execute this code will fail with a PostgreSQL CHECK constraint violation.

Create a migration that:

  1. Updates the bookings table CHECK constraint to include 'declined' status
  2. Adds a decline_reason TEXT column
  3. Updates the booking_status_history constraints to allow 'declined' transitions

Without this migration, the feature is non-functional in PostgreSQL mode.

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

In
`@services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts`
around lines 191 - 197, The code references a 'declined' status and persists a
declineReason but the DB schema lacks that support; add a migration that alters
the bookings CHECK constraint to include 'declined', adds a new decline_reason
TEXT column to bookings, and updates any CHECK/foreign-key constraints on
booking_status_history to allow transitions involving 'declined' (so
booking_status_history and bookings constraints accept 'declined' as a valid
status); ensure the migration is idempotent and used by the code that writes
declineReason (declineSubmittedBooking) and any repository methods that update
bookings or booking_status_history.

Comment on lines +19 to +24
bio: input.bio?.trim() || existing?.bio,
tradeCategories: (input.tradeCategories ?? existing?.tradeCategories ?? [])
.map((c) => c.trim())
.filter(Boolean),
serviceArea: input.serviceArea?.trim() || existing?.serviceArea,
isPublic: input.isPublic ?? existing?.isPublic ?? false,

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.

⚠️ Potential issue | 🟠 Major

Allow explicit clearing of bio and serviceArea.

Current fallback logic keeps old values when the client sends an empty string, so optional fields cannot be cleared once set. That breaks update semantics for editable optional fields.

Proposed fix
-      bio: input.bio?.trim() || existing?.bio,
+      bio: input.bio !== undefined ? input.bio.trim() || undefined : existing?.bio,
       tradeCategories: (input.tradeCategories ?? existing?.tradeCategories ?? [])
         .map((c) => c.trim())
         .filter(Boolean),
-      serviceArea: input.serviceArea?.trim() || existing?.serviceArea,
+      serviceArea:
+        input.serviceArea !== undefined ? input.serviceArea.trim() || undefined : existing?.serviceArea,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bio: input.bio?.trim() || existing?.bio,
tradeCategories: (input.tradeCategories ?? existing?.tradeCategories ?? [])
.map((c) => c.trim())
.filter(Boolean),
serviceArea: input.serviceArea?.trim() || existing?.serviceArea,
isPublic: input.isPublic ?? existing?.isPublic ?? false,
bio: input.bio !== undefined ? input.bio.trim() || undefined : existing?.bio,
tradeCategories: (input.tradeCategories ?? existing?.tradeCategories ?? [])
.map((c) => c.trim())
.filter(Boolean),
serviceArea:
input.serviceArea !== undefined ? input.serviceArea.trim() || undefined : existing?.serviceArea,
isPublic: input.isPublic ?? existing?.isPublic ?? false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform-api/src/providers/infrastructure/in-memory-provider-profile.repository.ts`
around lines 19 - 24, The update logic for bio and serviceArea uses truthy
fallback (input.bio?.trim() || existing?.bio) which prevents explicitly clearing
them with an empty string; change those assignments to check for undefined/null
instead of falsy so an empty trimmed string is accepted (e.g., set bio to
input.bio !== undefined ? input.bio.trim() : existing?.bio and similarly for
serviceArea), leaving tradeCategories and isPublic logic unchanged; update the
assignments in the in-memory-provider-profile.repository.ts where bio and
serviceArea are computed.

Must-fix items resolved:
- postgres-booking.repository: add decline_reason to BookingRow, both
  SELECT queries, and mapBookingRecord -> declineReason mapping; also
  write decline_reason in declineSubmittedBooking UPDATE
- provider-screen-actions: enforce required createdAt/updatedAt parsing
  (no empty-string fallback); extract shared parseProviderProfilePayload
  validator reused by load + save paths
- provider-state factories: clone tradeCategories array in
  createLoadedProfileState to avoid shared mutable ref
- api-client UpsertProviderProfileBody: make displayName optional to
  match server partial-update contract
- packages/domain bookingStatuses: add 'declined' to the const array
- bookings.controller decline endpoint: safe body access guard before
  reading declineReason; graceful handling of undefined body
- migration 0006: idempotent ALTER TABLE / ADD COLUMN IF NOT EXISTS for
  declined status + decline_reason column on bookings and
  booking_status_history tables
- relay-domain-event.publisher: route booking.declined through in-process
  retry/DLQ pipeline (consumeBookingDeclinedAttempt) mirroring
  booking.accepted; logging publisher still called first for audit trail
- in-memory-provider-profile.repository: use explicit undefined check
  (input.bio !== undefined) so callers can explicitly clear bio/serviceArea

High-value nitpicks included:
- Remove '| string' from product-app CreatedBooking.status union; add
  parseBookingStatus guard in booking-screen-actions.ts
- provider-profile-state.test: assert serviceArea mapping + tradeCategories
  clone isolation
- bookings.service.test: assert statusHistory length unchanged on replay
- bookings.service: include declineReason in BookingDeclinedDomainEvent
  booking payload (domain type already supports it)
- providers.service: emit structured breadcrumb on invalid displayName
- logging-domain-event.publisher: extract buildEventLogDetails helper to
  deduplicate payload mapping between accepted/declined publishers
- background-workers: export consumeBookingDeclinedAttempt and related
  symbols from package index
@imKXNNY

imKXNNY commented Mar 28, 2026

Copy link
Copy Markdown
Collaborator Author

CodeRabbit feedback resolved (review 4025873138)

Commit: aabf141

✅ All must-fix actionable items resolved

# Item File(s)
1 decline_reason added to BookingRow, both SELECT queries, mapBookingRecord mapping, and declineSubmittedBooking UPDATE postgres-booking.repository.ts
2 createdAt/updatedAt now required (no empty-string fallback); shared parseProviderProfilePayload validator reused in load + save paths provider-screen-actions.ts
3 tradeCategories cloned with spread in createLoadedProfileState to avoid shared mutable ref provider-state.ts
4 UpsertProviderProfileBody.displayName made optional to match server partial-update contract packages/api-client/src/index.ts
5 'declined' added to bookingStatuses const array packages/domain/src/index.ts
6 Safe body access guard in declineBooking controller before reading declineReason bookings.controller.ts
7 declineReason written in declineSubmittedBooking UPDATE + read back in both SELECT paths postgres-booking.repository.ts
8 Migration 0006 added: idempotent ALTER TABLE for declined status + decline_reason column on bookings and booking_status_history migrations/0006_booking_declined_support.sql
9 booking.declined routed through in-process retry/DLQ pipeline (consumeBookingDeclinedAttempt) mirroring accepted; logging publisher still called first relay-domain-event.publisher.ts
10 bio/serviceArea use explicit !== undefined check; callers can now explicitly clear these fields in-memory-provider-profile.repository.ts

✅ High-value nitpicks included

  • Removed | string from CreatedBooking.status union; added parseBookingStatus guard in booking-screen-actions
  • Provider-profile-state test: added serviceArea assertion + tradeCategories clone isolation test
  • bookings.service replay test: added explicit comment + assertion that statusHistory length unchanged
  • BookingDeclinedDomainEvent booking payload now includes declineReason; bookings.service passes it through
  • providers.service: structured breadcrumb emitted on invalid displayName before returning 400
  • logging-domain-event.publisher: extracted buildEventLogDetails helper to deduplicate accepted/declined payload mapping
  • background-workers: exported consumeBookingDeclinedAttempt and related symbols from package index

🧪 Test results

platform-api:      117 passed | 2 skipped (postgres integration tests, no DB)
product-app:        91 passed
background-workers: 11 passed
domain:            typecheck clean
api-client:        typecheck clean

All typechecks pass for touched packages: platform-api, product-app, api-client, domain, background-workers.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

@imKXNNY — excellent, thorough resolution pass! Here's a quick acknowledgement of each area:

Must-fix items (10/10 ✅)

  • Postgres persistence is now complete end-to-end (decline_reason in row, both SELECTs, map, and UPDATE).
  • Migration 0006 covers the schema gap with idempotent ALTER TABLE for both bookings and booking_status_history.
  • Controller body guard, bookingStatuses const, and optional displayName on the API-client type all address correctness/contract issues.
  • bio/serviceArea explicit !== undefined guards give callers a clear path to clear those fields.
  • booking.declined now goes through the full retry/DLQ pipeline — parity with booking.accepted achieved.

High-value nitpicks ✅

  • Removing | string from CreatedBooking.status with a parseBookingStatus guard is a solid improvement to type safety.
  • buildEventLogDetails deduplication in the logging publisher is a nice clean-up.
  • The tradeCategories spread clone and serviceArea test assertion close the state-mutation and coverage gaps.
  • declineReason flowing through to BookingDeclinedDomainEvent ensures the worker and downstream consumers have full context.

Triggering a fresh review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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 (2)
services/platform-api/src/orchestration/relay-domain-event.publisher.ts (2)

129-136: Consider extracting duplicate relay logic to reduce code duplication.

The retry/DLQ loop pattern (initial attempt + for-loop with breadcrumb logging) is now duplicated between publishBookingAccepted and publishBookingDeclined. Consider extracting a generic relay helper to improve maintainability as more event types are added.

Also applies to: 151-159

🤖 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 129 - 136, The retry/DLQ pattern duplicated between
publishBookingAccepted and publishBookingDeclined (the initial
consumeBookingAcceptedAttempt/consumeBookingDeclinedAttempt call plus the
for-loop using relayMaxAttempts, relayBaseBackoffMs and now for breadcrumb
logging) should be extracted into a single helper (e.g., relayWithRetry or
publishWithRetry) that accepts the event, a per-attempt handler function
(consumeBookingAcceptedAttempt/consumeBookingDeclinedAttempt), maxAttempts,
baseBackoffMs and now; replace both publishBookingAccepted and
publishBookingDeclined with calls to this helper so the initial attempt, retry
loop, breadcrumb logging, backoff calculation and DLQ handling are centralized.

127-136: Minor inconsistency: now is captured once vs fresh per attempt.

In publishBookingAccepted, this.relayClock.now() is called fresh for each retry attempt (lines 61, 88). Here, now is captured once at Line 127 and reused for all attempts. This may cause retry delay calculations to use a stale timestamp if backoff logic depends on the current time.

Additionally, shouldFail is hardcoded to false whereas the accepted flow uses this.relayAttemptPolicy.shouldFailAttempt(...), which limits chaos/fault-injection testing for declined events.

♻️ Suggested alignment with accepted flow
   async publishBookingDeclined(event: BookingDeclinedDomainEvent): Promise<void> {
     await this.loggingPublisher.publishBookingDeclined(event);

-    const now = this.relayClock.now();
-
     let finalWorkerResult = consumeBookingDeclinedAttempt({
       event,
       attempt: 1,
       maxAttempts: relayMaxAttempts,
       baseBackoffMs: relayBaseBackoffMs,
-      shouldFail: false,
-      now,
+      shouldFail: this.relayAttemptPolicy.shouldFailAttempt({
+        event,
+        attempt: 1,
+        maxAttempts: relayMaxAttempts,
+      }),
+      now: this.relayClock.now(),
     });

Apply similarly in the retry loop.

🤖 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 127 - 136, The declined-event path captures now once and hardcodes
shouldFail, causing stale timestamps and disabling fault-injection; update the
declined flow (calls to consumeBookingDeclinedAttempt) to mirror
publishBookingAccepted by calling this.relayClock.now() inside each
attempt/retry and compute shouldFail via
this.relayAttemptPolicy.shouldFailAttempt(attempt, maxAttempts) (use
relayMaxAttempts/relayBaseBackoffMs and the same call sites where
consumeBookingDeclinedAttempt is invoked) so each attempt receives a fresh now
and the proper shouldFail value.
🤖 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/background-workers/src/workers/index.ts`:
- Around line 13-20: Add a consumer registration for the booking-declined flow:
update the consumers array in main.ts to include an object with eventName:
'booking.declined' and handler: 'consumeBookingDeclinedAttempt' so the existing
consumeBookingDeclinedAttempt handler is wired (mirror the
booking-accepted-orchestration entry pattern); ensure the handler name exactly
matches consumeBookingDeclinedAttempt and that the eventName is
'booking.declined'.

---

Nitpick comments:
In `@services/platform-api/src/orchestration/relay-domain-event.publisher.ts`:
- Around line 129-136: The retry/DLQ pattern duplicated between
publishBookingAccepted and publishBookingDeclined (the initial
consumeBookingAcceptedAttempt/consumeBookingDeclinedAttempt call plus the
for-loop using relayMaxAttempts, relayBaseBackoffMs and now for breadcrumb
logging) should be extracted into a single helper (e.g., relayWithRetry or
publishWithRetry) that accepts the event, a per-attempt handler function
(consumeBookingAcceptedAttempt/consumeBookingDeclinedAttempt), maxAttempts,
baseBackoffMs and now; replace both publishBookingAccepted and
publishBookingDeclined with calls to this helper so the initial attempt, retry
loop, breadcrumb logging, backoff calculation and DLQ handling are centralized.
- Around line 127-136: The declined-event path captures now once and hardcodes
shouldFail, causing stale timestamps and disabling fault-injection; update the
declined flow (calls to consumeBookingDeclinedAttempt) to mirror
publishBookingAccepted by calling this.relayClock.now() inside each
attempt/retry and compute shouldFail via
this.relayAttemptPolicy.shouldFailAttempt(attempt, maxAttempts) (use
relayMaxAttempts/relayBaseBackoffMs and the same call sites where
consumeBookingDeclinedAttempt is invoked) so each attempt receives a fresh now
and the proper shouldFail value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0cc0f565-e5d0-43fe-b2ea-da938921a6df

📥 Commits

Reviewing files that changed from the base of the PR and between 98c5cc6 and aabf141.

📒 Files selected for processing (19)
  • apps/product-app/src/features/booking/booking-screen-actions.ts
  • apps/product-app/src/features/booking/booking-state.test.ts
  • apps/product-app/src/features/booking/booking-state.ts
  • apps/product-app/src/features/provider/provider-profile-state.test.ts
  • apps/product-app/src/features/provider/provider-screen-actions.ts
  • apps/product-app/src/features/provider/provider-state.ts
  • packages/api-client/src/index.ts
  • packages/domain/src/index.ts
  • services/background-workers/src/index.ts
  • services/background-workers/src/workers/index.ts
  • services/platform-api/migrations/0006_booking_declined_support.sql
  • services/platform-api/src/bookings/bookings.controller.ts
  • services/platform-api/src/bookings/bookings.service.test.ts
  • services/platform-api/src/bookings/bookings.service.ts
  • services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts
  • services/platform-api/src/orchestration/logging-domain-event.publisher.ts
  • services/platform-api/src/orchestration/relay-domain-event.publisher.ts
  • services/platform-api/src/providers/infrastructure/in-memory-provider-profile.repository.ts
  • services/platform-api/src/providers/providers.service.ts
✅ Files skipped from review due to trivial changes (3)
  • apps/product-app/src/features/booking/booking-state.test.ts
  • services/platform-api/migrations/0006_booking_declined_support.sql
  • apps/product-app/src/features/provider/provider-profile-state.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/product-app/src/features/booking/booking-state.ts
  • services/platform-api/src/orchestration/logging-domain-event.publisher.ts
  • services/platform-api/src/bookings/bookings.controller.ts
  • packages/domain/src/index.ts
  • services/platform-api/src/providers/infrastructure/in-memory-provider-profile.repository.ts
  • services/platform-api/src/providers/providers.service.ts
  • apps/product-app/src/features/provider/provider-state.ts
  • services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts

Comment thread services/background-workers/src/workers/index.ts
@imKXNNY

imKXNNY commented Mar 28, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the latest actionable re-review feedback in commit 9f08fab.

Changes made:

  • Relay declined path now mirrors accepted retry semantics for attempt metadata:
    • uses per-attempt fresh clock timestamps (this.relayClock.now())
    • uses relay attempt policy (this.relayAttemptPolicy.shouldFailAttempt(...)) instead of hardcoded false
  • Extended relay attempt policy context typing to support both booking.accepted and booking.declined events.
  • Added booking.declined consumer registration in background-workers bootstrap summary (services/background-workers/src/main.ts) with handler consumeBookingDeclinedAttempt.

Validation rerun:

  • corepack pnpm --filter @quickwerk/platform-api typecheck
  • corepack pnpm --filter @quickwerk/platform-api test -- src/orchestration/relay-domain-event.publisher.test.ts
  • corepack pnpm --filter @quickwerk/background-workers typecheck
  • corepack pnpm --filter @quickwerk/background-workers test -- src/main.test.ts

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