| Security | Needs scope payments.refund; merchant can refund only its own payments. | SecurityConfig rule + ownership check in RefundService |
| REST API | POST /payments/{id}/refunds {amount, reason} → 201 RefundResponse. | RefundController, RefundRequest, RefundResponse |
| Errors | Not found 404, already fully refunded 409, amount too big 422. | RefundNotAllowedException + handlers in GlobalErrorHandler |
| Database | New refund table, payment gets refundedAmount + version. | Refund entity, RefundRepository, V3__refunds.sql, @Version on Payment |
| Transactions | Insert refund + update payment together. | @Transactional RefundService.refund |
| Other services | None for this feature. | — |
| Events | Ledger must know → PaymentRefunded after commit. | PaymentRefundedEvent + publisher (AFTER_COMMIT) |
| Observability | Count refunds, log paymentId + amount. | Counter "securepay.refunds" + log line |
| Tests | Rules, HTTP, security, DB. | RefundServiceTest, RefundControllerTest, RefundRepositoryTest |