refactor(pricing): make the service the only price source

Price lists, annual tariffs and per-branch price overrides each answered
"what does this service cost?" differently, so a single date could carry
several answers and nobody could say which one was right. Price now lives
only on ServiceItem.price_rials, edited from the services page.

- drop PriceList/PriceListItem, their repositories and the seven
  /api/v1/price-list(s) endpoints; PricingController keeps only quote and
  the appointment price snapshot
- drop Tariff, TariffRepository, TariffService and the two
  /service-items/{uuid}/tariffs endpoints; creating or repricing a service
  no longer upserts a current-year tariff
- drop price_rials from ServiceBranchOverride; the entity stays for its
  duration columns, which DurationCalculator and ServiceSelectionValidator
  still read
- InvoiceService reads the item price directly
- PricingEngine collapses to a single source; breakdown.sources always
  reports service_item, keeping the response contract intact
- remove the price-lists admin page, its route and settings-menu entry, the
  tariff modal and the service detail tariffs tab; useAppointmentInvoice
  moves to its own hook file

Migration drops price_lists, price_list_items, service_tariffs and the
override price column.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-02 18:00:48 +03:30
co-authored by Claude Opus 5
parent f06efe26c0
commit 4fe0c4f9bf
41 changed files with 208 additions and 1835 deletions
+6 -14
View File
@@ -45,13 +45,6 @@ const mockApi = (item: unknown = ITEM, notFound = false) => {
if (url.includes('/service-item/')) {
return notFound ? Promise.reject(new Error('یافت نشد')) : Promise.resolve({ success: true, data: item });
}
if (url.includes('/tariffs')) return Promise.resolve({ success: true, data: {
current_year: 1404, default_price_rials: 8_500_000,
data: [
{ uuid: 't1', year: 1404, price_rials: 8_500_000, is_active: true },
{ uuid: 't2', year: 1403, price_rials: 7_000_000, is_active: false },
],
} });
if (url.includes('/tenant-insurances')) return Promise.resolve({ data: { data: [
{ uuid: 'ins1', insurance_name: 'بیمه ایران', insurance_kind: 'basic', coverage_percent: 70 },
] } });
@@ -104,13 +97,12 @@ describe('ServiceDetailPage (جزئیات سرویس)', () => {
expect(screen.getByText(/۱۴۰۲/)).toBeInTheDocument();
});
it('تب تعرفه‌ها فهرست سال‌ها را با نشان «سال جاری» می‌آورد', async () => {
/** تعرفهٔ سالانه حذف شد: قیمت فقط روی خودِ سرویس تعریف می‌شود. */
it('دیگر تب تعرفه‌ها ندارد', async () => {
render();
fireEvent.click(await screen.findByText('تعرفه‌ها'));
await screen.findByRole('heading', { name: 'سرم ۵۰۰cc' });
expect(await screen.findByText('سال جاری')).toBeInTheDocument();
expect(screen.getByText('۱۴۰۴')).toBeInTheDocument();
expect(screen.getByText('۱۴۰۳')).toBeInTheDocument();
expect(screen.queryByText('تعرفه‌ها')).not.toBeInTheDocument();
});
it('تب بیمه‌ها قراردادها و درصد پوشش را می‌آورد', async () => {
@@ -225,8 +217,8 @@ describe('ServiceDetailPage (جزئیات سرویس)', () => {
/** تب در URL می‌نشیند تا «بازگشت» و رفرش همان نما را بدهند. */
it('تب انتخاب‌شده از URL خوانده می‌شود', async () => {
render('/admin/clinic-services/it1?tab=tariffs');
render('/admin/clinic-services/it1?tab=insurance');
expect(await screen.findByText('سال جاری')).toBeInTheDocument();
expect(await screen.findByText('بیمه ایران')).toBeInTheDocument();
});
});