fix(security): unique payment reference_id, reject replayed callbacks (H3)

reference_id (the gateway's settled-transaction ref) was not unique, so the
same successful callback — or a RefNum replayed onto another order — could
credit twice. Add a unique index (NULL until success, so pending/failed rows
don't collide) and an application-level pre-check in the callback that fails the
payment if the reference already belongs to another order. The unique index is
the hard backstop behind the check.

Regression: PaymentCallbackAmountTest::testReplayedGatewayReferenceIsRejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-28 19:08:26 +03:30
co-authored by Claude Opus 4.8
parent aa87b4a9cb
commit 7fa4b55d3f
7 changed files with 80 additions and 2 deletions
@@ -54,6 +54,11 @@ class PaymentRepository extends ServiceEntityRepository
return $this->findOneBy(['orderId' => $orderId]);
}
public function findByReferenceId(string $referenceId): ?Payment
{
return $this->findOneBy(['referenceId' => $referenceId]);
}
public function findPendingByAppointment(Appointment $appointment): ?Payment
{
return $this->findOneBy([