feat(invoice): implement recorder identity resolution for payments and update related tests

This commit is contained in:
hamed
2026-08-04 11:19:07 +03:30
parent 2c7b86d917
commit 810e9351a9
7 changed files with 321 additions and 13 deletions
+34
View File
@@ -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 -10
View File
@@ -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';
}
}