Skip to content

Task 1-1: Idempotency Audit - Webhook Handlers

Phase: 1 Priority: High Module: web, webmarketplace Depends on: Không có Reference: docs/BountyHunter-Backend/details/feature-payment-marketplace/SPEC.md

Background

Webhook endpoints nhận confirm từ external gateways (Fincode, Slash, Apple, Google). Nếu gateway retry, hệ thống phải không double credit user.

Tasks

1. Audit tất cả webhook handlers

DI Note: Các handler trong PaymentService cần inject PaymentOrderRepository (hoặc tương đương) để query order status. UserSystemCoinService (hoặc tương đương) cần inject để credit coin. Đảm bảo cả 2 operations xảy ra trong cùng 1 @Transactional scope.

  • [ ] confirmCardPayment: check nếu order đã SUCCESS → skip (idempotency guard)
  • [ ] confirmCryptoPayment: verify Boolean return và idempotency — trace qua call chain để tìm nơi status check
  • [ ] confirmNftTransfer (3 branches: ADMIN, OWNER, default): check if already confirmed cho từng branch
  • [ ] mintCompressNftConfirm: check duplicate call và handle leafIndex=Unknown (log warn, không crash)

2. Chuẩn hóa pattern cho mọi webhook

// Pattern chuẩn (áp dụng cho mọi webhook handler):
@Transactional  // Status update + coin credit trong cùng 1 transaction
public ResponseWebhookDto confirmCardPayment(Map<String, Object> params) {
    String orderId = extractOrderId(params);
    PaymentOrder order = orderRepository.findById(orderId)
        .orElseThrow(() -> new BadRequestException("ORDER_NOT_FOUND"));

    // Idempotency guard: re-check status trước khi process
    if (order.getStatus() != PaymentStatus.PENDING) {
        LOGGER.warn("[WEBHOOK] Order {} already processed: status={}", orderId, order.getStatus());
        return ResponseWebhookDto.already_processed();
    }

    // Process in transaction:
    order.setStatus(PaymentStatus.SUCCESS);
    orderRepository.update(order);
    userSystemCoinService.deposit(order.getUserId(), order.getCoinAmount());
    return ResponseWebhookDto.ok();
}

3. Test signature validation

  • [ ] Fincode webhook: validate Fincode-Signature header — reject trước khi query DB
  • [ ] Slash webhook: validate signature theo Slash docs
  • [ ] Apple/Google IAP: validate receipt/purchaseToken với Apple/Google server trước khi credit

Verification / Acceptance Criteria

  • [ ] Tất cả 4 webhook handlers có idempotency guard (status check trước khi process)
  • [ ] Gọi cùng webhook 2 lần → lần 2 trả already_processed hoặc 200 không thay đổi state
  • [ ] Coin không bị credited 2 lần (verify balance unchanged sau duplicate webhook)
  • [ ] Invalid signature → 4xx response trước khi bất kỳ DB operation nào
  • [ ] @Transactional bao phủ cả status update lẫn coin credit

Files to Modify

  • web/controllers/coin/CoinController.java (confirmCardPayment, confirmCryptoPayment)
  • webmarketplace/controllers/nft/NftController.java (confirmNftTransfer, mintCompressNftConfirm)
  • application-core/service/payment/PaymentService.java (các confirm methods)