feat(billing): ensure finalized invoice for confirmed appointments with payments
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Billing;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Billing\Entity\Invoice;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* نوبتی که از پنل قطعی و پولش ثبت میشود باید در «پرداختها» دیده شود.
|
||||
*
|
||||
* فهرست پرداختها صورتحسابمحور است و صورتحساب فقط برای مراجعهٔ بیمهدار ساخته
|
||||
* میشد؛ نتیجهاش این بود که پرداختِ نوبتِ بدون بیمه هیچجا دیده نمیشد.
|
||||
*/
|
||||
class ConfirmedAppointmentAppearsInPaymentsTest extends ApiTestCase
|
||||
{
|
||||
private function makeDoctor(): Doctor
|
||||
{
|
||||
$user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($user, 'دکتر تست');
|
||||
$doctor->setMobileNumber($user->getMobileNumber());
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
return $doctor;
|
||||
}
|
||||
|
||||
private function makeAppointment(Doctor $doctor, int $visitPriceRials = 5_000_000): Appointment
|
||||
{
|
||||
$start = strtotime('+30 days') + random_int(0, 500_000) * 7;
|
||||
|
||||
$appointment = $this->newAppointment($doctor, $this->createUser(), $start, $start + 900);
|
||||
$appointment->setVisitPriceRials($visitPriceRials);
|
||||
$this->em->persist($appointment);
|
||||
$this->em->flush();
|
||||
|
||||
return $appointment;
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> ردیفهای فهرست پرداختهای همان پزشک */
|
||||
private function payments(Doctor $doctor): array
|
||||
{
|
||||
$body = $this->authJson('GET', '/api/v1/my/billing/payments?limit=100', $doctor->getUser());
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
return $body['data'] ?? [];
|
||||
}
|
||||
|
||||
public function testPartialPaymentOnConfirmShowsUpInThePaymentsList(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor();
|
||||
$appointment = $this->makeAppointment($doctor);
|
||||
|
||||
self::assertSame([], $this->payments($doctor));
|
||||
|
||||
$this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [
|
||||
'version' => $appointment->getVersion(),
|
||||
'payments' => [['method' => 'cash', 'amount_rials' => 1_000_000]],
|
||||
]);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$rows = $this->payments($doctor);
|
||||
self::assertCount(1, $rows);
|
||||
self::assertSame(5_000_000, $rows[0]['amount_rials']);
|
||||
self::assertSame(1_000_000, $rows[0]['paid_rials']);
|
||||
self::assertSame('partial', $rows[0]['status']);
|
||||
}
|
||||
|
||||
public function testFullPaymentOnConfirmIsListedAsPaid(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor();
|
||||
$appointment = $this->makeAppointment($doctor);
|
||||
|
||||
$this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [
|
||||
'version' => $appointment->getVersion(),
|
||||
'payments' => [['method' => 'pos', 'amount_rials' => 5_000_000]],
|
||||
]);
|
||||
|
||||
$rows = $this->payments($doctor);
|
||||
self::assertCount(1, $rows);
|
||||
self::assertSame('paid', $rows[0]['status']);
|
||||
}
|
||||
|
||||
/** صورتحساب باید نهایی باشد، وگرنه فهرست پرداختها آن را فیلتر میکند. */
|
||||
public function testTheGeneratedInvoiceIsFinalized(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor();
|
||||
$appointment = $this->makeAppointment($doctor);
|
||||
|
||||
$res = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [
|
||||
'version' => $appointment->getVersion(),
|
||||
'payments' => [['method' => 'cash', 'amount_rials' => 2_000_000]],
|
||||
]);
|
||||
|
||||
$this->em->clear();
|
||||
$invoices = $this->em->getRepository(Invoice::class)->findBy(['entityId' => $doctor->getId(), 'entityType' => 'doctor']);
|
||||
self::assertCount(1, $invoices);
|
||||
self::assertSame(Invoice::STATUS_FINALIZED, $invoices[0]->getStatus());
|
||||
self::assertSame($res['data']['session']['uuid'] !== null, true);
|
||||
}
|
||||
|
||||
/** مرزی: قطعیکردن بدون پرداخت سندی نمیسازد — همان رفتار قبلی. */
|
||||
public function testConfirmWithoutPaymentCreatesNoInvoice(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor();
|
||||
$appointment = $this->makeAppointment($doctor);
|
||||
|
||||
$this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [
|
||||
'version' => $appointment->getVersion(),
|
||||
]);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
self::assertSame([], $this->payments($doctor));
|
||||
}
|
||||
|
||||
/** خطا: پرداخت بیشتر از مبلغ نوبت رد میشود و صورتحسابی هم نمیماند. */
|
||||
public function testOverpaymentIsRejectedAndLeavesNoInvoice(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor();
|
||||
$appointment = $this->makeAppointment($doctor);
|
||||
|
||||
$this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [
|
||||
'version' => $appointment->getVersion(),
|
||||
'payments' => [['method' => 'cash', 'amount_rials' => 9_000_000]],
|
||||
]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame([], $this->payments($doctor));
|
||||
}
|
||||
|
||||
/** پرداختهای یک پزشک نباید در فهرست پزشک دیگر بیاید. */
|
||||
public function testPaymentsStayInsideTheirTenant(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor();
|
||||
$other = $this->makeDoctor();
|
||||
$appointment = $this->makeAppointment($doctor);
|
||||
|
||||
$this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [
|
||||
'version' => $appointment->getVersion(),
|
||||
'payments' => [['method' => 'cash', 'amount_rials' => 1_000_000]],
|
||||
]);
|
||||
|
||||
self::assertCount(1, $this->payments($doctor));
|
||||
self::assertSame([], $this->payments($other));
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ class ApiLeastPrivilegeTest extends ApiTestCase
|
||||
'app_settlement_settlement_balance' => 'کیف پول خودِ کاربر',
|
||||
'app_settlement_settlement_transactions' => 'تراکنشهای کیف پول خودِ کاربر',
|
||||
'app_settlement_settlement_listmine' => 'تسویههای خودِ کاربر',
|
||||
'app_userprofile_tourprogress_seen' => 'راهنماهای دیدهشدهٔ خودِ کاربر',
|
||||
'app_dashboard_dashboard_secretary' => 'محیط و مجوزهای خودِ منشی — ورودی رندر پنل',
|
||||
// رفتار مستند: بدون مجوز فقط قابلیتهای پلن میآید، نه وضعیت/تاریخ اشتراک.
|
||||
// تستش در SecretaryResourceEnforcementTest::testSubscriptionWithoutPermissionReturnsFeaturesOnly
|
||||
@@ -127,6 +128,7 @@ class ApiLeastPrivilegeTest extends ApiTestCase
|
||||
'app_userprofile_userprofile_create' => 'پروفایل خودِ کاربر',
|
||||
'app_userprofile_userprofile_update' => 'پروفایل خودِ کاربر',
|
||||
'app_userprofile_userprofile_uploadavatar' => 'آواتار خودِ کاربر',
|
||||
'app_userprofile_tourprogress_markseen' => 'ثبت دیدهشدن راهنما برای خودِ کاربر',
|
||||
'app_settlement_settlement_request' => 'تسویهٔ کیفپول خودِ کاربر',
|
||||
'app_secretary_secretary_addiban' => 'شبای خودِ منشی',
|
||||
'app_secretary_secretary_removeiban' => 'شبای خودِ منشی',
|
||||
|
||||
Reference in New Issue
Block a user