Skip to content

feat(phase3-slice1): booking completion + payment domain event + upload URL stub#10

Merged
imKXNNY merged 6 commits into
mainfrom
feature/phase3-slice1-booking-complete-upload-url-v2
Apr 1, 2026
Merged

feat(phase3-slice1): booking completion + payment domain event + upload URL stub#10
imKXNNY merged 6 commits into
mainfrom
feature/phase3-slice1-booking-complete-upload-url-v2

Conversation

@imKXNNY

@imKXNNY imKXNNY commented Apr 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Phase 3 Slice 1 delivers two vertical features closing Milestones 3a and 3b:

  1. Booking completion + payment capture — provider marks a job done, payment record is created, PaymentCapturedDomainEvent is emitted
  2. Verification document upload URL — provider requests a presigned URL stub for file upload

Both slices follow the exact same repository → service → controller → API client → app actions → tests pattern established in Phase 2.


What's in this PR

Milestone 3a — Booking Completion + Payment

Domain / packages

  • packages/domain: PaymentStatus, PaymentRecord, PaymentCapturedDomainEvent types
  • BookingStatus extended with 'completed'
  • CompleteAcceptedBookingInput / CompleteAcceptedBookingResult types

Booking repository

  • completeAcceptedBooking() — in-memory + postgres implementations
  • Idempotent: same provider re-completing returns replayed: true (200), cross-provider → 409

Payments module (new: services/platform-api/src/payments/)

  • PaymentRepository interface + InMemoryPaymentRepository
  • PaymentsService.capturePaymentForBooking() — idempotent payment capture
  • PaymentsService.getPaymentByBookingId()
  • PaymentsModule exported and imported by BookingsModule

Endpoints

  • POST /api/v1/bookings/:id/complete — provider-only; transitions booking, captures payment, emits domain event
  • GET /api/v1/bookings/:id/payment — returns payment record for booking owner

Background worker

  • payment-captured.worker.ts — stub with deterministic exponential backoff + DLQ (mirrors booking-accepted pattern)

Milestone 3b — Verification Document Upload URL

Domain / packages

  • packages/domain: UploadUrlRecord type

Upload URL repository (new in providers module)

  • UploadUrlRepository interface + InMemoryUploadUrlRepository
  • Stub presigned URL + 15-minute TTL expiry

Endpoint

  • POST /api/v1/providers/me/verification/upload-url — provider-only, returns { uploadId, presignedUrl, expiresAt, filename, mimeType }

API Client + App Actions

  • createCompleteBookingRequest, createGetBookingPaymentRequest, createRequestUploadUrlRequest
  • booking-state.ts: 'completed' added to status union
  • booking-screen-actions.ts: completeBookingRequest
  • provider-screen-actions.ts: requestVerificationUploadUrl

Test coverage

File Tests
bookings.service.complete.test.ts 9
payments.service.test.ts 4
providers.service.upload-url.test.ts 4
booking-screen-actions-complete.test.ts 5
payment-captured.worker.test.ts 8
All packages total 322 passed, 0 failed

TypeScript strict mode: ✅ clean across all 12 packages

Smoke script

scripts/smoke/phase3-slice1-complete-upload-smoke.sh — 13 assertions covering:

  • Full booking lifecycle: submit → accept → complete
  • Payment record retrieval
  • Role guards (customer cannot complete, customer cannot request upload URL)
  • Auth guards (unauthenticated blocked)
  • Idempotency (provider completes same booking twice → 200 both times)
  • Upload URL fields (uploadId, presignedUrl stub, expiresAt, filename, mimeType)

Architecture notes

  • Payments are captured inline in completeBooking() — no relay queue in Slice 1. The payment-captured.worker.ts is a stub that mirrors booking-accepted.worker.ts. Real async dispatch is Phase 3 Slice 2+.
  • Presigned URLs are stubs (https://storage.stub/uploads/...). Real S3/LocalStack integration is Phase 3 Slice 2.
  • amountCents: 0 for now — real pricing model is Phase 3 Slice 2+.

Phase 3 Slice 1 exit criteria

  • POST /api/v1/bookings/:id/complete transitions booking to completed and creates payment record
  • GET /api/v1/bookings/:id/payment returns payment record
  • PaymentCapturedDomainEvent emitted as structured breadcrumb
  • POST /api/v1/providers/me/verification/upload-url returns stub presigned URL
  • All role and auth guards enforced
  • Idempotency semantics consistent with Phase 2 pattern
  • 322 unit tests passing, typecheck clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Complete accepted bookings with automatic payment capture; booking status now includes "completed"
    • Retrieve payment details for completed bookings
    • Request presigned upload URLs for provider verification; role/authorization guards enforced
    • payment.captured domain-event relay and background worker added
  • Tests

    • Unit, integration, and end-to-end smoke tests for completion, payment capture, idempotency, upload URLs, and worker flows
  • Documentation

    • Phase 3 kickoff plan for payments/payouts, invoices/receipts, reviews, disputes, and implementation slices
  • Chores

    • Pinned dev dependency for React types; .gitignore updated

Kenny Güçlü and others added 2 commits April 1, 2026 14:40
…ad URL stub

Milestone 3a — POST /api/v1/bookings/:id/complete
- BookingStatus extended with 'completed'
- completeAcceptedBooking() in booking repository (in-memory + postgres)
- BookingsService.completeBooking() — role guard, status transition, payment capture
- BookingsService.getBookingPayment() — fetch payment by bookingId
- GET /api/v1/bookings/:id/payment endpoint
- PaymentCapturedDomainEvent emitted and logged as structured breadcrumb

Milestone 3a — Payments domain
- PaymentsModule: PaymentRepository interface + InMemoryPaymentRepository
- PaymentsService: capturePaymentForBooking() (idempotent), getPaymentByBookingId()
- BookingsModule imports PaymentsModule

Milestone 3b — POST /api/v1/providers/me/verification/upload-url
- UploadUrlRecord domain type added to @quickwerk/domain
- UploadUrlRepository interface + InMemoryUploadUrlRepository (stub presigned URL, 15min TTL)
- ProvidersService.requestUploadUrl() — provider-only role guard
- ProvidersController endpoint wired and tested

Background workers
- payment-captured.worker.ts stub (mirrors booking-accepted pattern, DLQ + retry)
- payment-captured.worker.test.ts (8 tests)

API client + app actions
- bookingApiRoutes.complete, bookingApiRoutes.payment
- providerApiRoutes.requestUploadUrl
- createCompleteBookingRequest, createGetBookingPaymentRequest, createRequestUploadUrlRequest
- booking-state: 'completed' added to status union
- booking-screen-actions: completeBookingRequest action
- provider-screen-actions: requestVerificationUploadUrl action

Tests (all passing: 322 total across all packages)
- bookings.service.complete.test.ts — 9 tests
- payments.service.test.ts — 4 tests
- providers.service.upload-url.test.ts — 4 tests
- booking-screen-actions-complete.test.ts — 5 tests

Smoke script
- scripts/smoke/phase3-slice1-complete-upload-smoke.sh — 13 assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce54a23e-bb33-4354-9b73-6f60efc718c3

📥 Commits

Reviewing files that changed from the base of the PR and between 98586df and 77f3983.

📒 Files selected for processing (1)
  • services/background-workers/src/main.ts

📝 Walkthrough

Walkthrough

Adds booking completion with server-side payment capture, a provider presigned upload‑URL flow, payments domain/repositories/services, payment-captured worker and DLQ logic, new controller endpoints and DI wiring, tests, and a Slice‑1 end‑to‑end smoke script.

Changes

Cohort / File(s) Summary
Project plan & config
.agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md, apps/admin-web/next-env.d.ts, apps/admin-web/package.json, .gitignore
Adds Phase‑3 plan doc; minor newline edits; pins @types/react@19.2.14; adds next-env.d.ts to .gitignore.
Product app — booking & provider UX
apps/product-app/src/features/booking/booking-screen-actions.ts, apps/product-app/src/features/booking/booking-state.ts, apps/product-app/src/features/booking/booking-screen-actions-complete.test.ts, apps/product-app/src/features/provider/provider-screen-actions.ts
Adds completeBookingRequest and tests; expands booking status to include completed; adds requestVerificationUploadUrl client call, types, and tests.
API client
packages/api-client/src/index.ts
Adds route helpers and request builders for booking complete, booking payment retrieval, and provider upload‑url (RequestUploadUrlBody).
Domain types
packages/domain/src/index.ts
Adds PaymentStatus, PaymentRecord, PaymentCapturedDomainEvent, and UploadUrlRecord exports.
Platform API — bookings & payments
services/platform-api/src/bookings/..., services/platform-api/src/payments/..., services/platform-api/src/bookings/bookings.controller.ts
Adds PaymentsModule, PaymentsService, payment repository contract and in‑memory impl; extends BookingsService to capture payment during completion and expose payment retrieval; adds controller endpoints and DI wiring.
Booking repositories
services/platform-api/src/bookings/domain/booking.repository.ts, .../in-memory-booking.repository.ts, .../postgres-booking.repository.ts
Adds completed status; introduces completeAcceptedBooking types and implementations with transactional locking, idempotency, and status‑history event writes.
Providers — upload URL
services/platform-api/src/providers/domain/upload-url.repository.ts, .../infrastructure/in-memory-upload-url.repository.ts, .../infrastructure/upload-url-repository.provider.ts, services/platform-api/src/providers/providers.service.ts, services/platform-api/src/providers/providers.controller.ts, services/platform-api/src/providers/providers.module.ts
Adds upload‑URL repository contract, in‑memory impl (15min TTL, stub presigned URL), DI provider, ProvidersService.requestUploadUrl, controller endpoint, module wiring, and tests.
Orchestration / event publishing
services/platform-api/src/orchestration/domain-event.publisher.ts, .../logging-domain-event.publisher.ts, .../relay-domain-event.publisher.ts
Extends domain event publisher interface and logging/relay publishers to emit/relay payment.captured domain events and integrate worker consumption.
Background workers
services/background-workers/src/workers/payment-captured.worker.ts, .../payment-captured.worker.test.ts, services/background-workers/src/workers/index.ts, services/background-workers/src/index.ts, services/background-workers/src/main.ts
Adds payment-captured worker with deterministic exponential retry, DLQ marking, envelope builders, exports and tests; registers consumer pipeline for payment.captured.
Smoke tests
scripts/smoke/phase3-slice1-complete-upload-smoke.sh
New end‑to‑end bash smoke script exercising booking completion, payment capture retrieval, idempotency, upload‑URL issuance, and authorization checks.
Misc tests & wiring
various .../*.test.ts
Multiple tests updated/added to wire new dependencies (payments, upload URL) and to exercise new flows (bookings completion, payments, providers upload).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Product App
    participant API as Platform API
    participant PaymentSvc as PaymentsService
    participant BookingRepo as Booking Repository
    participant EventPub as Event Publisher

    Client->>API: POST /api/v1/bookings/{id}/complete (Bearer)
    API->>API: resolve session, correlationId
    API->>PaymentSvc: capturePaymentForBooking(bookingId,...)
    PaymentSvc->>PaymentSvc: payments.createPayment(...)
    PaymentSvc-->>API: PaymentRecord {status: captured}
    API->>BookingRepo: completeAcceptedBooking(bookingId,...)
    BookingRepo-->>API: BookingRecord {status: completed}
    API->>EventPub: publishPaymentCaptured(payment.captured event)
    EventPub-->>API: ack
    API-->>Client: 200 { booking, payment }
Loading
sequenceDiagram
    participant Provider as Provider App
    participant API as Platform API
    participant UploadRepo as Upload URL Repository

    Provider->>API: POST /api/v1/providers/me/verification/upload-url (Bearer, filename, mimeType)
    API->>API: resolve session, validate provider role
    API->>UploadRepo: createUploadUrl(providerUserId, filename, mimeType, now)
    UploadRepo-->>API: UploadUrlRecord { uploadId, presignedUrl, expiresAt, filename, mimeType }
    API-->>Provider: 201 Created { uploadId, presignedUrl, expiresAt, filename, mimeType }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I hopped through fields and status streams,

Captured coins and uploaded dreams,
A presigned path, a tiny token,
Events emitted, not broken,
Phase Three hops home on carrot beams 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% 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 PR title clearly summarizes the main changes: booking completion feature, payment domain event, and upload URL stub for Phase 3 Slice 1 implementation.

✏️ 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/phase3-slice1-booking-complete-upload-url-v2

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

🧹 Nitpick comments (6)
services/platform-api/src/providers/providers.module.ts (1)

6-7: Remove duplicate provider registration for upload URL repository.

InMemoryUploadUrlRepository and uploadUrlRepositoryProvider are both registered in the providers array. Keep only the token-bound provider to avoid redundant DI bindings and potential multi-instance issues. The service injects by token (UPLOAD_URL_REPOSITORY), not by class.

♻️ Proposed cleanup
-import { InMemoryUploadUrlRepository } from './infrastructure/in-memory-upload-url.repository';
 import { uploadUrlRepositoryProvider } from './infrastructure/upload-url-repository.provider';
@@
   providers: [
     ProvidersService,
     providerVerificationRepositoryProvider,
     providerProfileRepositoryProvider,
-    InMemoryUploadUrlRepository,
     uploadUrlRepositoryProvider,
   ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/providers/providers.module.ts` around lines 6 - 7,
Remove the duplicate provider registration in the module providers array: keep
the token-bound provider export uploadUrlRepositoryProvider and remove the
direct class InMemoryUploadUrlRepository so the DI binding uses the
UPLOAD_URL_REPOSITORY token only; update the providers array to include
uploadUrlRepositoryProvider (and any other token providers) and delete the
InMemoryUploadUrlRepository entry to avoid multi-instance bindings when
injecting by UPLOAD_URL_REPOSITORY.
packages/domain/src/index.ts (1)

135-145: Field capturedAt is always required but status may not be captured.

PaymentRecord.capturedAt is a required field, but status can be 'pending' | 'authorized' | 'failed' | 'refunded' in addition to 'captured'. For non-captured payments, this field name is semantically misleading and may cause confusion.

Consider renaming to createdAt or making it optional (capturedAt?: string) when the payment isn't in captured status.

💡 Option A: Rename to createdAt
 export type PaymentRecord = {
   paymentId: string;
   bookingId: string;
   customerUserId: string;
   providerUserId: string;
   amountCents: number;
   currency: string;
   status: PaymentStatus;
-  capturedAt: string;
+  createdAt: string;
   correlationId: string;
 };
🤖 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 135 - 145, PaymentRecord currently
requires capturedAt even when status (PaymentStatus) may be
'pending'|'authorized'|'failed'|'refunded'; update the type so the timestamp
makes sense: either rename capturedAt to createdAt across the codebase (type
PaymentRecord and all usages) to represent the creation time, or make capturedAt
optional (capturedAt?: string) and ensure callers only set/read it when status
=== 'captured' (update validation/constructors and any code that assumes it
always exists). Reference the PaymentRecord type, capturedAt field, and
PaymentStatus usages when making the change.
services/platform-api/src/bookings/bookings.service.complete.test.ts (1)

96-113: Consider adding a test for cross-provider completion attempt.

The PR objectives mention "cross-provider attempts return 409", but there's no test verifying that a different provider attempting to complete a booking accepted by another provider receives a 409 conflict response. This is an important idempotency/authorization edge case.

💡 Suggested test case
it('returns 409 when different provider attempts to complete booking accepted by another provider', async () => {
  const { service } = createService();
  const customer = createSession('customer', 'customer-1');
  const providerA = createSession('provider', 'provider-a');
  const providerB = createSession('provider', 'provider-b');

  const created = await service.createBooking(customer, { requestedService: 'Plumbing' });
  expect(created.ok).toBe(true);
  if (!created.ok) return;

  await service.acceptBooking(providerA, created.booking.bookingId);

  const result = await service.completeBooking(providerB, created.booking.bookingId);
  expect(result.ok).toBe(false);
  if (!result.ok) expect(result.statusCode).toBe(409);
});
🤖 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.complete.test.ts` around
lines 96 - 113, Add a new test that verifies a different provider cannot
complete a booking accepted by another provider: use createService() and
createSession('provider-a') / createSession('provider-b'), call
service.createBooking(...) then service.acceptBooking(providerA, bookingId) and
then call service.completeBooking(providerB, bookingId) and assert the result is
a failure with statusCode 409 (i.e., result.ok is false and result.statusCode
=== 409) to ensure cross-provider completion returns Conflict; reference the
existing helpers createService, createSession and the service methods
createBooking, acceptBooking, completeBooking when adding the test.
scripts/smoke/phase3-slice1-complete-upload-smoke.sh (2)

76-80: JSON extraction with sed is fragile.

The extract function uses sed regex to parse JSON, which can fail with escaped quotes, nested objects, or multiline values. Consider using jq for more robust parsing, as it's already commonly available in CI environments.

💡 Suggested jq-based extract function
 extract() {
   local json="$1"
   local field="$2"
-  echo "$json" | sed -n "s/.*\"${field}\":\"\([^\"]*\)\".*/\1/p" | head -1
+  echo "$json" | jq -r ".$field // empty" 2>/dev/null || echo ""
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/smoke/phase3-slice1-complete-upload-smoke.sh` around lines 76 - 80,
The current extract function uses sed which breaks on escaped quotes, nested
JSON, and multiline values; update the extract function to parse JSON with jq
instead: accept the same parameters (json, field), check for jq availability,
and run the JSON through jq -r --arg field "$field" '.[$field] // empty' (or
equivalent) to reliably extract the value; if jq is not available in the
environment add a lightweight fallback (e.g., a python -c one-liner) that safely
decodes JSON, and ensure the function returns an empty string on missing keys to
preserve existing behavior.

160-164: Consider adding cross-provider completion smoke test.

The PR objectives mention "cross-provider attempts return 409", but this smoke test only validates that a customer cannot complete (403). Adding a test where a second provider attempts to complete a booking accepted by the first provider would increase coverage.

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

In `@scripts/smoke/phase3-slice1-complete-upload-smoke.sh` around lines 160 - 164,
Add a cross-provider completion smoke test after the customer 403 check: ensure
the booking has been accepted by the first provider (call req_status POST
"/api/v1/bookings/${BOOKING_ID}/accept" with PROVIDER1_TOKEN and assert
success), then simulate a different provider attempting to complete the same
booking (call req_status POST "/api/v1/bookings/${BOOKING_ID}/complete" with
PROVIDER2_TOKEN) and assert_status that this returns "409" to verify
cross-provider conflict handling; use the existing helper functions req_status
and assert_status and the existing BOOKING_ID variable and token variables
(PROVIDER1_TOKEN, PROVIDER2_TOKEN) to locate where to insert these steps.
services/platform-api/src/providers/providers.service.ts (1)

466-516: Consider validating filename and mimeType inputs.

The method accepts filename and mimeType without validation. Empty strings or invalid MIME types could be passed through to the repository. Other methods in this service (e.g., upsertProfile) validate required fields like displayName.

💡 Suggested input validation
   async requestUploadUrl(
     session: AuthSession,
     input: { filename: string; mimeType: string },
     context?: { correlationId?: string },
   ): Promise<
     | { ok: false; statusCode: 400 | 403; error: string }
     | { ok: true; statusCode: 201; uploadUrl: UploadUrlRecord }
   > {
     const correlationId = context?.correlationId ?? 'corr-missing';
+
+    const filename = input.filename?.trim();
+    const mimeType = input.mimeType?.trim();
+
+    if (!filename || !mimeType) {
+      logStructuredBreadcrumb({
+        event: 'provider.upload-url.request',
+        correlationId,
+        status: 'failed',
+        details: { reason: 'invalid-input', actorUserId: session.userId },
+      });
+      return { ok: false, statusCode: 400, error: 'filename and mimeType are required.' };
+    }

     if (session.role !== 'provider') {
🤖 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 466 -
516, The requestUploadUrl handler (requestUploadUrl) currently passes
input.filename and input.mimeType straight to uploadUrls.createUploadUrl; add
validation to reject empty or malformed values before calling createUploadUrl:
check filename is a non-empty string (and optionally length/character rules) and
mimeType is a non-empty valid MIME type (use a simple regex or a shared
validation helper if available), and if validation fails return a 400-style
response (ok: false, statusCode: 400, error: "...") and log the failure with
logStructuredBreadcrumb (event: 'provider.upload-url.request', status: 'failed',
details: { reason: 'invalid-input', ... }) so invalid inputs never reach
uploadUrls.createUploadUrl or the repository.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md:
- Line 180: The file references the wrong smoke script name: update the
reference `phase3-payment-upload-smoke.sh` to match the actual script added
`phase3-slice1-complete-upload-smoke.sh` so the doc aligns with the PR
objectives; edit the line that currently lists "Smoke script:
`phase3-payment-upload-smoke.sh`" and replace the filename with
`phase3-slice1-complete-upload-smoke.sh`.
- Line 4: Update the status line "**Status:** PLANNING — Phase 2 complete (PR
`#9`), Phase 3 not yet started" to reflect that Phase 3 Slice 1 has been delivered
(e.g., "**Status:** IN PROGRESS — Phase 3 Slice 1 delivered (PR #<number>)" or
similar), ensuring the exact status string in
.agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md is replaced so the
file accurately reflects the current implementation state.

In `@apps/admin-web/next-env.d.ts`:
- Line 3: Remove the manual import statement 'import
"./.next/dev/types/routes.d.ts";' from the auto-generated next-env.d.ts and do
not commit manual edits to that file; instead either (A) delete the import and
revert next-env.d.ts to the generated default and add next-env.d.ts to
.gitignore so Next manages it, or (B) if you actually want typed routes, enable
typedRoutes: true in next.config.ts and then regenerate the Next files so the
proper routes typings are produced—do not keep a manual import in next-env.d.ts.

In `@services/platform-api/src/bookings/bookings.service.ts`:
- Around line 455-482: A PaymentCapturedDomainEvent is built
(paymentCapturedEvent) and a breadcrumb is logged (logStructuredBreadcrumb with
event 'payment.captured.domain-event.emit') but the event is never published
because BookingDomainEventPublisher only exposes publishBookingAccepted() and
publishBookingDeclined(); add proper publishing to avoid the misleading log.
Update the BookingDomainEventPublisher interface to include
publishPaymentCaptured(paymentCapturedEvent: PaymentCapturedDomainEvent) (or add
a separate PaymentDomainEventPublisher with publishPaymentCaptured) and then
call that publisher where paymentCapturedEvent is constructed; also change the
breadcrumb to reflect success only after the publish promise resolves or log
failure on publish error.

In
`@services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts`:
- Around line 269-276: The call to insertStatusEvent is using toStatus:
'completed' which violates the booking_status_history enum constraint (migration
0006 allows only 'submitted','accepted','declined'), so either change the
inserted status to one of the allowed values (e.g., set toStatus: 'accepted' in
the code path that calls insertStatusEvent) or update the database migration to
add 'completed' to the enum; locate the call in postgress-booking.repository.ts
(insertStatusEvent invocation) and fix the status value or create a new
migration that alters the enum to include 'completed' to resolve the constraint
violation.

In `@services/platform-api/src/payments/domain/payment.repository.ts`:
- Around line 13-15: CreatePaymentResult currently includes an unused failure
branch { ok: false; reason: 'duplicate-booking' }; update the type by either
removing that union member if duplicate cases are handled by returning { ok:
true; payment, replayed: true } (remove the { ok: false; reason:
'duplicate-booking' } variant from CreatePaymentResult) or add a comment/docs on
CreatePaymentResult explaining when { ok: false; reason: 'duplicate-booking' }
would be returned in future implementations and why it differs from the replayed
true path so callers know which behavior to expect.

In `@services/platform-api/src/payments/payments.service.ts`:
- Around line 20-35: The breadcrumb currently logs status: 'succeeded'
unconditionally before checking the capture result; update the logic in
payments.service.ts around logStructuredBreadcrumb and the result handling so
the breadcrumb reflects the real outcome: either move the
logStructuredBreadcrumb call to after the if (!result.ok) check (so it runs only
on success), or compute status based on result.ok (e.g., status: result.ok ?
'succeeded' : 'failed') and include result.reason when failed; reference the
existing symbols logStructuredBreadcrumb, result, and input to locate and adjust
the code.

In `@services/platform-api/src/providers/providers.service.upload-url.test.ts`:
- Around line 86-99: The TTL assertion in the test using only `before` is too
tight and causes flakiness; after calling `service.requestUploadUrl` (the code
that reads `result.uploadUrl.expiresAt`) capture an `after = Date.now()` then
compute a lower bound using `before + 15min` and an upper bound using `after +
15min` (add a small slack like ±1s) and assert that `expiresMs` falls between
those bounds so the test tolerates execution latency.

---

Nitpick comments:
In `@packages/domain/src/index.ts`:
- Around line 135-145: PaymentRecord currently requires capturedAt even when
status (PaymentStatus) may be 'pending'|'authorized'|'failed'|'refunded'; update
the type so the timestamp makes sense: either rename capturedAt to createdAt
across the codebase (type PaymentRecord and all usages) to represent the
creation time, or make capturedAt optional (capturedAt?: string) and ensure
callers only set/read it when status === 'captured' (update
validation/constructors and any code that assumes it always exists). Reference
the PaymentRecord type, capturedAt field, and PaymentStatus usages when making
the change.

In `@scripts/smoke/phase3-slice1-complete-upload-smoke.sh`:
- Around line 76-80: The current extract function uses sed which breaks on
escaped quotes, nested JSON, and multiline values; update the extract function
to parse JSON with jq instead: accept the same parameters (json, field), check
for jq availability, and run the JSON through jq -r --arg field "$field"
'.[$field] // empty' (or equivalent) to reliably extract the value; if jq is not
available in the environment add a lightweight fallback (e.g., a python -c
one-liner) that safely decodes JSON, and ensure the function returns an empty
string on missing keys to preserve existing behavior.
- Around line 160-164: Add a cross-provider completion smoke test after the
customer 403 check: ensure the booking has been accepted by the first provider
(call req_status POST "/api/v1/bookings/${BOOKING_ID}/accept" with
PROVIDER1_TOKEN and assert success), then simulate a different provider
attempting to complete the same booking (call req_status POST
"/api/v1/bookings/${BOOKING_ID}/complete" with PROVIDER2_TOKEN) and
assert_status that this returns "409" to verify cross-provider conflict
handling; use the existing helper functions req_status and assert_status and the
existing BOOKING_ID variable and token variables (PROVIDER1_TOKEN,
PROVIDER2_TOKEN) to locate where to insert these steps.

In `@services/platform-api/src/bookings/bookings.service.complete.test.ts`:
- Around line 96-113: Add a new test that verifies a different provider cannot
complete a booking accepted by another provider: use createService() and
createSession('provider-a') / createSession('provider-b'), call
service.createBooking(...) then service.acceptBooking(providerA, bookingId) and
then call service.completeBooking(providerB, bookingId) and assert the result is
a failure with statusCode 409 (i.e., result.ok is false and result.statusCode
=== 409) to ensure cross-provider completion returns Conflict; reference the
existing helpers createService, createSession and the service methods
createBooking, acceptBooking, completeBooking when adding the test.

In `@services/platform-api/src/providers/providers.module.ts`:
- Around line 6-7: Remove the duplicate provider registration in the module
providers array: keep the token-bound provider export
uploadUrlRepositoryProvider and remove the direct class
InMemoryUploadUrlRepository so the DI binding uses the UPLOAD_URL_REPOSITORY
token only; update the providers array to include uploadUrlRepositoryProvider
(and any other token providers) and delete the InMemoryUploadUrlRepository entry
to avoid multi-instance bindings when injecting by UPLOAD_URL_REPOSITORY.

In `@services/platform-api/src/providers/providers.service.ts`:
- Around line 466-516: The requestUploadUrl handler (requestUploadUrl) currently
passes input.filename and input.mimeType straight to uploadUrls.createUploadUrl;
add validation to reject empty or malformed values before calling
createUploadUrl: check filename is a non-empty string (and optionally
length/character rules) and mimeType is a non-empty valid MIME type (use a
simple regex or a shared validation helper if available), and if validation
fails return a 400-style response (ok: false, statusCode: 400, error: "...") and
log the failure with logStructuredBreadcrumb (event:
'provider.upload-url.request', status: 'failed', details: { reason:
'invalid-input', ... }) so invalid inputs never reach uploadUrls.createUploadUrl
or the repository.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c9feba1-07dc-4bc9-9359-a7062cc274ab

📥 Commits

Reviewing files that changed from the base of the PR and between f9a129b and dd81915.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (37)
  • .agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md
  • apps/admin-web/next-env.d.ts
  • apps/admin-web/package.json
  • apps/product-app/src/features/booking/booking-screen-actions-complete.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-screen-actions.ts
  • packages/api-client/src/index.ts
  • packages/domain/src/index.ts
  • scripts/smoke/phase3-slice1-complete-upload-smoke.sh
  • services/background-workers/src/workers/payment-captured.worker.test.ts
  • services/background-workers/src/workers/payment-captured.worker.ts
  • services/platform-api/src/bookings/bookings.controller.ts
  • services/platform-api/src/bookings/bookings.module.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/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/payments/domain/payment.repository.ts
  • services/platform-api/src/payments/infrastructure/in-memory-payment.repository.ts
  • services/platform-api/src/payments/infrastructure/payment-repository.provider.ts
  • services/platform-api/src/payments/payments.module.ts
  • services/platform-api/src/payments/payments.service.test.ts
  • services/platform-api/src/payments/payments.service.ts
  • services/platform-api/src/providers/domain/upload-url.repository.ts
  • services/platform-api/src/providers/infrastructure/in-memory-upload-url.repository.ts
  • services/platform-api/src/providers/infrastructure/upload-url-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.discovery.test.ts
  • services/platform-api/src/providers/providers.service.profile.test.ts
  • services/platform-api/src/providers/providers.service.provider-detail.test.ts
  • services/platform-api/src/providers/providers.service.test.ts
  • services/platform-api/src/providers/providers.service.ts
  • services/platform-api/src/providers/providers.service.upload-url.test.ts

Comment thread .agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md Outdated
Comment thread .agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md Outdated
Comment thread apps/admin-web/next-env.d.ts Outdated
Comment on lines +455 to +482
const paymentCapturedEvent: PaymentCapturedDomainEvent = {
eventName: 'payment.captured',
eventId: randomUUID(),
occurredAt: completedAt,
correlationId,
replayed: completed.replayed,
payment: {
paymentId: payment.paymentId,
bookingId: payment.bookingId,
customerUserId: payment.customerUserId,
providerUserId: payment.providerUserId,
amountCents: payment.amountCents,
currency: payment.currency,
status: 'captured',
},
};

logStructuredBreadcrumb({
event: 'payment.captured.domain-event.emit',
correlationId,
status: 'succeeded',
details: {
eventId: paymentCapturedEvent.eventId,
paymentId: payment.paymentId,
bookingId,
replayed: completed.replayed,
},
});

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
# Check the domain event publisher interface and implementations
rg -nA 10 'BookingDomainEventPublisher' --type ts

Repository: MarkoAtc/QuickWerk

Length of output: 1778


🏁 Script executed:

#!/bin/bash
# First, let's locate the bookings.service.ts file and examine the code in question
find . -name "bookings.service.ts" -type f

Repository: MarkoAtc/QuickWerk

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Search for BookingDomainEventPublisher to understand its interface
rg -n "BookingDomainEventPublisher" --type ts -A 5

Repository: MarkoAtc/QuickWerk

Length of output: 11203


🏁 Script executed:

#!/bin/bash
# Search for publishPaymentCaptured or similar payment-related publish methods
rg -n "publishPayment" --type ts

Repository: MarkoAtc/QuickWerk

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Search for publish methods in domain event publisher to understand available methods
rg -n "publish.*\(" --type ts | grep -i "domain\|event" | head -20

Repository: MarkoAtc/QuickWerk

Length of output: 2015


🏁 Script executed:

#!/bin/bash
# Read the actual code at lines 455-482 to see context
sed -n '450,490p' services/platform-api/src/bookings/bookings.service.ts

Repository: MarkoAtc/QuickWerk

Length of output: 1199


🏁 Script executed:

#!/bin/bash
# Search for PaymentCapturedDomainEvent definition
rg -n "PaymentCapturedDomainEvent" --type ts -B 2 -A 5

Repository: MarkoAtc/QuickWerk

Length of output: 5975


🏁 Script executed:

#!/bin/bash
# Check if there are any calls to publish payment events
rg -n "payment.*captured" --type ts -i | head -20

Repository: MarkoAtc/QuickWerk

Length of output: 2188


🏁 Script executed:

#!/bin/bash
# Look for the domain-event.publisher.ts file to see full interface
cat services/platform-api/src/orchestration/domain-event.publisher.ts

Repository: MarkoAtc/QuickWerk

Length of output: 449


🏁 Script executed:

#!/bin/bash
# Check if there's a separate payment event publisher or another way to publish payment events
rg -n "PaymentDomainEventPublisher|payment.*publisher" --type ts -i

Repository: MarkoAtc/QuickWerk

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check what other services inject for event publishing - look for similar patterns
rg -n "publishBookingAccepted\|publishBookingDeclined" --type ts -B 3 -A 3 | head -40

Repository: MarkoAtc/QuickWerk

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Find all usages of publishBookingAccepted and publishBookingDeclined
rg -n "publishBookingAccepted\|publishBookingDeclined" --type ts

Repository: MarkoAtc/QuickWerk

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check the complete context around line 240 and 355 to see the pattern
sed -n '235,245p' services/platform-api/src/bookings/bookings.service.ts
sed -n '350,360p' services/platform-api/src/bookings/bookings.service.ts

Repository: MarkoAtc/QuickWerk

Length of output: 634


PaymentCapturedDomainEvent is constructed but never published, and the breadcrumb is misleading.

The paymentCapturedEvent object is created (lines 455-470) and a breadcrumb logs 'payment.captured.domain-event.emit' with status: 'succeeded' (line 473), but there is no actual call to publish the event. This means the payment-captured worker will never receive the event to process, and the log falsely claims an emit succeeded when none occurred.

The issue is that BookingDomainEventPublisher interface only supports publishBookingAccepted() and publishBookingDeclined() — there is no publishPaymentCaptured() method. The interface must be extended to support payment events, or an alternative event publishing mechanism needs to be used for payment domain events (separate from booking events).

🤖 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 455 -
482, A PaymentCapturedDomainEvent is built (paymentCapturedEvent) and a
breadcrumb is logged (logStructuredBreadcrumb with event
'payment.captured.domain-event.emit') but the event is never published because
BookingDomainEventPublisher only exposes publishBookingAccepted() and
publishBookingDeclined(); add proper publishing to avoid the misleading log.
Update the BookingDomainEventPublisher interface to include
publishPaymentCaptured(paymentCapturedEvent: PaymentCapturedDomainEvent) (or add
a separate PaymentDomainEventPublisher with publishPaymentCaptured) and then
call that publisher where paymentCapturedEvent is constructed; also change the
breadcrumb to reflect success only after the publish promise resolves or log
failure on publish error.

Comment on lines +269 to +276
await this.insertStatusEvent(client, {
bookingId: input.bookingId,
changedAt: input.completedAt,
fromStatus: 'accepted',
toStatus: 'completed',
actorRole: input.actorRole,
actorUserId: input.actorUserId,
});

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 CHECK constraints on booking_status_history table
rg -n "CHECK.*to_status|CHECK.*from_status" --type sql

Repository: MarkoAtc/QuickWerk

Length of output: 631


🏁 Script executed:

# First, find the actual code at lines 269-276 in the repository
cat -n services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts | sed -n '269,276p'

Repository: MarkoAtc/QuickWerk

Length of output: 385


🏁 Script executed:

# Search for all migrations in order and see if any add 'completed' status
find services/platform-api/migrations -name "*.sql" -type f | sort | head -20

Repository: MarkoAtc/QuickWerk

Length of output: 480


🏁 Script executed:

# Search for 'completed' in migration files to see if it's ever added to constraints
rg -n "completed" services/platform-api/migrations/ -A 2 -B 2

Repository: MarkoAtc/QuickWerk

Length of output: 44


🏁 Script executed:

# Search for declineSubmittedBooking method to verify the 'declined' claim
rg -n "declineSubmittedBooking" services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts -A 10

Repository: MarkoAtc/QuickWerk

Length of output: 648


🏁 Script executed:

# Check the full content of migration 0006 to understand the current constraints
cat services/platform-api/migrations/0006_booking_declined_support.sql

Repository: MarkoAtc/QuickWerk

Length of output: 1684


🏁 Script executed:

# Check if there are any migrations after 0006
ls -la services/platform-api/migrations/ | tail -20

Repository: MarkoAtc/QuickWerk

Length of output: 747


🏁 Script executed:

# Get full declineSubmittedBooking method to see what status it writes
rg -n "declineSubmittedBooking" services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts -A 30

Repository: MarkoAtc/QuickWerk

Length of output: 1369


🏁 Script executed:

# Search for all status writes in the repository file to understand what statuses are being used
rg -n "toStatus:" services/platform-api/src/bookings/infrastructure/postgres-booking.repository.ts

Repository: MarkoAtc/QuickWerk

Length of output: 233


The toStatus: 'completed' value violates database constraints and will fail at runtime.

The insertStatusEvent call writes toStatus: 'completed', but the booking_status_history table constraints only allow 'submitted', 'accepted', and 'declined' (per migration 0006). This INSERT will fail with a constraint violation at runtime.

The claim about 'declined' status in declineSubmittedBooking is not applicable—migration 0006 has already added 'declined' to the allowed constraint values.

🤖 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 269 - 276, The call to insertStatusEvent is using toStatus:
'completed' which violates the booking_status_history enum constraint (migration
0006 allows only 'submitted','accepted','declined'), so either change the
inserted status to one of the allowed values (e.g., set toStatus: 'accepted' in
the code path that calls insertStatusEvent) or update the database migration to
add 'completed' to the enum; locate the call in postgress-booking.repository.ts
(insertStatusEvent invocation) and fix the status value or create a new
migration that alters the enum to include 'completed' to resolve the constraint
violation.

Comment thread services/platform-api/src/payments/domain/payment.repository.ts
Comment thread services/platform-api/src/payments/payments.service.ts Outdated
@coderabbitai

coderabbitai Bot commented Apr 1, 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 12 file(s) based on 8 unresolved review comments.

Files modified:

  • .agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md
  • .gitignore
  • apps/admin-web/next-env.d.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-domain-event.publisher.ts
  • services/platform-api/src/payments/domain/payment.repository.ts
  • services/platform-api/src/payments/payments.service.ts
  • services/platform-api/src/providers/providers.service.upload-url.test.ts

Commit: 70de91c92f372b75bd3e585face23fbf088b8fe7

The changes have been pushed to the feature/phase3-slice1-booking-complete-upload-url-v2 branch.

Time taken: 6m 16s

Fixed 12 file(s) based on 8 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@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: 4

🧹 Nitpick comments (1)
services/platform-api/src/bookings/bookings.service.ts (1)

474-484: Drop the second payment.captured.domain-event.emit breadcrumb.

publishPaymentCaptured() already goes through LoggingBookingDomainEventPublisher.publishPaymentCaptured(), which emits this same event name. Keeping another emit log here will double-count publishes and make the trace noisy.

Suggested cleanup
-    logStructuredBreadcrumb({
-      event: 'payment.captured.domain-event.emit',
-      correlationId,
-      status: 'succeeded',
-      details: {
-        eventId: paymentCapturedEvent.eventId,
-        paymentId: payment.paymentId,
-        bookingId,
-        replayed: completed.replayed,
-      },
-    });
🤖 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 474 -
484, Remove the duplicate breadcrumb emit by deleting the
logStructuredBreadcrumb call that logs event
'payment.captured.domain-event.emit' in the current function; rely on
LoggingBookingDomainEventPublisher.publishPaymentCaptured (and its wrapper
publishPaymentCaptured()) to emit that breadcrumb so you don’t double-count
publishes or create noisy traces.
🤖 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 405-453: The code currently calls bookings.completeAcceptedBooking
(committing the booking to completed) before calling
paymentsService.capturePaymentForBooking, which can throw and leave a terminal
booking without a payment; change the flow so payment capture occurs before
finalizing the booking or add a compensating rollback if capture fails.
Specifically, either call paymentsService.capturePaymentForBooking using
completedAt/bookingId/customerUserId/providerUserId first and only call
bookings.completeAcceptedBooking after a successful capture, or, if you must
finalize first, implement a rollback via the bookings API (e.g., a new
revert/transition method or bookings.completeAcceptedBooking with an undo path)
that resets status back to the prior state on capture failure and surface a 500
with consistent state handling; ensure errors from capture are caught and logged
with correlationId and actorUserId (session.userId).
- Around line 523-525: The current guard only verifies customer ownership
(checks session.role === 'customer' against booking.customerUserId) and lets any
provider read payments; update the access check in the bookings service method
that returns payments so both participants are validated: if session.role ===
'customer' require booking.customerUserId === session.userId, and if
session.role === 'provider' require booking.providerUserId === session.userId;
otherwise return the same 403 error object. Ensure you reference the existing
variables booking, session, booking.customerUserId and booking.providerUserId
and preserve existing behavior for other roles (e.g., admin) if applicable.
- Around line 445-460: The payment.captured event is using completed.replayed
instead of the replay flag returned by capturePaymentForBooking, which mislabels
first successful payment events; update the PaymentCapturedDomainEvent
construction in bookings.service.ts to set replayed to the replay flag returned
by capturePaymentForBooking (use the payment.replayed/value from the const {
payment } = await this.paymentsService.capturePaymentForBooking(...) call) so
the event reflects the payment operation's actual replay status.

In `@services/platform-api/src/orchestration/relay-domain-event.publisher.ts`:
- Around line 195-200: The publishPaymentCaptured method currently only calls
loggingPublisher.publishPaymentCaptured and bypasses the relay/retry/DLQ path,
preventing services/background-workers/src/workers/payment-captured.worker.ts
from ever running; update publishPaymentCaptured (in
relay-domain-event.publisher) to use the same relay/publish path used by the
accepted/declined publishers (e.g., invoke the relay/emit method or the shared
publishViaRelay helper used by publishPaymentAccepted/publishPaymentDeclined) so
the event is enqueued for the background worker and goes through retry/DLQ
handling instead of only being logged.

---

Nitpick comments:
In `@services/platform-api/src/bookings/bookings.service.ts`:
- Around line 474-484: Remove the duplicate breadcrumb emit by deleting the
logStructuredBreadcrumb call that logs event
'payment.captured.domain-event.emit' in the current function; rely on
LoggingBookingDomainEventPublisher.publishPaymentCaptured (and its wrapper
publishPaymentCaptured()) to emit that breadcrumb so you don’t double-count
publishes or create noisy traces.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd7dca96-7479-4dd2-8231-db0eaf42366b

📥 Commits

Reviewing files that changed from the base of the PR and between dd81915 and 70de91c.

📒 Files selected for processing (12)
  • .agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md
  • .gitignore
  • apps/admin-web/next-env.d.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-domain-event.publisher.ts
  • services/platform-api/src/payments/domain/payment.repository.ts
  • services/platform-api/src/payments/payments.service.ts
  • services/platform-api/src/providers/providers.service.upload-url.test.ts
✅ Files skipped from review due to trivial changes (4)
  • .gitignore
  • apps/admin-web/next-env.d.ts
  • services/platform-api/src/payments/domain/payment.repository.ts
  • .agent/plans/phase3-payments-payouts-kickoff-2026-03-31.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/platform-api/src/bookings/bookings.service.test.ts
  • services/platform-api/src/providers/providers.service.upload-url.test.ts
  • services/platform-api/src/bookings/bookings.service.complete.test.ts

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

coderabbitai Bot commented Apr 1, 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 3 file(s) based on 4 unresolved review comments.

Files modified:

  • services/background-workers/src/workers/index.ts
  • services/platform-api/src/bookings/bookings.service.ts
  • services/platform-api/src/orchestration/relay-domain-event.publisher.ts

Commit: d5a3dcf586c11dd5a4bc3b5bae8b1d7db68dabd2

The changes have been pushed to the feature/phase3-slice1-booking-complete-upload-url-v2 branch.

Time taken: 5m 42s

Fixed 3 file(s) based on 4 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@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: 3

♻️ Duplicate comments (1)
services/platform-api/src/bookings/bookings.service.ts (1)

426-438: ⚠️ Potential issue | 🟠 Major

Use the capture result’s replay flag, not payment.replayed.

Line 437 reads captureResult.payment.replayed, but replay is returned at the operation level (captureResult.replayed). This can mislabel first-vs-replay emits.

Suggested fix
-      paymentReplayed = captureResult.payment.replayed ?? false;
+      paymentReplayed = captureResult.replayed;
#!/bin/bash
# Verify replay flag location and current usage
rg -n -C3 "capturePaymentForBooking\\(|replayed" services/platform-api/src/payments/payments.service.ts services/platform-api/src/bookings/bookings.service.ts --type ts
rg -n -C3 "type PaymentRecord|interface PaymentRecord" --type ts
🤖 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 426 -
438, captureResult.replayed (operation-level) is the correct replay flag —
update the booking completion code that calls
paymentsService.capturePaymentForBooking so it uses captureResult.replayed
instead of captureResult.payment.replayed; specifically, after assigning payment
= captureResult.payment, set paymentReplayed = captureResult.replayed ?? false
(or equivalent) and remove reliance on captureResult.payment.replayed; check
symbols capturePaymentForBooking, captureResult, payment, and paymentReplayed to
locate the change.
🤖 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 19-26: The workerPipelines constant is missing the
payment-captured pipeline; update the exported workerPipelines array (the const
named workerPipelines) to include the string 'payment-captured-orchestration'
alongside the existing 'booking-accepted-orchestration' and
'booking-declined-orchestration' entries so the payment-captured worker is
registered and monitored; preserve the as const typing and ordering consistent
with related booking pipelines.

In `@services/platform-api/src/bookings/bookings.service.ts`:
- Around line 407-434: The code captures payment via
paymentsService.capturePaymentForBooking before verifying the booking can
transition to completed; move the transition eligibility check so you validate
bookingToComplete's current state (e.g., status/accepted flag or a helper like
canTransitionToCompleted) and return the 409 error/logging path if not eligible
before calling capturePaymentForBooking; apply the same ordering for the
replay/alternate branch around the capture logic (the blocks referencing
captureResult/paymentReplayed and the subsequent 467-495 flow) so no payment is
created for invalid transitions.
- Around line 379-381: The return type for completeBooking is missing the 500
error status; update the Promise return union in the completeBooking signature
so the error branch includes 500 (i.e., change statusCode: 403 | 404 | 409 to
403 | 404 | 409 | 500) to match the actual return where a payment capture
failure returns { ok: false, statusCode: 500, error: string }; ensure the type
change references the existing variant that currently uses
ReturnType<BookingsService['serializeRecord']> and PaymentRecord so the
successful branch remains unchanged.

---

Duplicate comments:
In `@services/platform-api/src/bookings/bookings.service.ts`:
- Around line 426-438: captureResult.replayed (operation-level) is the correct
replay flag — update the booking completion code that calls
paymentsService.capturePaymentForBooking so it uses captureResult.replayed
instead of captureResult.payment.replayed; specifically, after assigning payment
= captureResult.payment, set paymentReplayed = captureResult.replayed ?? false
(or equivalent) and remove reliance on captureResult.payment.replayed; check
symbols capturePaymentForBooking, captureResult, payment, and paymentReplayed to
locate the change.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8cf55ff-1df5-448c-8037-6bd07fbaccf6

📥 Commits

Reviewing files that changed from the base of the PR and between 70de91c and d5a3dcf.

📒 Files selected for processing (3)
  • services/background-workers/src/workers/index.ts
  • services/platform-api/src/bookings/bookings.service.ts
  • services/platform-api/src/orchestration/relay-domain-event.publisher.ts

Comment thread services/background-workers/src/workers/index.ts
Comment thread services/platform-api/src/bookings/bookings.service.ts
Comment thread services/platform-api/src/bookings/bookings.service.ts
@coderabbitai

coderabbitai Bot commented Apr 1, 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 3 file(s) based on 3 unresolved review comments.

Files modified:

  • services/background-workers/src/index.ts
  • services/background-workers/src/workers/index.ts
  • services/platform-api/src/bookings/bookings.service.ts

Commit: 98586dfe7585dca10e1a3f6cd17acf1ee34e1c5e

The changes have been pushed to the feature/phase3-slice1-booking-complete-upload-url-v2 branch.

Time taken: 4m 50s

Fixed 3 file(s) based on 3 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@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

♻️ Duplicate comments (2)
services/platform-api/src/bookings/bookings.service.ts (2)

447-458: ⚠️ Potential issue | 🟠 Major

Use captureResult.replayed as the source of truth.

capturePaymentForBooking() returns the idempotency flag next to payment. Reading captureResult.payment.replayed here will mislabel the payment.captured event/logs whenever PaymentRecord does not carry that field.

Suggested fix
       payment = captureResult.payment;
-      paymentReplayed = captureResult.payment.replayed ?? false;
+      paymentReplayed = captureResult.replayed;
🤖 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 447 -
458, The code currently sets paymentReplayed from captureResult.payment.replayed
which can be missing on PaymentRecord; instead use the idempotency flag returned
on the outer result. After assigning payment = captureResult.payment, set
paymentReplayed = captureResult.replayed ?? false (i.e., read the replay flag
from captureResult rather than captureResult.payment) so functions like
capturePaymentForBooking and variables payment and paymentReplayed use the
correct source of truth.

421-455: ⚠️ Potential issue | 🔴 Critical

Preserve repository replay/conflict semantics before capturing payment.

This preflight only accepts accepted, so same-provider retries on an already completed booking now return 409 instead of the expected replayed success. It also still lets a different provider reach capturePaymentForBooking() for another provider’s accepted booking before completeAcceptedBooking() rejects the transition.

Suggested fix
-    // Validate booking can transition to completed before capturing payment
-    if (bookingToComplete.status !== 'accepted') {
+    // Match repository transition semantics before capturing payment
+    const isSameProvider = bookingToComplete.providerUserId === session.userId;
+    const isReplay = bookingToComplete.status === 'completed' && isSameProvider;
+
+    if (bookingToComplete.providerUserId && !isSameProvider) {
+      logStructuredBreadcrumb({
+        event: 'booking.complete.write',
+        correlationId,
+        status: 'failed',
+        details: {
+          reason: 'transition-conflict',
+          bookingId,
+          actorUserId: session.userId,
+          currentStatus: bookingToComplete.status,
+        },
+      });
+
+      return {
+        ok: false,
+        statusCode: 409,
+        error: `Booking cannot transition from ${bookingToComplete.status} to completed.`,
+      };
+    }
+
+    if (bookingToComplete.status !== 'accepted' && !isReplay) {
       logStructuredBreadcrumb({
         event: 'booking.complete.write',
         correlationId,
         status: 'failed',
🤖 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 421 -
455, The current preflight rejects already-completed bookings causing retries to
return 409 instead of a replay success and allows a different provider to
capture before the transition check; to fix, call
this.paymentsService.capturePaymentForBooking(...) before enforcing the
bookingToComplete.status check so capture handles replay semantics first
(inspect captureResult.replayed and return success if true), then validate
transition (bookingToComplete.status === 'accepted') and provider identity
(compare bookingToComplete.providerUserId with session.userId and the
providerUserId passed) before calling completeAcceptedBooking(...); use the
existing symbols bookingToComplete.status, captureResult.replayed,
this.paymentsService.capturePaymentForBooking, bookingId,
bookingToComplete.providerUserId, session.userId, and completeAcceptedBooking to
locate and reorder/add these checks.
🤖 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/index.ts`:
- Around line 8-10: The payment-captured consumer exported as
consumePaymentCapturedAttempt (along with buildPaymentCapturedWorkerEnvelope and
markPaymentCapturedDlq) is not being registered, so wire it into the consumer
registration routine where booking.accepted/booking.declined are registered: add
a registration call that registers the "payment.captured" subject using
buildPaymentCapturedWorkerEnvelope for the envelope creation,
consumePaymentCapturedAttempt as the handler, and markPaymentCapturedDlq as the
DLQ handler (mirror the existing booking.* registration pattern so subscription
options and error handling match).

---

Duplicate comments:
In `@services/platform-api/src/bookings/bookings.service.ts`:
- Around line 447-458: The code currently sets paymentReplayed from
captureResult.payment.replayed which can be missing on PaymentRecord; instead
use the idempotency flag returned on the outer result. After assigning payment =
captureResult.payment, set paymentReplayed = captureResult.replayed ?? false
(i.e., read the replay flag from captureResult rather than
captureResult.payment) so functions like capturePaymentForBooking and variables
payment and paymentReplayed use the correct source of truth.
- Around line 421-455: The current preflight rejects already-completed bookings
causing retries to return 409 instead of a replay success and allows a different
provider to capture before the transition check; to fix, call
this.paymentsService.capturePaymentForBooking(...) before enforcing the
bookingToComplete.status check so capture handles replay semantics first
(inspect captureResult.replayed and return success if true), then validate
transition (bookingToComplete.status === 'accepted') and provider identity
(compare bookingToComplete.providerUserId with session.userId and the
providerUserId passed) before calling completeAcceptedBooking(...); use the
existing symbols bookingToComplete.status, captureResult.replayed,
this.paymentsService.capturePaymentForBooking, bookingId,
bookingToComplete.providerUserId, session.userId, and completeAcceptedBooking to
locate and reorder/add these checks.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 550b2ca8-53f6-4536-bab2-d3f61228fd73

📥 Commits

Reviewing files that changed from the base of the PR and between d5a3dcf and 98586df.

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

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

coderabbitai Bot commented Apr 1, 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/background-workers/src/main.ts

Commit: 77f39836199c27d436b05ae35d4dfff1ce5a92a9

The changes have been pushed to the feature/phase3-slice1-booking-complete-upload-url-v2 branch.

Time taken: 5m 9s

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

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
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