feat: add optional inventory package association to service items and implement audit logging

- Added `inventory_package_uuid` and `inventory_package_title` fields to the `ServiceItem` interface.
- Updated API documentation to reflect new fields in service item responses.
- Implemented methods in `ClinicServiceController` to handle inventory package associations.
- Created `ServiceItemAuditLog` entity and repository for tracking changes to service items.
- Added functionality to log changes to service items, including inventory package associations.
- Implemented tests for attaching/detaching inventory packages and auditing changes.
- Created database migrations for new fields and audit log table.
This commit is contained in:
hamed
2026-07-18 12:26:15 +03:30
parent 42d9ad26c5
commit c4a661b542
14 changed files with 885 additions and 24 deletions
@@ -38,6 +38,10 @@ const mockApi = (item: unknown = ITEM, notFound = false) => {
effective_plan: { name: 'professional', level: 2, max_secretaries: 10, features: { services: true } },
} });
if (url.includes('/payment/config')) return Promise.resolve({ success: true, data: { test_mode: true, gateways: [] } });
if (url.includes('/audit-logs')) return Promise.resolve({ success: true, data: [
{ uuid: 'l1', field: 'price_rials', operation: 'update', old_value: '7000000', new_value: '8500000', actor_name: 'دکتر تست', created_at: 1_800_000_000 },
{ uuid: 'l2', field: 'name', operation: 'create', old_value: null, new_value: 'سرم ۵۰۰cc', actor_name: 'دکتر تست', created_at: 1_700_000_000 },
] });
if (url.includes('/service-item/')) {
return notFound ? Promise.reject(new Error('یافت نشد')) : Promise.resolve({ success: true, data: item });
}
@@ -51,6 +55,12 @@ const mockApi = (item: unknown = ITEM, notFound = false) => {
if (url.includes('/tenant-insurances')) return Promise.resolve({ data: { data: [
{ uuid: 'ins1', insurance_name: 'بیمه ایران', insurance_kind: 'basic', coverage_percent: 70 },
] } });
if (url.includes('/inventory-packages')) return Promise.resolve({ success: true, data: [
{
uuid: 'pkg1', title: 'پکیج سرم', total: 1_200_000, available: true,
items: [{ itemUuid: 'i1', name: 'سرم رینگر', unit: 'عدد', price: 600_000, amount: 2 }],
},
] });
return Promise.resolve({ success: true, data: [] });
});
};
@@ -125,6 +135,35 @@ describe('ServiceDetailPage (جزئیات سرویس)', () => {
expect(screen.queryByText('این خدمت شامل بیمه می‌شود')).not.toBeInTheDocument();
});
it('تب کالاهای مرتبط، اقلام پکیج متصل را نشان می‌دهد', async () => {
mockApi({ ...ITEM, inventory_package_uuid: 'pkg1', inventory_package_title: 'پکیج سرم' });
render();
fireEvent.click(await screen.findByText('کالاهای مرتبط'));
expect(await screen.findByText('پکیج سرم')).toBeInTheDocument();
expect(screen.getByText('سرم رینگر')).toBeInTheDocument();
expect(screen.getByText('۲ عدد')).toBeInTheDocument();
});
it('سرویس بدون پکیج، حالت خالی با لینک انبار می‌دهد', async () => {
render();
fireEvent.click(await screen.findByText('کالاهای مرتبط'));
expect(await screen.findByText('پکیج کالایی به این سرویس متصل نیست')).toBeInTheDocument();
expect(screen.getByText('مدیریت انبار')).toBeInTheDocument();
});
it('تب لاگ تغییرات، مقدار قبل و بعد را خوانا نشان می‌دهد', async () => {
render();
fireEvent.click(await screen.findByText('لاگ تغییرات'));
expect(await screen.findByText('قیمت پایه')).toBeInTheDocument();
expect(screen.getByText('۷۰۰٬۰۰۰ تومان')).toBeInTheDocument();
expect(screen.getByText('۸۵۰٬۰۰۰ تومان')).toBeInTheDocument();
expect(screen.getByText('ایجاد')).toBeInTheDocument();
expect(screen.getAllByText(/توسط دکتر تست/).length).toBeGreaterThan(0);
});
it('سرویس غیرفعال نشان «غیرفعال» و دکمه فعال‌کردن دارد', async () => {
mockApi({ ...ITEM, active: false });
render();