- 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.
208 lines
8.1 KiB
PHP
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']);
|
|
}
|
|
}
|