feat(invoice): implement recorder identity resolution for payments and update related tests
This commit is contained in:
@@ -12,6 +12,7 @@ use App\Insurance\Repository\InsuranceRepository;
|
||||
use App\Insurance\Service\TenantInsuranceService;
|
||||
use App\Patient\Entity\PatientSession;
|
||||
use App\Patient\Repository\PatientSessionRepository;
|
||||
use App\Shared\Service\ActorIdentityResolver;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class InvoiceService
|
||||
@@ -23,6 +24,7 @@ class InvoiceService
|
||||
private readonly PatientSessionRepository $sessionRepo,
|
||||
private readonly InsuranceRepository $insuranceRepo,
|
||||
private readonly EventDispatcherInterface $events,
|
||||
private readonly ActorIdentityResolver $actorIdentity,
|
||||
) {}
|
||||
|
||||
/** @var array<int, string|null> نام بیمهها، یکبار در هر درخواست. */
|
||||
@@ -112,6 +114,10 @@ class InvoiceService
|
||||
$session = $sessionId !== null ? $this->sessionRepo->find($sessionId) : null;
|
||||
$data['session'] = $session?->toArray();
|
||||
|
||||
if ($session !== null) {
|
||||
$data['session']['payments'] = $this->withRecorderIdentity($session, $data['session']['payments'] ?? []);
|
||||
}
|
||||
|
||||
// نام بیمهها برای چاپ روی فاکتور؛ فاکتور فقط شناسه را نگه میدارد.
|
||||
$data['base_insurance_name'] = $this->insuranceName($invoice->getBaseInsuranceId());
|
||||
$data['supplementary_insurance_name'] = $this->insuranceName($invoice->getSupplementaryInsuranceId());
|
||||
@@ -119,6 +125,34 @@ class InvoiceService
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* ستون «ثبتکننده»ی خلاصهٔ فاکتور: هویتِ کاربرِ ثبتکننده کنار هر پرداخت.
|
||||
*
|
||||
* `created_by_name` روی ردیف، عکسِ لحظهٔ ثبت است و برای کاربری که آنوقت پروفایل
|
||||
* نداشته شمارهٔ موبایل ذخیره شده؛ پس نام از خودِ کاربر دوباره حل میشود و عکس فقط
|
||||
* برای کاربرِ حذفشده میماند. `created_by` هم شناسهها را میدهد تا کلاینت بتواند
|
||||
* به پروفایلش لینک کند.
|
||||
*
|
||||
* @param list<array<string, mixed>> $rows
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function withRecorderIdentity(PatientSession $session, array $rows): array
|
||||
{
|
||||
$actorByUuid = [];
|
||||
foreach ($session->getPayments() as $payment) {
|
||||
$actorByUuid[$payment->getUuid()] = $payment->getCreatedBy();
|
||||
}
|
||||
|
||||
return array_map(function (array $row) use ($actorByUuid): array {
|
||||
$identity = $this->actorIdentity->identity($actorByUuid[$row['uuid'] ?? ''] ?? null);
|
||||
|
||||
$row['created_by'] = $identity;
|
||||
$row['created_by_name'] = $identity['name'] ?? ($row['created_by_name'] ?? null);
|
||||
|
||||
return $row;
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated flat list of a tenant's recorded (finalized/paid) invoices for
|
||||
* the payments list (node 1). Rows arrive ready-shaped from the repository;
|
||||
|
||||
@@ -8,7 +8,7 @@ use App\Settlement\Repository\SettlementRepository;
|
||||
use App\Settlement\Repository\WalletTransactionRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Exception\AppException;
|
||||
use App\UserProfile\Repository\UserProfileRepository;
|
||||
use App\Shared\Service\ActorIdentityResolver;
|
||||
|
||||
/**
|
||||
* منطق کیف پولِ بیمار: موجودی، شارژ (credit)، برداشت (debit) و تسویهٔ سرویس از
|
||||
@@ -24,7 +24,7 @@ class WalletService
|
||||
public function __construct(
|
||||
private readonly WalletTransactionRepository $walletRepo,
|
||||
private readonly SettlementRepository $settlementRepo,
|
||||
private readonly UserProfileRepository $profileRepo,
|
||||
private readonly ActorIdentityResolver $actorIdentity,
|
||||
) {}
|
||||
|
||||
public function balance(User $patient): int
|
||||
@@ -32,16 +32,14 @@ class WalletService
|
||||
return $this->settlementRepo->getWalletBalance($patient);
|
||||
}
|
||||
|
||||
/** نامِ نمایشیِ کاربرِ عامل (برای ستون «ثبتکننده»)؛ در نبودِ پروفایل، شماره موبایل. */
|
||||
/**
|
||||
* نامِ نمایشیِ کاربرِ عامل (برای ستون «ثبتکننده»).
|
||||
* پیادهسازی در {@see ActorIdentityResolver} است تا نامِ ذخیرهشده و نامِ
|
||||
* نمایشدادهشده از یک قاعده بیایند.
|
||||
*/
|
||||
public function resolveActorName(?User $actor): ?string
|
||||
{
|
||||
if ($actor === null) {
|
||||
return null;
|
||||
}
|
||||
$profile = $this->profileRepo->findByUser($actor);
|
||||
$name = trim(($profile?->getLabel() ?? '') . ' ' . ($profile?->getFamily() ?? ''));
|
||||
|
||||
return $name !== '' ? $name : $actor->getMobileNumber();
|
||||
return $this->actorIdentity->name($actor);
|
||||
}
|
||||
|
||||
/** شارژِ کیف پول (credit). فرض بر مثبت بودنِ مبلغ است (اعتبارسنجی در Controller). */
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Shared\Service;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\UserProfile\Repository\UserProfileRepository;
|
||||
|
||||
/**
|
||||
* «چه کسی این را ثبت کرد» — نامِ نمایشی و هویتِ کاربرِ عامل، یک جا.
|
||||
*
|
||||
* ستونهای `created_by_name` عکسِ لحظهٔ ثبتاند و ممکن است شمارهٔ موبایل باشند (وقتی
|
||||
* کاربر هنوز پروفایل نداشته). برای نمایش، نام از خودِ کاربر دوباره حل میشود تا
|
||||
* فاکتورِ دیروز هم نامِ امروز را نشان دهد؛ عکسِ ذخیرهشده فقط وقتی میماند که کاربر
|
||||
* حذف شده باشد.
|
||||
*/
|
||||
class ActorIdentityResolver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserProfileRepository $profileRepo,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
) {}
|
||||
|
||||
/** نامِ نمایشی: پروفایل ← نام واقعیِ حساب ← شمارهٔ موبایل. */
|
||||
public function name(?User $actor): ?string
|
||||
{
|
||||
if ($actor === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$profile = $this->profileRepo->findByUser($actor);
|
||||
$name = trim(($profile?->getLabel() ?? '') . ' ' . ($profile?->getFamily() ?? ''));
|
||||
if ($name !== '') {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$realName = trim((string) $actor->getRealName());
|
||||
|
||||
return $realName !== '' ? $realName : $actor->getMobileNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* هویتِ کاملِ عامل برای نمایش و لینکدادن به پروفایلش.
|
||||
*
|
||||
* `doctor_uuid` فقط برای پزشک پر میشود — تنها نقشی که در پنل صفحهٔ پروفایلِ
|
||||
* مستقل دارد. مسیرِ لینک کارِ کلاینت است، نه سرور: سرور مسیرهای پنل را نمیداند.
|
||||
*
|
||||
* @return array{user_uuid:string,name:?string,role:string,doctor_uuid:?string}|null
|
||||
*/
|
||||
public function identity(?User $actor): ?array
|
||||
{
|
||||
if ($actor === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$role = $this->primaryRole($actor);
|
||||
|
||||
return [
|
||||
'user_uuid' => $actor->getUuid(),
|
||||
'name' => $this->name($actor),
|
||||
'role' => $role,
|
||||
'doctor_uuid' => $role === 'doctor' ? $this->doctorRepo->findByUser($actor)?->getUuid() : null,
|
||||
];
|
||||
}
|
||||
|
||||
/** همان اولویتِ نقشِ `oauth/userinfo` — پرتوانترین نقش برنده است. */
|
||||
private function primaryRole(User $actor): string
|
||||
{
|
||||
$roles = $actor->getRoles();
|
||||
if (in_array('ROLE_ADMIN', $roles, true)) return 'admin';
|
||||
if (in_array('ROLE_CLINIC', $roles, true)) return 'clinic';
|
||||
if (in_array('ROLE_DOCTOR', $roles, true)) return 'doctor';
|
||||
if (in_array('ROLE_SECRETARY', $roles, true)) return 'secretary';
|
||||
if (in_array('ROLE_STAFF', $roles, true)) return 'staff';
|
||||
if (in_array('ROLE_REPRESENTATION', $roles, true)) return 'representation';
|
||||
|
||||
return 'user';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user