feat(invoice): implement recorder identity resolution for payments and update related tests
This commit is contained in:
@@ -8,6 +8,7 @@ vi.mock('../lib/api', () => ({
|
||||
}));
|
||||
|
||||
import { api } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import InvoiceSummaryModal from './InvoiceSummaryModal';
|
||||
|
||||
const get = api.get as ReturnType<typeof vi.fn>;
|
||||
@@ -121,6 +122,61 @@ describe('InvoiceSummaryModal', () => {
|
||||
expect(screen.getByText('بیمه ایران')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/**
|
||||
* ستون «ثبتکننده»: نامِ حلشدهٔ سرور، و لینک فقط وقتی بیننده صفحهٔ آن پروفایل را
|
||||
* میتواند باز کند. عکسِ لحظهٔ ثبت (`created_by_name`) فقط fallback است.
|
||||
*/
|
||||
describe('ثبتکنندهٔ پرداخت', () => {
|
||||
const withRecorder = (recorder: object | null) => ({
|
||||
...baseInvoice,
|
||||
session: {
|
||||
...fullSession,
|
||||
payments: [{
|
||||
uuid: 'p1', method: 'wallet', amount_rials: 1_500_000, paid_at: 1700100000,
|
||||
created_by_name: '09120000000', created_by: recorder,
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
it('پزشکِ ثبتکننده به پروفایل پزشک لینک میشود', async () => {
|
||||
useAuthStore.setState({ primaryRole: 'clinic' } as any);
|
||||
mockInvoice(withRecorder({ user_uuid: 'u1', name: 'دکتر رضایی', role: 'doctor', doctor_uuid: 'doc-9' }));
|
||||
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
|
||||
|
||||
const link = await screen.findByRole('link', { name: 'دکتر رضایی' });
|
||||
expect(link).toHaveAttribute('href', '/admin/doctors/doc-9');
|
||||
// نامِ زنده جای شمارهٔ ذخیرهشده مینشیند.
|
||||
expect(screen.queryByText('09120000000')).toBeNull();
|
||||
});
|
||||
|
||||
it('منشیِ ثبتکننده برای کلینیک به فهرست منشیهای خودش لینک میشود', async () => {
|
||||
useAuthStore.setState({ primaryRole: 'clinic' } as any);
|
||||
mockInvoice(withRecorder({ user_uuid: 'u2', name: 'منشی مدیسا', role: 'secretary', doctor_uuid: null }));
|
||||
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
|
||||
|
||||
const link = await screen.findByRole('link', { name: 'منشی مدیسا' });
|
||||
expect(link).toHaveAttribute('href', '/admin/my-secretaries');
|
||||
});
|
||||
|
||||
it('بینندهای که آن صفحه را ندارد، فقط نام میبیند نه لینک', async () => {
|
||||
useAuthStore.setState({ primaryRole: 'secretary' } as any);
|
||||
mockInvoice(withRecorder({ user_uuid: 'u2', name: 'منشی مدیسا', role: 'secretary', doctor_uuid: null }));
|
||||
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
|
||||
|
||||
expect(await screen.findByText('منشی مدیسا')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: 'منشی مدیسا' })).toBeNull();
|
||||
});
|
||||
|
||||
it('بدون created_by (کاربر حذفشده): همان عکسِ ذخیرهشده، بدون لینک', async () => {
|
||||
useAuthStore.setState({ primaryRole: 'clinic' } as any);
|
||||
mockInvoice(withRecorder(null));
|
||||
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
|
||||
|
||||
expect(await screen.findByText('09120000000')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: '09120000000' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('فاکتور بدون بیمه، جدول بیمه ندارد', async () => {
|
||||
mockInvoice(baseInvoice);
|
||||
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api } from '../lib/api';
|
||||
import type { ApiResponse } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import Modal from './ui/Modal';
|
||||
import { formatDate, formatDateTime, formatRial } from '../lib/utils';
|
||||
import { METHOD_LABELS } from './session/PaymentStep';
|
||||
|
||||
interface InvoiceItem { uuid: string; title: string; quantity: number; total_rials: number; patient_rials: number }
|
||||
interface SessionPayment { uuid: string; method: string; amount_rials: number; paid_at: number; created_by_name: string | null }
|
||||
/** ثبتکنندهٔ پرداخت — نامش زنده از خودِ کاربر حل میشود، نه از عکسِ لحظهٔ ثبت. */
|
||||
interface PaymentRecorder {
|
||||
user_uuid: string;
|
||||
name: string | null;
|
||||
role: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'staff' | 'representation' | 'user';
|
||||
doctor_uuid: string | null;
|
||||
}
|
||||
interface SessionPayment {
|
||||
uuid: string; method: string; amount_rials: number; paid_at: number;
|
||||
created_by_name: string | null;
|
||||
created_by?: PaymentRecorder | null;
|
||||
}
|
||||
interface SessionConsumable { uuid: string; item_name: string; quantity: number; line_total_rials: number }
|
||||
interface SessionData {
|
||||
session_at: number | null; paid_at: number | null;
|
||||
@@ -60,8 +73,47 @@ function SectionTable({ title, cols, rows }: { title: string; cols: string[]; ro
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* پروفایلِ ثبتکننده در پنلِ همین بیننده — یا `null` وقتی صفحهای برایش وجود ندارد.
|
||||
*
|
||||
* فقط پزشک صفحهٔ پروفایلِ مستقل دارد؛ منشی و پرسنل صفحهٔ فهرستِ مدیریتشان را دارند
|
||||
* و ادمین فهرستِ خودش را. مسیری که نقشِ بیننده اجازهاش را ندارد لینک نمیشود، وگرنه
|
||||
* کلیک به داشبورد پرت میکرد.
|
||||
*/
|
||||
function recorderProfilePath(recorder: PaymentRecorder, viewerRole: string | null): string | null {
|
||||
if (recorder.role === 'doctor' && recorder.doctor_uuid) {
|
||||
return ['admin', 'doctor', 'clinic', 'representation'].includes(viewerRole ?? '')
|
||||
? `/admin/doctors/${recorder.doctor_uuid}`
|
||||
: null;
|
||||
}
|
||||
if (recorder.role === 'secretary') {
|
||||
if (viewerRole === 'admin') return '/admin/secretaries';
|
||||
return viewerRole === 'clinic' || viewerRole === 'doctor' ? '/admin/my-secretaries' : null;
|
||||
}
|
||||
if (recorder.role === 'staff') {
|
||||
return ['clinic', 'doctor', 'secretary'].includes(viewerRole ?? '') ? '/admin/staff' : null;
|
||||
}
|
||||
if (viewerRole === 'admin') return `/admin/users/${recorder.user_uuid}`;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** سلولِ «ثبتکننده»: نام، و اگر پروفایلی در دسترسِ بیننده باشد، لینکش. */
|
||||
function RecorderCell({ payment, viewerRole }: { payment: SessionPayment; viewerRole: string | null }) {
|
||||
const recorder = payment.created_by ?? null;
|
||||
const name = recorder?.name ?? payment.created_by_name;
|
||||
if (!name) return <>-</>;
|
||||
|
||||
const path = recorder ? recorderProfilePath(recorder, viewerRole) : null;
|
||||
|
||||
return path
|
||||
? <Link to={path} style={{ color: 'var(--primary)', textDecoration: 'underline' }}>{name}</Link>
|
||||
: <>{name}</>;
|
||||
}
|
||||
|
||||
/** خلاصه فاکتور — invoice summary, ported pixel-for-pixel from tauri InvoiceSummary. */
|
||||
export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceUuid: string | null; onClose: () => void }) {
|
||||
const viewerRole = useAuthStore(s => s.primaryRole);
|
||||
const { data, isLoading } = useQuery<ApiResponse<any>>({
|
||||
queryKey: ['invoice', invoiceUuid],
|
||||
queryFn: () => api.get(`/api/v1/billing/invoices/${invoiceUuid}`),
|
||||
@@ -156,7 +208,7 @@ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceU
|
||||
METHOD_LABELS[p.method] ?? p.method,
|
||||
formatRial(p.amount_rials),
|
||||
p.paid_at ? formatDateTime(p.paid_at) : '-',
|
||||
p.created_by_name ?? '-',
|
||||
<RecorderCell payment={p} viewerRole={viewerRole} />,
|
||||
]),
|
||||
['', <span style={{ fontWeight: 700 }}>مجموع پرداختیها</span>, <span style={{ fontWeight: 700 }}>{formatRial(session.paid_total_rials)}</span>, '', ''],
|
||||
]
|
||||
|
||||
+24
-1
@@ -130,7 +130,12 @@ ddev exec php bin/console app:billing:backfill-claims # ساخت م
|
||||
"final_price_rials": 2240000,
|
||||
"paid_total_rials": 1500000,
|
||||
"payments": [
|
||||
{ "uuid": "…", "method": "wallet", "amount_rials": 1500000, "paid_at": 1718990000, "created_by_name": "منشی تست", "created_at": 1718990000 }
|
||||
{
|
||||
"uuid": "…", "method": "wallet", "amount_rials": 1500000,
|
||||
"paid_at": 1718990000, "created_at": 1718990000,
|
||||
"created_by_name": "منشی تست",
|
||||
"created_by": { "user_uuid": "…", "name": "منشی تست", "role": "secretary", "doctor_uuid": null }
|
||||
}
|
||||
],
|
||||
"consumables": [
|
||||
{ "uuid": "…", "item_name": "عینک", "quantity": 2, "price_rials": 20000, "line_total_rials": 40000 }
|
||||
@@ -141,6 +146,24 @@ ddev exec php bin/console app:billing:backfill-claims # ساخت م
|
||||
}
|
||||
```
|
||||
|
||||
### ثبتکنندهٔ هر پرداخت (2026-08)
|
||||
|
||||
`created_by` هویتِ کاربری است که پرداخت را ثبت کرده، برای نمایش نام و لینکدادن به
|
||||
پروفایلش:
|
||||
|
||||
| فیلد | توضیح |
|
||||
|---|---|
|
||||
| `user_uuid` | uuid کاربر |
|
||||
| `name` | نامِ نمایشی — پروفایل ← `real_name` ← شمارهٔ موبایل |
|
||||
| `role` | نقشِ پرتوان کاربر: `admin\|clinic\|doctor\|secretary\|staff\|representation\|user` |
|
||||
| `doctor_uuid` | فقط برای نقش `doctor` — تنها نقشی که در پنل صفحهٔ پروفایل مستقل دارد |
|
||||
|
||||
- `created_by_name` روی ردیف **عکسِ لحظهٔ ثبت** است و برای کاربری که آنوقت پروفایل
|
||||
نداشته ممکن است شمارهٔ موبایل باشد؛ در این پاسخ با نامِ زندهٔ همان کاربر بازنویسی
|
||||
میشود و فقط وقتی دستنخورده میماند که `created_by` تهی باشد (کاربر حذف شده).
|
||||
- مسیرِ پروفایل عمداً در پاسخ نیست: سرور مسیرهای پنل را نمیشناسد و دسترسیِ هر نقش
|
||||
به هر صفحه کارِ کلاینت است.
|
||||
|
||||
**Errors:** `404 ERR_NOT_FOUND_001`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,72 @@ class InvoiceShowSessionTest extends ApiTestCase
|
||||
self::assertSame(40_000, $s['consumables_total_rials']);
|
||||
}
|
||||
|
||||
/**
|
||||
* ستون «ثبتکننده» باید نامِ زندهٔ کاربر و شناسههای لینکش را بدهد؛ عکسِ
|
||||
* `created_by_name` ممکن است شمارهٔ موبایلِ زمانی باشد که کاربر پروفایل نداشته.
|
||||
*/
|
||||
public function testShowResolvesRecorderIdentityFromUser(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$record = $this->patientRecord($doctor);
|
||||
|
||||
$recorderDoctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر ثبتکننده');
|
||||
$this->em->persist($recorderDoctor);
|
||||
$recorder = $recorderDoctor->getUser();
|
||||
$recorder->setRealName('دکتر ثبتکننده');
|
||||
|
||||
$session = new PatientSession($record);
|
||||
$this->em->persist($session);
|
||||
$this->em->flush();
|
||||
|
||||
$payment = new SessionPayment($session, 'cash', 500_000, 1_700_100_000);
|
||||
// عکسِ کهنه: همان چیزی که هنگام ثبت ذخیره شده بود.
|
||||
$payment->setCreatedByName($recorder->getMobileNumber())->setCreatedBy($recorder);
|
||||
$this->em->persist($payment);
|
||||
$session->addPayment($payment);
|
||||
|
||||
$invoice = new Invoice('doctor', $doctor->getId());
|
||||
$invoice->setPatientSessionId($session->getId());
|
||||
$this->em->persist($invoice);
|
||||
$this->em->flush();
|
||||
|
||||
$res = $this->authJson('GET', '/api/v1/billing/invoices/' . $invoice->getUuid(), $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$row = $res['data']['data']['session']['payments'][0];
|
||||
self::assertSame('دکتر ثبتکننده', $row['created_by_name']);
|
||||
self::assertSame($recorder->getUuid(), $row['created_by']['user_uuid']);
|
||||
self::assertSame('doctor', $row['created_by']['role']);
|
||||
self::assertSame($recorderDoctor->getUuid(), $row['created_by']['doctor_uuid']);
|
||||
}
|
||||
|
||||
/** کاربرِ حذفشده: هویتی نیست، پس همان عکسِ ذخیرهشده میماند. */
|
||||
public function testShowKeepsSnapshotNameWhenRecorderUnknown(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$record = $this->patientRecord($doctor);
|
||||
|
||||
$session = new PatientSession($record);
|
||||
$this->em->persist($session);
|
||||
$this->em->flush();
|
||||
|
||||
$payment = new SessionPayment($session, 'cash', 100_000, 1_700_100_000);
|
||||
$payment->setCreatedByName('منشی قدیمی');
|
||||
$this->em->persist($payment);
|
||||
$session->addPayment($payment);
|
||||
|
||||
$invoice = new Invoice('doctor', $doctor->getId());
|
||||
$invoice->setPatientSessionId($session->getId());
|
||||
$this->em->persist($invoice);
|
||||
$this->em->flush();
|
||||
|
||||
$res = $this->authJson('GET', '/api/v1/billing/invoices/' . $invoice->getUuid(), $owner);
|
||||
|
||||
$row = $res['data']['data']['session']['payments'][0];
|
||||
self::assertSame('منشی قدیمی', $row['created_by_name']);
|
||||
self::assertNull($row['created_by']);
|
||||
}
|
||||
|
||||
public function testShowSessionNullWhenInvoiceHasNoSession(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
|
||||
Reference in New Issue
Block a user