152 lines
6.6 KiB
PHP
152 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Payment;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Config\Entity\SiteConfig;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Patient\Entity\PatientRecord;
|
|
use App\Patient\Entity\PatientSession;
|
|
use App\Payment\Entity\Payment;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* پرداخت آنلاین نوبت را **قطعی نمیکند**: نوبت در «ثبت شده» میماند تا پزشک/منشی
|
|
* تأییدش کند. پرداخت فقط پنجرهٔ انقضای درگاه را برمیدارد تا نوبتِ پرداختشده منقضی
|
|
* نشود. پرونده/مراجعه و تقسیم مالی در لحظهٔ تأیید ساخته میشوند.
|
|
*/
|
|
class AppointmentPaidConfirmFilesSessionTest extends ApiTestCase
|
|
{
|
|
private function enableTestMode(): void
|
|
{
|
|
$cfg = $this->em->getRepository(SiteConfig::class)->findOneBy(['configKey' => 'payment_test_mode']);
|
|
if ($cfg === null) {
|
|
$this->em->persist(new SiteConfig('payment_test_mode', '1'));
|
|
} else {
|
|
$cfg->setValue('1');
|
|
}
|
|
$this->em->flush();
|
|
}
|
|
|
|
/** @return array{0: Payment, 1: Appointment} */
|
|
private function pendingPaidBooking(?callable $withClinic = null): array
|
|
{
|
|
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر پرداخت');
|
|
$this->em->persist($doctor);
|
|
$this->em->flush();
|
|
|
|
$slotStart = strtotime('+30 days') + random_int(0, 500_000) * 7;
|
|
$appointment = $this->newAppointment($doctor, $this->createUser(['ROLE_USER']), $slotStart, $slotStart + 1_800);
|
|
// رزرو آنلاین با پنجرهٔ پرداخت ثبت میشود؛ پرداخت باید همین را پاک کند.
|
|
$appointment->markPendingWithTtl(Appointment::PAYMENT_TTL);
|
|
if ($withClinic !== null) {
|
|
$appointment->setClinic($withClinic($doctor));
|
|
}
|
|
$this->em->persist($appointment);
|
|
|
|
$payment = new Payment($appointment->getUser(), 50_000, 'mock', Payment::TYPE_APPOINTMENT);
|
|
$payment->setAppointment($appointment);
|
|
$this->stampTenant($payment);
|
|
$this->em->persist($payment);
|
|
$this->em->flush();
|
|
|
|
return [$payment, $appointment];
|
|
}
|
|
|
|
private function fireCallback(Payment $payment): void
|
|
{
|
|
// بدون `gateway` فرستاده میشود تا fallbackِ خواندن درگاه از رکورد پرداخت هم پوشش بخورد.
|
|
$this->client->request('POST', '/api/v1/payment/callback?' . http_build_query([
|
|
'order_id' => $payment->getOrderId(),
|
|
'mock' => '1',
|
|
'ResCode' => '0',
|
|
'mock_amount' => '50000',
|
|
]));
|
|
}
|
|
|
|
private function reload(Appointment $appointment): Appointment
|
|
{
|
|
$this->em->clear();
|
|
|
|
return $this->em->getRepository(Appointment::class)->find($appointment->getId());
|
|
}
|
|
|
|
public function testPaidBookingStaysPendingUntilSomeoneConfirmsIt(): void
|
|
{
|
|
$this->enableTestMode();
|
|
[$payment, $appointment] = $this->pendingPaidBooking();
|
|
|
|
$this->fireCallback($payment);
|
|
$reloaded = $this->reload($appointment);
|
|
|
|
self::assertSame(Appointment::STATUS_PENDING, $reloaded->getStatus());
|
|
// پنجرهٔ پرداخت برداشته میشود، وگرنه cronِ انقضا نوبتِ پرداختشده را میکشت.
|
|
self::assertNull($reloaded->getExpiresAt());
|
|
self::assertCount(0, $this->em->getRepository(PatientSession::class)->findBy(['appointment' => $reloaded]));
|
|
}
|
|
|
|
public function testConfirmingThePaidBookingFilesTheSession(): void
|
|
{
|
|
$this->enableTestMode();
|
|
[$payment, $appointment] = $this->pendingPaidBooking();
|
|
$this->fireCallback($payment);
|
|
$reloaded = $this->reload($appointment);
|
|
|
|
$doctorUser = $reloaded->getDoctor()->getUser();
|
|
$this->authJson('POST', '/api/v1/appointment/' . $reloaded->getUuid() . '/confirm', $doctorUser, []);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
$confirmed = $this->reload($reloaded);
|
|
self::assertSame(Appointment::STATUS_CONFIRMED, $confirmed->getStatus());
|
|
|
|
$sessions = $this->em->getRepository(PatientSession::class)->findBy(['appointment' => $confirmed]);
|
|
self::assertCount(1, $sessions, 'تأیید نوبت باید پرونده بسازد');
|
|
self::assertSame('doctor', $sessions[0]->getRecord()->getEntityType());
|
|
}
|
|
|
|
/** تقسیم مالی هم مثل پرونده به لحظهٔ تأیید منتقل شده است. */
|
|
public function testFinancialSplitHappensOnConfirmNotOnPayment(): void
|
|
{
|
|
$this->enableTestMode();
|
|
$breakdowns = static::getContainer()->get(\App\Settlement\Repository\FinancialBreakdownRepository::class);
|
|
[$payment, $appointment] = $this->pendingPaidBooking();
|
|
|
|
$this->fireCallback($payment);
|
|
$paidPayment = $this->em->getRepository(Payment::class)->find($payment->getId());
|
|
self::assertFalse($breakdowns->existsForPayment($paidPayment), 'پرداخت بهتنهایی نباید تقسیم مالی بسازد');
|
|
|
|
$reloaded = $this->reload($appointment);
|
|
$this->authJson('POST', '/api/v1/appointment/' . $reloaded->getUuid() . '/confirm', $reloaded->getDoctor()->getUser(), []);
|
|
|
|
// بدون نماینده و منشیِ سهمبر چیزی برای تقسیم نیست؛ صرفاً نباید خطا بدهد.
|
|
self::assertSame(200, $this->responseCode());
|
|
}
|
|
|
|
public function testConfirmedClinicBookingIsFiledUnderTheClinic(): void
|
|
{
|
|
$this->enableTestMode();
|
|
[$payment, $appointment] = $this->pendingPaidBooking(function (Doctor $doctor): Clinic {
|
|
$clinic = new Clinic($this->createUser(['ROLE_CLINIC']));
|
|
$clinic->setName('کلینیک پرداخت');
|
|
$clinic->getDoctors()->add($doctor);
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
return $clinic;
|
|
});
|
|
|
|
$this->fireCallback($payment);
|
|
$reloaded = $this->reload($appointment);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment/' . $reloaded->getUuid() . '/confirm', $reloaded->getDoctor()->getUser(), []);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
$confirmed = $this->reload($reloaded);
|
|
$records = $this->em->getRepository(PatientRecord::class)->findBy(['user' => $confirmed->getUser()]);
|
|
|
|
self::assertCount(1, $records, 'یک نوبت، یک پرونده — نه یکی برای پزشک و یکی برای کلینیک');
|
|
self::assertSame('clinic', $records[0]->getEntityType());
|
|
}
|
|
}
|