Skip to content

[Refactor] 결제 승인 트랜잭션 분리 및 멱등성 보강 (#62)#103

Merged
shinae1023 merged 2 commits into
mainfrom
refactor/#62-payment
Jun 18, 2026
Merged

[Refactor] 결제 승인 트랜잭션 분리 및 멱등성 보강 (#62)#103
shinae1023 merged 2 commits into
mainfrom
refactor/#62-payment

Conversation

@shinae1023

@shinae1023 shinae1023 commented Jun 18, 2026

Copy link
Copy Markdown
Member

✨ 어떤 이유로 PR를 하셨나요?

  • feature 병합
  • 버그 수정(아래에 issue #를 남겨주세요)
  • 코드 개선
  • 코드 수정
  • 배포
  • 기타(아래에 자세한 내용 기입해주세요)

📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요

  • 결제 승인 흐름을 외부 PG 호출과 내부 DB 트랜잭션으로 분리했습니다.

  • 결제 상태 전이를 PENDING -> PROCESSING -> COMPLETED/FAILED로 정리했습니다.

  • 크레딧 거래에 referenceId 기반 멱등성을 추가해 중복 충전/차감/환불을 방지했습니다.

  • 결제 재시도, 승인 실패, 크레딧 멱등성 관련 테스트를 보강했습니다.

  • 기존에는 외부 결제 승인 호출이 DB 트랜잭션 안에서 수행되어 락 점유 시간이 길고 장애 전파 위험이 있었습니다.

  • 동일 요청 재시도나 복구 시 크레딧 원장 중복 반영 가능성을 줄일 필요가 있었습니다.

  • 결제 상태를 더 명확하게 표현해 운영 중 추적성과 복구 가능성을 높이기 위함입니다.

📸 작업 화면 스크린샷

⚠️ PR하기 전에 확인해주세요

  • 로컬테스트를 진행하셨나요?
  • 머지할 브랜치를 확인하셨나요?
  • 관련 label을 선택하셨나요?

🚨 관련 이슈 번호 [#62]

Summary by CodeRabbit

  • New Features

    • Added idempotency to payment and credit operations to prevent duplicate charges when requests are retried.
    • Enhanced payment state tracking with improved confirmation flow and failure handling.
  • Bug Fixes

    • Improved payment processing reliability to ensure failed payments are properly handled without unintended credit charges.
  • Tests

    • Added comprehensive test coverage for idempotency scenarios and payment failure handling.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@shinae1023, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 45 minutes and 23 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 185cf705-cd94-4447-b49c-3484169e1781

📥 Commits

Reviewing files that changed from the base of the PR and between 7e94f91 and 8cd0ef5.

📒 Files selected for processing (1)
  • ops/db/migrations/20260618_credit_transactions_unique_reference.sql
📝 Walkthrough

Walkthrough

Introduces a PROCESSING payment status and refactors the payment confirmation flow to use a new PaymentTransactionService that applies pessimistic row-locking across start/complete/fail steps. CreditService gains referenceId-based idempotency via a new unique constraint and a shared apply() path. PaymentService.confirm is changed to NOT_SUPPORTED propagation and delegates entirely to the new service.

Changes

Idempotent Payment Confirmation Flow

Layer / File(s) Summary
Payment entity state and CreditTransaction schema
src/main/java/com/jobdri/jobdri_api/domain/payment/entity/PaymentStatus.java, src/main/java/com/jobdri/jobdri_api/domain/payment/entity/Payment.java, src/main/java/com/jobdri/jobdri_api/domain/payment/entity/CreditTransaction.java
Adds PROCESSING enum constant to PaymentStatus; adds markProcessing, belongsTo, and hasPaymentKey methods to Payment; expands CreditTransaction with a composite unique constraint on (user_id, type, reference_id) and explicit reference_id column mapping.
CreditService idempotency via referenceId
src/main/java/com/jobdri/jobdri_api/domain/payment/repository/CreditTransactionRepository.java, src/main/java/com/jobdri/jobdri_api/domain/payment/service/CreditService.java
Adds findByUserIdAndTypeAndReferenceId to the repository; refactors charge/use/refund to a shared apply() that validates referenceId, returns existing balanceAfter on duplicate, and centralizes credit adjustment via applyCreditChange and resolveTransactionAmount.
PaymentTransactionService: locked confirmation lifecycle
src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java
New service with startConfirmation (pessimistic lock, PROCESSING transition, paymentKey-based idempotency), completeConfirmation (enforces PROCESSING, calls CreditService.charge), failConfirmation (PROCESSINGFAILED on key match), and helpers getOwnedPaymentForUpdate and validateAmount.
PaymentService.confirm delegation and propagation change
src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java
Replaces CreditService injection with PaymentTransactionService, changes confirm propagation to NOT_SUPPORTED, and delegates the full confirmation lifecycle; removes inline payment lookup, ownership/status/amount checks, and direct credit charging.
Idempotency and failure tests
src/test/java/com/jobdri/jobdri_api/domain/payment/service/CreditServiceTest.java, src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java
Verifies charge/use idempotency by referenceId (single transaction persisted on duplicate calls), repeat-confirm idempotency (one CHARGE, same paymentId returned), and Toss failure path (payment FAILED, no credit charged, no CHARGE transaction).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PaymentService
  participant PaymentTransactionService
  participant TossPaymentClient
  participant CreditService

  Client->>PaymentService: confirm(user, request)
  PaymentService->>PaymentTransactionService: startConfirmation(userId, request)
  PaymentTransactionService-->>PaymentService: PaymentConfirmationStart(alreadyCompleted)

  alt alreadyCompleted
    PaymentService-->>Client: existing PaymentConfirmResponse
  else
    PaymentService->>TossPaymentClient: confirm(orderId, paymentKey, amount)
    alt RuntimeException
      TossPaymentClient-->>PaymentService: throws GeneralException
      PaymentService->>PaymentTransactionService: failConfirmation(userId, orderId, paymentKey)
      PaymentTransactionService-->>PaymentService: payment set to FAILED
      PaymentService-->>Client: rethrows exception
    else success
      TossPaymentClient-->>PaymentService: TossPaymentResponse
      PaymentService->>PaymentTransactionService: completeConfirmation(userId, request)
      PaymentTransactionService->>CreditService: charge(userId, amount, orderId)
      PaymentTransactionService-->>PaymentService: PaymentConfirmResponse
      PaymentService-->>Client: PaymentConfirmResponse
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • JobDri-Developer/BackEnd#70: Initial implementation of the Toss payment confirmation and credit-charge pipeline (PaymentService, TossPaymentClient, CreditService, payment/credit entities) that this PR directly refactors for idempotency and transactional safety.
  • JobDri-Developer/BackEnd#71: Added pessimistic locking (findByOrderIdForUpdate) and concurrency fixes to the payment confirmation flow that this PR builds upon with PaymentTransactionService and the PROCESSING state.

Suggested labels

fix

Poem

🐰 A rabbit once paid twice by mistake,
But now idempotency's here for goodness sake!
PROCESSING guards the lock with care,
referenceId says "no duplicates there!"
One charge, one credit, one happy hare. 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main changes: refactoring payment approval transaction separation and strengthening idempotency, which directly aligns with the PR's primary objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description follows the required template and includes all critical sections: reason for PR (코드 개선, 코드 수정), detailed explanation of changes with clear objectives, and related issue number (#62).

✏️ 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 refactor/#62-payment

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

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)
src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java (1)

115-143: ⚡ Quick win

Consider verifying mock invocation count for clarity.

The test correctly validates idempotency, but explicitly verifying that tossPaymentClient.confirm is called exactly once (not twice) would make the early-return behavior more obvious to readers.

✨ Proposed enhancement

Add after line 142:

         assertThat(creditTransactionRepository.findAllByUserIdAndTypeOrderByCreatedAtDescIdDesc(
                 user.getId(),
                 CreditTransactionType.CHARGE
         )).hasSize(1);
+        verify(tossPaymentClient, times(1)).confirm(paymentKey, prepared.orderId(), 2500);
     }

You'll need to add this import:

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java`
around lines 115 - 143, The test
confirmReturnsExistingResultWhenAlreadyCompleted validates idempotency through
assertions but lacks an explicit verification that tossPaymentClient.confirm is
called exactly once, making the early-return behavior less obvious. Add a verify
statement after the existing assertions to explicitly check that
tossPaymentClient.confirm is invoked exactly once using
verify(tossPaymentClient, times(1)).confirm(paymentKey, prepared.orderId(),
2500). Additionally, add the required Mockito imports for verify and times at
the top of the test file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/entity/CreditTransaction.java`:
- Around line 13-21: The unique constraint defined in the CreditTransaction
entity's `@UniqueConstraint` annotation
(uk_credit_transactions_user_type_reference on user_id, type, and reference_id)
exists only in JPA metadata and is not enforced in the actual database schema.
Create a Flyway or Liquibase migration file in ops/db/migrations/ that first
identifies and removes any existing duplicate rows in the credit_transactions
table that violate the (user_id, type, reference_id) uniqueness requirement,
then explicitly adds the unique constraint using ALTER TABLE to enforce it at
the database level, ensuring the constraint definition matches what is declared
in the CreditTransaction entity's `@UniqueConstraint` annotation, and includes
pre-migration validation to confirm no duplicates remain before applying the
constraint.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/CreditService.java`:
- Around line 41-50: The code currently treats a transaction as idempotent based
solely on matching (user, type, referenceId), but does not verify that the
amount in a retry request matches the original transaction amount. To fix this,
after retrieving the existingTransaction, calculate the transactionAmount for
the current request using resolveTransactionAmount, then compare this amount
with the existing transaction's amount before returning the old balance. If the
amounts do not match, reject the request by throwing an appropriate exception to
prevent amount mismatches from being silently ignored.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java`:
- Around line 80-86: The TossPaymentClient.confirm() method currently wraps both
HTTP errors and transient failures (timeouts, connection issues) into a single
GeneralException, causing PaymentService.confirm() to treat all exceptions
identically and mark payments as FAILED. To fix this, modify
TossPaymentClient.confirm() to distinguish between definitive rejections (4xx
HTTP responses) and transient failures (connection timeouts, network errors) by
throwing different exception types for each case. Then in
PaymentService.confirm(), update the catch block to only call
paymentTransactionService.failConfirmation() when catching the exception type
that represents definitive rejections, allowing transient failures to remain
unhandled so the payment stays in PROCESSING state for later reconciliation.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java`:
- Around line 35-37: The validation in PaymentTransactionService.confirm method
currently rejects all retries for payments in PROCESSING status by throwing an
exception on line 35, which prevents recovery if the confirmation process fails
after the status is set to PROCESSING. Modify the validation logic to allow
retries for the same paymentKey to be resumable; instead of unconditionally
throwing an exception for PROCESSING status, check if the incoming paymentKey
matches an existing PROCESSING payment and allow it to proceed as a resumable
operation rather than rejecting it as an error condition.

---

Nitpick comments:
In
`@src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java`:
- Around line 115-143: The test confirmReturnsExistingResultWhenAlreadyCompleted
validates idempotency through assertions but lacks an explicit verification that
tossPaymentClient.confirm is called exactly once, making the early-return
behavior less obvious. Add a verify statement after the existing assertions to
explicitly check that tossPaymentClient.confirm is invoked exactly once using
verify(tossPaymentClient, times(1)).confirm(paymentKey, prepared.orderId(),
2500). Additionally, add the required Mockito imports for verify and times at
the top of the test file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a9ceb38-23ce-4d3e-9109-54184c58f907

📥 Commits

Reviewing files that changed from the base of the PR and between d3a4e80 and 7e94f91.

📒 Files selected for processing (9)
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/CreditTransaction.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/Payment.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/PaymentStatus.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/repository/CreditTransactionRepository.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/CreditService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/CreditServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java

Comment on lines +13 to +21
@Table(
name = "credit_transactions",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_credit_transactions_user_type_reference",
columnNames = {"user_id", "type", "reference_id"}
)
}
)

Copy link
Copy Markdown

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
# Description: Look for schema migrations or ddl-auto config that creates/enforces
# the credit_transactions reference_id mapping and composite uniqueness.

echo "Migration/config files:"
fd -i '(^V[0-9].*|.*changelog.*|.*schema.*|.*migration.*|application.*\.(yml|yaml|properties))$'

echo
echo "Relevant schema/config references:"
rg -n -C3 'uk_credit_transactions_user_type_reference|credit_transactions|reference_id|ddl-auto|hibernate\.hbm2ddl'

Repository: JobDri-Developer/BackEnd

Length of output: 3867


Create an explicit migration to enforce the unique constraint and backfill existing duplicates.

The idempotency constraint (user_id, type, reference_id) is defined only in JPA metadata at lines 13-21. No explicit Flyway/Liquibase migration exists in ops/db/migrations/ and the constraint is absent from schema.sql. When deployed to production, ddl-auto: update cannot reliably create or enforce complex constraints, and existing duplicate rows will cause the constraint application to fail.

Create a migration file to:

  1. Identify and deduplicate existing credit_transactions rows by (user_id, type, reference_id)
  2. Add the unique constraint explicitly via ALTER TABLE
  3. Ensure pre-deployment validation that no duplicates remain

Also applies to: 45-46

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/entity/CreditTransaction.java`
around lines 13 - 21, The unique constraint defined in the CreditTransaction
entity's `@UniqueConstraint` annotation
(uk_credit_transactions_user_type_reference on user_id, type, and reference_id)
exists only in JPA metadata and is not enforced in the actual database schema.
Create a Flyway or Liquibase migration file in ops/db/migrations/ that first
identifies and removes any existing duplicate rows in the credit_transactions
table that violate the (user_id, type, reference_id) uniqueness requirement,
then explicitly adds the unique constraint using ALTER TABLE to enforce it at
the database level, ensuring the constraint definition matches what is declared
in the CreditTransaction entity's `@UniqueConstraint` annotation, and includes
pre-migration validation to confirm no duplicates remain before applying the
constraint.

Comment on lines +41 to +50
CreditTransaction existingTransaction = creditTransactionRepository
.findByUserIdAndTypeAndReferenceId(managedUser.getId(), type, referenceId)
.orElse(null);
if (existingTransaction != null) {
return existingTransaction.getBalanceAfter();
}

int transactionAmount = resolveTransactionAmount(type, amount);
applyCreditChange(managedUser, type, amount);
saveTransaction(managedUser, type, transactionAmount, description, referenceId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject same referenceId replays with a different amount.

Line 44 returns the old balance for any existing (user, type, referenceId), even when the retry carries a different amount. Compare the signed transaction amount before treating the call as idempotent.

Suggested fix
+        int transactionAmount = resolveTransactionAmount(type, amount);
         CreditTransaction existingTransaction = creditTransactionRepository
                 .findByUserIdAndTypeAndReferenceId(managedUser.getId(), type, referenceId)
                 .orElse(null);
         if (existingTransaction != null) {
+            if (existingTransaction.getAmount() != transactionAmount) {
+                throw new GeneralException(
+                        GeneralErrorCode.INVALID_PARAMETER,
+                        "referenceId가 기존 거래 금액과 일치하지 않습니다."
+                );
+            }
             return existingTransaction.getBalanceAfter();
         }
 
-        int transactionAmount = resolveTransactionAmount(type, amount);
         applyCreditChange(managedUser, type, amount);
         saveTransaction(managedUser, type, transactionAmount, description, referenceId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/CreditService.java`
around lines 41 - 50, The code currently treats a transaction as idempotent
based solely on matching (user, type, referenceId), but does not verify that the
amount in a retry request matches the original transaction amount. To fix this,
after retrieving the existingTransaction, calculate the transactionAmount for
the current request using resolveTransactionAmount, then compare this amount
with the existing transaction's amount before returning the old balance. If the
amounts do not match, reject the request by throwing an appropriate exception to
prevent amount mismatches from being silently ignored.

Comment on lines +80 to +86
try {
TossPaymentConfirmResponse tossResponse =
tossPaymentClient.confirm(request.paymentKey(), request.orderId(), request.amount());
validateTossResponse(request, tossResponse);
} catch (RuntimeException e) {
paymentTransactionService.failConfirmation(validatedUser.getId(), request.orderId(), request.paymentKey());
throw e;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect Toss client failure taxonomy and confirm whether
# PaymentService can distinguish definitive payment rejection from unknown outcomes.

echo "Toss payment client definitions/usages:"
rg -n -C4 'class TossPaymentClient|interface TossPaymentClient|confirm\s*\('

echo
echo "Payment confirmation exception/status handling:"
rg -n -C4 'PAYMENT_CONFIRM_FAILED|PAYMENT_ALREADY_PROCESSED|RestClientException|WebClientResponseException|HttpStatus|RuntimeException'

Repository: JobDri-Developer/BackEnd

Length of output: 50380


Do not mark payments FAILED for unknown gateway outcomes.

TossPaymentClient.confirm() catches both HttpStatusCodeException (HTTP error responses) and RestClientException (timeouts, connection failures) and wraps both into GeneralException(GeneralErrorCode.PAYMENT_CONFIRM_FAILED) with no distinction. PaymentService.confirm() then treats all RuntimeExceptions identically, calling failConfirmation() to mark the payment FAILED before rethrowing.

This conflates definitive rejections with transient failures. If Toss approves but the response times out or the client fails to receive it, the payment becomes FAILED locally and unrecoverable.

Distinguish transient/unknown outcomes from definitive rejections in TossPaymentClient (e.g., map 4xx Toss responses to a specific exception type for true declines, leave transport failures as unknown). Only call failConfirmation() for definitive rejections; unknown outcomes should remain in PROCESSING for later reconciliation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java`
around lines 80 - 86, The TossPaymentClient.confirm() method currently wraps
both HTTP errors and transient failures (timeouts, connection issues) into a
single GeneralException, causing PaymentService.confirm() to treat all
exceptions identically and mark payments as FAILED. To fix this, modify
TossPaymentClient.confirm() to distinguish between definitive rejections (4xx
HTTP responses) and transient failures (connection timeouts, network errors) by
throwing different exception types for each case. Then in
PaymentService.confirm(), update the catch block to only call
paymentTransactionService.failConfirmation() when catching the exception type
that represents definitive rejections, allowing transient failures to remain
unhandled so the payment stays in PROCESSING state for later reconciliation.

Comment on lines +35 to +37
if (payment.getStatus() == PaymentStatus.PROCESSING || payment.getStatus() == PaymentStatus.FAILED) {
throw new GeneralException(GeneralErrorCode.PAYMENT_ALREADY_PROCESSED, "이미 처리된 결제입니다.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make same-key PROCESSING confirmations recoverable.

PaymentService.confirm commits PROCESSING before the Toss call; if the process dies or completion fails afterward, Line 35 rejects every retry and leaves the payment stuck without credit. Treat the same paymentKey as resumable, or add a lease/reconciliation path.

One possible local recovery path
-        if (payment.getStatus() == PaymentStatus.PROCESSING || payment.getStatus() == PaymentStatus.FAILED) {
+        if (payment.getStatus() == PaymentStatus.PROCESSING) {
+            if (!payment.hasPaymentKey(request.paymentKey())) {
+                throw new GeneralException(GeneralErrorCode.PAYMENT_ALREADY_PROCESSED, "이미 처리된 결제입니다.");
+            }
+            return new PaymentConfirmationStart(payment, false);
+        }
+        if (payment.getStatus() == PaymentStatus.FAILED) {
             throw new GeneralException(GeneralErrorCode.PAYMENT_ALREADY_PROCESSED, "이미 처리된 결제입니다.");
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java`
around lines 35 - 37, The validation in PaymentTransactionService.confirm method
currently rejects all retries for payments in PROCESSING status by throwing an
exception on line 35, which prevents recovery if the confirmation process fails
after the status is set to PROCESSING. Modify the validation logic to allow
retries for the same paymentKey to be resumable; instead of unconditionally
throwing an exception for PROCESSING status, check if the incoming paymentKey
matches an existing PROCESSING payment and allow it to proceed as a resumable
operation rather than rejecting it as an error condition.

@shinae1023 shinae1023 merged commit c45368f into main Jun 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant