Skip to content

feat: Add platform fee, refund/dispute events, and CancelPayment RPC to financial-gateway#1761

Merged
bjcoombs merged 4 commits intodevelopfrom
test-coverage-80--2-3-4--gateway-features
Mar 18, 2026
Merged

feat: Add platform fee, refund/dispute events, and CancelPayment RPC to financial-gateway#1761
bjcoombs merged 4 commits intodevelopfrom
test-coverage-80--2-3-4--gateway-features

Conversation

@bjcoombs
Copy link
Copy Markdown
Collaborator

Summary

  • Task 2: Add platform_fee_minor_units field to DispatchPaymentResponse proto. Wire through from Stripe adapter result to gRPC response and Starlark client (previously hardcoded to int64(0)).
  • Task 3: Add PaymentRefundedEvent and PaymentDisputedEvent proto messages. Map REFUNDED and DISPUTED webhook statuses in webhook_handler.go to domain events published to existing Kafka topics (financial-gateway.payment-refunded.v1, financial-gateway.payment-disputed.v1).
  • Task 4: Implement CancelPayment RPC with testable PaymentIntentCanceller and PaymentIntentResolver interfaces. Handles idempotency (already-cancelled payments succeed). Replaces the Unimplemented stub.

Changes Made

  • api/proto/.../financial_gateway.proto: Added platform_fee_minor_units field (tag 7) to DispatchPaymentResponse
  • api/proto/.../events.proto: Added PaymentRefundedEvent and PaymentDisputedEvent messages
  • services/financial-gateway/adapters/stripe/payment_intent_adapter.go: Added PaymentIntentCanceller, PaymentIntentResolver interfaces; CancelResult struct; CancelPayment method
  • services/financial-gateway/adapters/http/webhook_handler.go: Map REFUNDED/DISPUTED statuses to new domain events; updated topicToEventType
  • services/financial-gateway/service/grpc_service.go: Implemented CancelPayment RPC with tenant resolution and error mapping
  • services/financial-gateway/client/starlark.go: Read platform_fee_minor_units from response instead of hardcoding 0

Test plan

  • All existing financial-gateway tests pass
  • New adapter tests: CancelPayment success, not-configured, missing account, resolver not found, already-cancelled idempotency, empty reason
  • New webhook handler tests: PaymentRefunded publishes to outbox, PaymentDisputed publishes to outbox, refund without payment_order_id acknowledged without publish
  • New gRPC service test: CancelPayment returns Unavailable when Stripe not configured
  • Updated e2e test: CancelPayment returns FailedPrecondition without tenant context (was Unimplemented)
  • Full go build ./... passes

…to financial-gateway

Task 2: Add platform_fee_minor_units field to DispatchPaymentResponse proto.
Wire it through from Stripe adapter to gRPC response and Starlark client
(previously hardcoded to 0).

Task 3: Add PaymentRefundedEvent and PaymentDisputedEvent proto messages.
Map REFUNDED and DISPUTED webhook statuses to domain events published to
their respective Kafka topics. Topics already existed in topics.go.

Task 4: Implement CancelPayment RPC with testable PaymentIntentCanceller
and PaymentIntentResolver interfaces. Handles idempotency for
already-cancelled payments. Replaces the Unimplemented stub.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

Adds platform_fee_minor_units to dispatch responses, introduces PaymentRefundedEvent and PaymentDisputedEvent protos, maps Stripe REFUNDED/DISPUTED webhooks to domain events, implements PaymentIntent cancellation (interfaces, CancelPayment, error mapping) in the Stripe adapter and wires cancellation through the gRPC service with tests.

Changes

Cohort / File(s) Summary
Protocol Buffer Definitions
api/proto/meridian/financial_gateway/v1/financial_gateway.proto, api/proto/meridian/financial_gateway_events/v1/events.proto
Added platform_fee_minor_units to DispatchPaymentResponse; added PaymentRefundedEvent and PaymentDisputedEvent messages with validation and timestamp fields.
Webhook Event Handling
services/financial-gateway/adapters/http/webhook_handler.go, services/financial-gateway/adapters/http/webhook_handler_test.go
Mapped Stripe "REFUNDED" and "DISPUTED" statuses to domain events and topics; added tests for publishing and acknowledgement-only behavior when payment_order_id missing; unmapped statuses now return errNoMapping.
Stripe PaymentIntent Adapter
services/financial-gateway/adapters/stripe/payment_intent_adapter.go, services/financial-gateway/adapters/stripe/payment_intent_adapter_test.go
Added PaymentIntentCanceller and PaymentIntentResolver interfaces; extended adapter config with Canceller/Resolver; added CancelPayment method, CancelResult, sentinel errors and idempotent cancellation handling; comprehensive tests and mocks added.
Service Layer / gRPC
services/financial-gateway/service/grpc_service.go, services/financial-gateway/service/grpc_service_test.go
Implemented CancelPayment flow (tenant/client validation, account injection, adapter call), added mapCancelError, updated CancelPayment signature, and included PlatformFeeMinorUnits in DispatchPaymentResponse; added test for missing Stripe config returning Unavailable.
Client mapping
services/financial-gateway/client/starlark.go
Dispatch response mapping now reads resp.GetPlatformFeeMinorUnits() instead of constant zero.
E2E Test
services/financial-gateway/e2e/e2e_test.go
Renamed test to TestCancelPayment_MissingTenant and changed expected gRPC status from Unimplemented to FailedPrecondition.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Service as FinancialGatewayService
    participant ClientFactory
    participant Adapter as StripeAdapter
    participant Resolver as PaymentIntentResolver
    participant Canceller as PaymentIntentCanceller
    participant Stripe

    Client->>Service: CancelPayment(paymentOrderId, reason)
    Service->>ClientFactory: create tenant-scoped Stripe client
    ClientFactory-->>Service: Stripe client (AccountID)
    Service->>Service: inject Stripe account into ctx
    Service->>Adapter: CancelPayment(ctx, paymentOrderId, reason)
    Adapter->>Resolver: FindByPaymentOrderID(paymentOrderId)
    Resolver-->>Adapter: paymentIntentId
    Adapter->>Canceller: Cancel(ctx, paymentIntentId, params)
    Canceller->>Stripe: Cancel PaymentIntent
    Stripe-->>Canceller: PaymentIntent (status)
    Canceller-->>Adapter: PaymentIntent
    Adapter->>Adapter: map status -> DispatchStatus
    Adapter-->>Service: CancelResult
    Service->>Service: mapCancelError or success
    Service-->>Client: CancelPaymentResponse(status, reason)
Loading
sequenceDiagram
    participant StripeWebhook
    participant Handler as WebhookHandler
    participant TopicMapper
    participant Outbox
    participant DomainEvent

    StripeWebhook->>Handler: webhook payload (REFUNDED / DISPUTED)
    Handler->>Handler: parse & mapToDomainEvent(status)
    alt REFUNDED
        Handler->>DomainEvent: create PaymentRefundedEvent
    else DISPUTED
        Handler->>DomainEvent: create PaymentDisputedEvent
    end
    Handler->>TopicMapper: determine topic
    Handler->>Outbox: publish event to topic
    Outbox-->>Handler: acknowledgment
    Handler-->>StripeWebhook: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% 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
Title check ✅ Passed The title clearly and concisely summarizes the three main features added: platform fee support, refund/dispute events, and the CancelPayment RPC implementation.
Description check ✅ Passed The description provides detailed context on all changes across multiple tasks, lists affected files, and includes comprehensive test coverage details.

✏️ 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 test-coverage-80--2-3-4--gateway-features
📝 Coding Plan
  • Generate coding plan for human review comments

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

@claude
Copy link
Copy Markdown

claude Bot commented Mar 18, 2026

Claude Code Review

Commit: ec01f963e71fafe1bdb3d4c22f7be8e13b68236e | CI: running

Summary

Solid implementation of three related financial-gateway features: platform fee wiring, refund/dispute webhook events, and CancelPayment RPC. The code follows established patterns well — event mapping mirrors existing SETTLED/REJECTED handlers, the adapter/service layering is clean, and error handling is thorough. Previous review findings (overly broad 400 handling and discarded cancel result) have been fully addressed.

Risk Assessment

Area Level Detail
Blast radius Low Financial-gateway only; new event topics are additive
Rollback Safe Proto fields are additive; no migrations; topic constants already exist
Scale Low Cancel is single PI lookup + cancel; webhook events follow existing outbox pattern
Cross-system Low New Kafka topics are additive; downstream consumers can adopt at their own pace
Migration N/A No database changes

Findings

Severity Location Description Status
Note payment_intent_adapter.go:329 String-matching Stripe error message for idempotency detection is fragile but acceptable — Stripe does not expose a structured field for the prior state. Test coverage validates the exact string. Acknowledged
Note grpc_service.go:155 Response hardcodes Status CANCELLED rather than mapping from result.Status. This is correct since CancelPayment only returns nil error on successful (or idempotent) cancellation. Acknowledged

Bot Review Notes

CodeRabbit thread 1 (events.proto:125): Keep provider_reference_id stable across lifecycle events. CodeRabbit flags that PaymentRefundedEvent.provider_reference_id will contain a Stripe Charge ID while PaymentCapturedEvent.provider_reference_id contains a PaymentIntent ID. Assessment: Valid observation but not a bug. Refund webhooks originate from charge objects, so the charge ID is the correct provider reference. The proto field comments already document this. Downstream consumers should treat provider_reference_id as opaque per-event-type.

CodeRabbit thread 2 (grpc_service.go:236): Don't report transient cancel-provider failures as Internal. Assessment: Already addressed. The current mapCancelError default branch returns codes.Unavailable, not codes.Internal.

CodeRabbit thread 3 (payment_intent_adapter.go:346): Fragility of string-matching Stripe error messages. Assessment: Valid concern, acceptable trade-off. There is no structured alternative from the Stripe API. The test suite validates the exact string pattern, so any Stripe-side change will surface as a test failure.

Previously Flagged

Severity Location Description Status
Critical payment_intent_adapter.go Overly broad 400 handling treated every Stripe 400 as idempotent success Resolved — now checks ErrorCodePaymentIntentUnexpectedState + message substring
Critical grpc_service.go CancelResult discarded; response always returned CANCELLED Resolved — result captured and logged; error path prevents false CANCELLED

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 60.16260% with 49 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
services/financial-gateway/service/grpc_service.go 6.00% 47 Missing ⚠️
...-gateway/adapters/stripe/payment_intent_adapter.go 97.67% 1 Missing ⚠️
services/financial-gateway/client/starlark.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

claude[bot]
claude Bot previously requested changes Mar 18, 2026
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Two correctness concerns in the CancelPayment path. See summary comment for full details.

Comment thread services/financial-gateway/adapters/stripe/payment_intent_adapter.go Outdated
Comment thread services/financial-gateway/service/grpc_service.go
…g result

Address review feedback:
- Only treat "status of canceled" Stripe 400 as idempotent success.
  Succeeded/captured payments now correctly return an error.
- Log the CancelResult in grpc_service instead of discarding it.
- Add test for already-succeeded cancel attempt returning error.
coderabbitai[bot]
coderabbitai Bot previously requested changes Mar 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
services/financial-gateway/adapters/http/webhook_handler.go (1)

191-194: ⚠️ Potential issue | 🟡 Minor

Stale comment examples after this change.

The comment cites REFUNDED and DISPUTED as examples of events without domain mappings, but this PR adds mappings for both. Consider updating the examples to reflect current behavior.

📝 Suggested fix
 		if errors.As(err, &noMapping) {
-			// Events with no domain event mapping (e.g., REFUNDED, DISPUTED) are
-			// acknowledged to prevent Stripe retries. Full handling is out of scope here.
+			// Events with no domain event mapping are acknowledged to prevent
+			// Stripe retries.
 			h.writeSuccess(w, "event acknowledged")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/financial-gateway/adapters/http/webhook_handler.go` around lines 191
- 194, Update the stale comment above the early-ack path in webhook_handler.go
(the block that calls h.writeSuccess(w, "event acknowledged")) to remove or
replace the examples REFUNDED and DISPUTED since this PR added domain mappings
for them; edit the comment in the function handling Stripe events (the code path
that returns after h.writeSuccess) to either list correct unmapped event
examples or simply state "events with no domain event mapping" without specific
examples so the comment stays accurate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/proto/meridian/financial_gateway_events/v1/events.proto`:
- Around line 121-125: The new event field provider_reference_id in this proto
is being repurposed to mean a Stripe Charge ID but is already used as the Stripe
PaymentIntent ID in PaymentCapturedEvent and exposed upstream via
DispatchPaymentResponse, which will break consumers; update the proto so
provider_reference_id continues to carry the PaymentIntent ID (same semantic as
PaymentCapturedEvent and DispatchPaymentResponse) or, if you need to record the
Charge ID, add a new distinct field like charge_id or provider_charge_id instead
of reusing provider_reference_id; ensure the change is applied for all affected
messages (including the other occurrence mentioned) so consumers can reliably
correlate lifecycle events.

In `@services/financial-gateway/service/grpc_service.go`:
- Around line 214-228: The cancel error mapper mapCancelError currently returns
codes.Internal for unknown errors; instead delegate to the existing
upstream/provider mapping by returning the result of mapStripeError(err) in the
default branch (so cancel-path network/provider/dispatch failures become
codes.Unavailable like mapStripeError does) while preserving the existing
specific cases (stripeadapter.ErrCancelNotConfigured,
stripeadapter.ErrPaymentIntentNotFound, stripeadapter.ErrMissingStripeAccount,
context.Canceled, context.DeadlineExceeded).

---

Outside diff comments:
In `@services/financial-gateway/adapters/http/webhook_handler.go`:
- Around line 191-194: Update the stale comment above the early-ack path in
webhook_handler.go (the block that calls h.writeSuccess(w, "event
acknowledged")) to remove or replace the examples REFUNDED and DISPUTED since
this PR added domain mappings for them; edit the comment in the function
handling Stripe events (the code path that returns after h.writeSuccess) to
either list correct unmapped event examples or simply state "events with no
domain event mapping" without specific examples so the comment stays accurate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3bab05f9-65ff-435e-b2f6-7c174ac5a503

📥 Commits

Reviewing files that changed from the base of the PR and between 094cbcb and a2b31c3.

📒 Files selected for processing (10)
  • api/proto/meridian/financial_gateway/v1/financial_gateway.proto
  • api/proto/meridian/financial_gateway_events/v1/events.proto
  • services/financial-gateway/adapters/http/webhook_handler.go
  • services/financial-gateway/adapters/http/webhook_handler_test.go
  • services/financial-gateway/adapters/stripe/payment_intent_adapter.go
  • services/financial-gateway/adapters/stripe/payment_intent_adapter_test.go
  • services/financial-gateway/client/starlark.go
  • services/financial-gateway/e2e/e2e_test.go
  • services/financial-gateway/service/grpc_service.go
  • services/financial-gateway/service/grpc_service_test.go

Comment thread api/proto/meridian/financial_gateway_events/v1/events.proto
Comment thread services/financial-gateway/service/grpc_service.go
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

See summary comment. No blocking issues found. Previous concern resolved in commit 0b299b8. LGTM.

coderabbitai[bot]
coderabbitai Bot previously requested changes Mar 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (1)
services/financial-gateway/service/grpc_service.go (1)

219-235: ⚠️ Potential issue | 🟠 Major

Transient cancel-provider failures still reported as Internal.

The past review comment about this issue remains valid. The default branch returns codes.Internal, but provider failures (network issues, Stripe API errors) should return codes.Unavailable to match mapStripeError's behavior and enable proper client retry logic.

Internal signals a server bug, while Unavailable indicates temporary provider issues that may succeed on retry.

🔧 Proposed fix
 func mapCancelError(err error) error {
 	switch {
 	case errors.Is(err, stripeadapter.ErrCancelNotConfigured):
 		return status.Error(codes.Unimplemented, "cancel support not configured")
 	case errors.Is(err, stripeadapter.ErrPaymentIntentNotFound):
 		return status.Error(codes.NotFound, "payment intent not found for payment order")
 	case errors.Is(err, stripeadapter.ErrMissingStripeAccount):
 		return status.Error(codes.FailedPrecondition, "stripe connected account not configured")
 	case errors.Is(err, context.Canceled):
 		return status.Error(codes.Canceled, "request canceled")
 	case errors.Is(err, context.DeadlineExceeded):
 		return status.Error(codes.DeadlineExceeded, "request deadline exceeded")
 	default:
-		return status.Error(codes.Internal, "failed to cancel payment")
+		return status.Error(codes.Unavailable, "stripe cancel failed")
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/financial-gateway/service/grpc_service.go` around lines 219 - 235,
The mapCancelError function currently maps unknown/provider failures to
codes.Internal; change its default branch to return codes.Unavailable (matching
mapStripeError behavior) so transient provider/network/Stripe API errors are
reported as Unavailable for proper client retries; update the default return in
mapCancelError to use status.Error(codes.Unavailable, "failed to cancel
payment") and ensure any tests or callers that expect Internal are adjusted
accordingly.
🧹 Nitpick comments (1)
services/financial-gateway/adapters/stripe/payment_intent_adapter.go (1)

318-322: Consider: The reason parameter is only used as a presence check.

The method accepts a reason string but discards its value, always using "requested_by_customer". This might be intentional since Stripe only accepts specific enum values for CancellationReason, but it could confuse callers who expect their reason to be forwarded.

Consider either:

  1. Documenting this behavior in the method's doc comment
  2. Mapping the input reason to valid Stripe cancellation reasons
  3. Changing the parameter to a boolean or enum if only presence matters
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/financial-gateway/adapters/stripe/payment_intent_adapter.go` around
lines 318 - 322, The function currently ignores the value of the reason
parameter and always sets params.CancellationReason to "requested_by_customer";
update the implementation (the block that creates params :=
&stripego.PaymentIntentCancelParams{} and sets params.CancellationReason) to
either (A) validate and map the incoming reason string to a permitted Stripe
cancellation enum and assign that mapped value via params.CancellationReason =
stripego.String(mappedReason), returning an error for unknown values, or (B)
change the function signature to accept a boolean/typed enum instead of a
free-form string and use that to set CancellationReason; if you opt not to
change behavior, add a clear doc comment on the function that the reason
parameter is only a presence flag and that "requested_by_customer" is always
used. Ensure you also keep params.SetStripeAccount(accountID) unchanged.
🤖 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/financial-gateway/adapters/stripe/payment_intent_adapter.go`:
- Around line 337-346: Replace the fragile string match on stripeErr.Msg with a
check on stripeErr.Code (or fetch the PaymentIntent to inspect its Status) in
the cancellation handling code: where the adapter currently does
strings.Contains(stripeErr.Msg, "status of canceled"), use stripeErr.Code ==
"<appropriate_stripe_code_for_already_canceled>" (or call the Stripe API to
retrieve the PaymentIntent status and confirm it's already canceled) and then
return the same CancelResult with ProviderReference piID and Status
DispatchStatus_DISPATCH_STATUS_FAILED; update references to stripeErr, piID,
paymentOrderID and the CancelResult return path in the cancellation handling
branch accordingly.

---

Duplicate comments:
In `@services/financial-gateway/service/grpc_service.go`:
- Around line 219-235: The mapCancelError function currently maps
unknown/provider failures to codes.Internal; change its default branch to return
codes.Unavailable (matching mapStripeError behavior) so transient
provider/network/Stripe API errors are reported as Unavailable for proper client
retries; update the default return in mapCancelError to use
status.Error(codes.Unavailable, "failed to cancel payment") and ensure any tests
or callers that expect Internal are adjusted accordingly.

---

Nitpick comments:
In `@services/financial-gateway/adapters/stripe/payment_intent_adapter.go`:
- Around line 318-322: The function currently ignores the value of the reason
parameter and always sets params.CancellationReason to "requested_by_customer";
update the implementation (the block that creates params :=
&stripego.PaymentIntentCancelParams{} and sets params.CancellationReason) to
either (A) validate and map the incoming reason string to a permitted Stripe
cancellation enum and assign that mapped value via params.CancellationReason =
stripego.String(mappedReason), returning an error for unknown values, or (B)
change the function signature to accept a boolean/typed enum instead of a
free-form string and use that to set CancellationReason; if you opt not to
change behavior, add a clear doc comment on the function that the reason
parameter is only a presence flag and that "requested_by_customer" is always
used. Ensure you also keep params.SetStripeAccount(accountID) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c09f4885-2e8c-486f-8aa2-b5bd13dfb4ed

📥 Commits

Reviewing files that changed from the base of the PR and between a2b31c3 and 0b299b8.

📒 Files selected for processing (4)
  • services/financial-gateway/adapters/http/webhook_handler.go
  • services/financial-gateway/adapters/stripe/payment_intent_adapter.go
  • services/financial-gateway/adapters/stripe/payment_intent_adapter_test.go
  • services/financial-gateway/service/grpc_service.go

- Use ErrorCodePaymentIntentUnexpectedState for cancel idempotency check
- Map default cancel errors to codes.Unavailable instead of Internal
- Add ErrInvalidRequest mapping for non-cancellable payment states
- Fix AlreadyCancelled test mock to include proper error code
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Previous critical findings resolved. All three CodeRabbit threads evaluated — one already addressed, two are valid observations but not correctness bugs. See summary comment for details.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
services/financial-gateway/adapters/stripe/payment_intent_adapter.go (1)

333-349: ⚠️ Potential issue | 🟠 Major

Avoid message-fragment matching for idempotent cancel detection.

On Line 337, using strings.Contains(stripeErr.Msg, "status of canceled") is brittle. If Stripe message text changes, already-cancelled requests can incorrectly fall through to ErrInvalidRequest (mapped to FailedPrecondition in services/financial-gateway/service/grpc_service.go Line 228), breaking idempotent behavior.

Proposed fix (prefer structured Stripe fields over message text)
-		if errors.As(err, &stripeErr) && stripeErr.Code == stripego.ErrorCodePaymentIntentUnexpectedState {
+		if errors.As(err, &stripeErr) && stripeErr.Code == stripego.ErrorCodePaymentIntentUnexpectedState {
 			// Stripe returns payment_intent_unexpected_state when the PI
 			// is in a non-cancellable state. Check if it's already canceled
 			// (idempotent success) vs a truly non-cancellable state (e.g., succeeded).
-			if strings.Contains(stripeErr.Msg, "status of canceled") {
+			if stripeErr.PaymentIntent != nil && stripeErr.PaymentIntent.Status == stripego.PaymentIntentStatusCanceled {
 				a.logger.Info("stripe payment intent already cancelled",
 					"payment_order_id", paymentOrderID,
 					"payment_intent_id", piID,
 				)
 				return CancelResult{
 					ProviderReference: piID,
 					Status:            financialgatewayv1.DispatchStatus_DISPATCH_STATUS_FAILED,
 				}, nil
 			}
 			// Non-cancellable state (e.g., succeeded) — return as invalid request
 			return CancelResult{}, fmt.Errorf("payment intent %s cannot be cancelled: %w", piID, ErrInvalidRequest)
 		}
For stripe-go v82, when canceling a PaymentIntent fails with code `payment_intent_unexpected_state`, is `stripe.Error.PaymentIntent.Status` reliably populated with the current status (e.g., `canceled`)?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/financial-gateway/adapters/stripe/payment_intent_adapter.go` around
lines 333 - 349, The code currently detects an already-cancelled PaymentIntent
by matching text in stripeErr.Msg (strings.Contains(stripeErr.Msg, "status of
canceled")), which is brittle; instead check the structured Stripe error
payload: when handling the payment_intent_unexpected_state branch, verify
stripeErr.PaymentIntent is non-nil and compare stripeErr.PaymentIntent.Status to
the canonical canceled status (e.g., stripego.PaymentIntentStatusCanceled or the
literal "canceled"), then treat that as idempotent success by returning the
CancelResult (ProviderReference piID, Status DISPATCH_STATUS_FAILED) as before;
otherwise fall back to returning ErrInvalidRequest. Update any imports/types as
needed and keep the surrounding logic in the same function handling the
stripeErr variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@services/financial-gateway/adapters/stripe/payment_intent_adapter.go`:
- Around line 333-349: The code currently detects an already-cancelled
PaymentIntent by matching text in stripeErr.Msg (strings.Contains(stripeErr.Msg,
"status of canceled")), which is brittle; instead check the structured Stripe
error payload: when handling the payment_intent_unexpected_state branch, verify
stripeErr.PaymentIntent is non-nil and compare stripeErr.PaymentIntent.Status to
the canonical canceled status (e.g., stripego.PaymentIntentStatusCanceled or the
literal "canceled"), then treat that as idempotent success by returning the
CancelResult (ProviderReference piID, Status DISPATCH_STATUS_FAILED) as before;
otherwise fall back to returning ErrInvalidRequest. Update any imports/types as
needed and keep the surrounding logic in the same function handling the
stripeErr variable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 056b7d94-3cc5-411f-9f14-b22a50608d9f

📥 Commits

Reviewing files that changed from the base of the PR and between 0b299b8 and ec01f96.

📒 Files selected for processing (3)
  • services/financial-gateway/adapters/stripe/payment_intent_adapter.go
  • services/financial-gateway/adapters/stripe/payment_intent_adapter_test.go
  • services/financial-gateway/service/grpc_service.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/financial-gateway/adapters/stripe/payment_intent_adapter_test.go

@bjcoombs bjcoombs dismissed stale reviews from claude[bot], coderabbitai[bot], and coderabbitai[bot] March 18, 2026 10:14

Stale bot review — verified findings addressed

@bjcoombs bjcoombs merged commit 344b8ce into develop Mar 18, 2026
43 checks passed
@bjcoombs bjcoombs deleted the test-coverage-80--2-3-4--gateway-features branch March 18, 2026 10:14
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