feat(payment): canceled status, manageable origin allowlist, CORS subdomains

Unify and harden the payment flow (same API for the main site and all
consumer sites; per-client difference is only frontend_address).

- Payment gains STATUS_CANCELED. Gateways distinguish user-cancel from
  failure (Mellat ResCode=17, SEP CanceledByUser, mock cancel=1) via a new
  PaymentVerifyResult::canceled flag; callback sets canceled vs failed and
  skips the circuit-breaker on cancel.
- Expiry job now cancels the pending payment when a booking lapses
  (AppointmentExpiryService + PaymentRepository::findPendingByAppointment).
- frontend_address allowlist is read from the payment_allowed_frontend_hosts
  site setting (manageable via PATCH /api/v1/admin/settings), falling back to
  the ALLOWED_FRONTEND_HOSTS env var — so a new consumer site needs no code
  change.
- .env: broaden CORS_ALLOW_ORIGIN to city subdomains (*.localhost /
  *.clinic-pro.ddev.site) and add yazd-nobat.localhost to ALLOWED_FRONTEND_HOSTS.
- Update docs/api/payment.md and docs/api/admin.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-16 10:06:43 +03:30
co-authored by Claude Opus 4.8
parent 45242a3128
commit 492a7df989
13 changed files with 237 additions and 16 deletions
@@ -2,6 +2,7 @@
namespace App\Payment\Repository;
use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Payment\Entity\Payment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
@@ -53,6 +54,14 @@ class PaymentRepository extends ServiceEntityRepository
return $this->findOneBy(['orderId' => $orderId]);
}
public function findPendingByAppointment(Appointment $appointment): ?Payment
{
return $this->findOneBy([
'appointment' => $appointment,
'status' => Payment::STATUS_PENDING,
]);
}
public function save(Payment $entity, bool $flush = true): void
{
$this->getEntityManager()->persist($entity);