Files
clinicpro/tests/Secretary/SecretaryShareApiTest.php
T
hamed 8d2b0d908a feat: Add online share functionality for secretaries
- Introduced `online_share_enabled` and `online_share_percent` fields in the `doctor_secretaries` table to manage secretary shares from online appointments.
- Added `bank_account` field in the `profiles` table to store user-level IBANs for settlements.
- Created `secretary_earnings` table to track earnings per secretary from online appointments, including a foreign key relationship with `financial_breakdowns`.
- Implemented `SecretaryEarning` entity and repository for managing secretary earnings.
- Developed `SecretaryShareResolver` service to determine which secretaries earn from online payments.
- Added `UserIbanResolver` service to handle user IBAN retrieval and management.
- Created `HasIbansTrait` for entities to manage IBANs in a JSON format.
- Implemented tests for secretary earnings and API endpoints for managing secretary shares and IBANs.
2026-07-25 18:34:18 +03:30

208 lines
8.1 KiB
PHP

<?php
namespace App\Tests\Secretary;
use App\Auth\Entity\User;
use App\Doctor\Entity\Doctor;
use App\Secretary\Entity\DoctorSecretary;
use App\Tests\ApiTestCase;
/**
* اندپوینت‌های مدیریت سهم منشی (ادمین) و گزارش/شبا (پنل منشی).
*/
class SecretaryShareApiTest extends ApiTestCase
{
/** @return array{0: DoctorSecretary, 1: User} */
private function makeRelation(): array
{
$doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
$doctor = new Doctor($doctorUser, 'دکتر تست API سهم');
$this->em->persist($doctor);
$secretaryUser = $this->createUser(['ROLE_USER', 'ROLE_SECRETARY']);
$relation = new DoctorSecretary($doctor, $secretaryUser);
$this->em->persist($relation);
$this->em->flush();
return [$relation, $secretaryUser];
}
// ── ادمین ────────────────────────────────────────────────────────────────
public function testAdminSeesTheRelationDetailWithEarnings(): void
{
[$relation] = $this->makeRelation();
$admin = $this->createUser(['ROLE_ADMIN']);
$body = $this->authJson('GET', '/api/v1/admin/secretary/' . $relation->getUuid(), $admin);
self::assertSame(200, $this->responseCode());
$row = $body['data']['data'];
self::assertFalse($row['online_share_enabled']);
// JSON عددِ گِرد را بدون ممیز می‌دهد، پس مقایسه‌ی نوع‌آزاد.
self::assertEquals(0, $row['online_share_percent']);
self::assertSame(0, $row['earnings']['total_rials']);
}
public function testAdminEnablesTheShareWithAPercent(): void
{
[$relation] = $this->makeRelation();
$admin = $this->createUser(['ROLE_ADMIN']);
$body = $this->authJson('PUT', '/api/v1/admin/secretary/' . $relation->getUuid() . '/online-share', $admin, [
'enabled' => true,
'percent' => 5,
]);
self::assertSame(200, $this->responseCode());
self::assertTrue($body['data']['data']['online_share_enabled']);
self::assertEquals(5, $body['data']['data']['online_share_percent']);
}
public function testPercentAbove100IsRejected(): void
{
[$relation] = $this->makeRelation();
$admin = $this->createUser(['ROLE_ADMIN']);
$this->authJson('PUT', '/api/v1/admin/secretary/' . $relation->getUuid() . '/online-share', $admin, [
'enabled' => true, 'percent' => 101,
]);
self::assertSame(422, $this->responseCode());
}
public function testEnablingWithoutAPercentIsRejected(): void
{
[$relation] = $this->makeRelation();
$admin = $this->createUser(['ROLE_ADMIN']);
$this->authJson('PUT', '/api/v1/admin/secretary/' . $relation->getUuid() . '/online-share', $admin, [
'enabled' => true, 'percent' => 0,
]);
self::assertSame(422, $this->responseCode());
}
public function testNonAdminIsForbidden(): void
{
[$relation, $secretaryUser] = $this->makeRelation();
$this->authJson('GET', '/api/v1/admin/secretary/' . $relation->getUuid(), $secretaryUser);
self::assertSame(403, $this->responseCode());
}
public function testUnknownRelationIsNotFound(): void
{
$admin = $this->createUser(['ROLE_ADMIN']);
$this->authJson('GET', '/api/v1/admin/secretary/00000000-0000-4000-8000-000000000000', $admin);
self::assertSame(404, $this->responseCode());
}
// ── پنل منشی ─────────────────────────────────────────────────────────────
public function testSummaryReportsDisabledWhenNoRelationHasAShare(): void
{
[, $secretaryUser] = $this->makeRelation();
$body = $this->authJson('GET', '/api/v1/secretary/earnings/summary', $secretaryUser);
self::assertSame(200, $this->responseCode());
self::assertFalse($body['data']['data']['enabled']);
self::assertSame(0, $body['data']['data']['total_rials']);
}
public function testSummaryReportsEnabledWithThePercentOnceAdminTurnsItOn(): void
{
[$relation, $secretaryUser] = $this->makeRelation();
$relation->setOnlineShareEnabled(true)->setOnlineSharePercent(7.5);
$this->em->flush();
$body = $this->authJson('GET', '/api/v1/secretary/earnings/summary', $secretaryUser);
self::assertSame(200, $this->responseCode());
self::assertTrue($body['data']['data']['enabled']);
self::assertSame(7.5, $body['data']['data']['share_percent']);
}
public function testReportIsEmptyButSucceedsWithoutEarnings(): void
{
[, $secretaryUser] = $this->makeRelation();
$body = $this->authJson('GET', '/api/v1/secretary/earnings/report', $secretaryUser);
self::assertSame(200, $this->responseCode());
self::assertSame([], $body['data']);
self::assertSame(0, $body['meta']['totalRecords']);
}
public function testSecretaryManagesItsIbans(): void
{
[, $secretaryUser] = $this->makeRelation();
$added = $this->authJson('POST', '/api/v1/secretary/iban', $secretaryUser, [
'iban' => 'IR' . str_pad((string) random_int(0, 999_999), 24, '0', STR_PAD_LEFT),
'bank_name' => 'ملی',
]);
self::assertSame(201, $this->responseCode());
$ibans = $added['data']['data']['bank_account'];
self::assertCount(1, $ibans);
// شبای تازه تأییدنشده است؛ تسویه با آن مجاز نیست.
self::assertFalse($ibans[0]['verified']);
$removed = $this->authJson('DELETE', '/api/v1/secretary/iban/' . $ibans[0]['id'], $secretaryUser);
self::assertSame(200, $this->responseCode());
self::assertSame([], $removed['data']['data']['bank_account']);
}
public function testInvalidIbanIsRejected(): void
{
[, $secretaryUser] = $this->makeRelation();
$this->authJson('POST', '/api/v1/secretary/iban', $secretaryUser, ['iban' => '12345']);
self::assertSame(422, $this->responseCode());
}
public function testThirdIbanIsRejected(): void
{
[, $secretaryUser] = $this->makeRelation();
$iban = fn() => 'IR' . str_pad((string) random_int(0, 999_999_999), 24, '0', STR_PAD_LEFT);
$this->authJson('POST', '/api/v1/secretary/iban', $secretaryUser, ['iban' => $iban()]);
$this->authJson('POST', '/api/v1/secretary/iban', $secretaryUser, ['iban' => $iban()]);
$this->authJson('POST', '/api/v1/secretary/iban', $secretaryUser, ['iban' => $iban()]);
self::assertSame(422, $this->responseCode());
}
public function testSettlementAcceptsAVerifiedSecretaryIban(): void
{
[, $secretaryUser] = $this->makeRelation();
$added = $this->authJson('POST', '/api/v1/secretary/iban', $secretaryUser, [
'iban' => 'IR' . str_pad((string) random_int(0, 999_999), 24, '0', STR_PAD_LEFT),
]);
$ibanId = $added['data']['data']['bank_account'][0]['id'];
// تأیید شبا کارِ ادمین است؛ اینجا مستقیم روی پروفایل ست می‌کنیم.
$profile = static::getContainer()->get(\App\UserProfile\Repository\UserProfileRepository::class)
->findByUser($secretaryUser);
$ibans = $profile->getIbans();
$ibans[0]['verified'] = true;
$profile->setBankAccount($ibans);
$this->em->flush();
// بدون موجودی: خطای موجودی می‌گیرد، نه خطای شبا — یعنی شبا پذیرفته شده است.
$body = $this->authJson('POST', '/api/v1/settlement', $secretaryUser, [
'amount_rials' => 100_000,
'iban_id' => $ibanId,
]);
self::assertSame(422, $this->responseCode());
self::assertSame('موجودی کافی نیست', $body['errors'][0]['message']);
}
}