Refactor booking system: Remove unused policies, packages, and related entities

- Removed package consumption flags and related properties from PriceQuote.
- Eliminated unused domain event publishing for policies and waitlist in Schedule.
- Cleaned up BookingEngineSeeder by removing package and policy related logic.
- Updated SeedScenariosCommand to reflect removal of policies from output.
- Dropped policy, package, treatment course, cancellation, waitlist, and domain event tables in migration.
- Removed domain event assertions from tests related to resource blocking.
This commit is contained in:
hamed
2026-08-01 20:50:47 +03:30
parent 9486721fa3
commit c4f1f25c80
27 changed files with 129 additions and 1510 deletions
+10 -121
View File
@@ -1,138 +1,27 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { api, ApiError, type ApiResponse } from '../lib/api';
import type { CancellationPolicy, CancellationPreview, CancellationResult, WaitlistEntry } from '../types';
/**
* سیاست لغو و لیست انتظار.
* لغو نوبت — فقط انتقال وضعیت.
*
* پیش‌نمایش لغو همیشه از سرور می‌آید — همان محاسبه‌ای که خودِ لغو انجام می‌دهد، تا عددی
* که کاربر می‌بیند با آنچه کسر می‌شود یکی باشد.
* سیاست لغو، جریمه و لیست انتظار از محصول حذف شده‌اند، پس پیش‌نمایش مالی هم وجود ندارد:
* چیزی برای پیش‌نمایش نمانده. لغو همان `PATCH /api/v1/appointment/{uuid}/status` است که
* تایم‌لاین نوبت و دلیل را هم ثبت می‌کند.
*/
const POLICY_KEY = ['cancellation-policy'];
const WAITLIST_KEY = ['waitlist'];
function fail(e: unknown, fallback: string) {
toast.error(e instanceof ApiError ? e.message : fallback);
}
interface PolicyResponse {
default: CancellationPolicy | null;
overrides: CancellationPolicy[];
}
export function useCancellationPolicy() {
const qc = useQueryClient();
const query = useQuery({
queryKey: POLICY_KEY,
queryFn: () => api.get<ApiResponse<PolicyResponse>>('/api/v1/cancellation-policy'),
});
const save = useMutation({
mutationFn: (body: Record<string, unknown>) =>
api.put<ApiResponse<CancellationPolicy>>('/api/v1/cancellation-policy', body),
onSuccess: () => {
toast.success('سیاست لغو ذخیره شد');
qc.invalidateQueries({ queryKey: POLICY_KEY });
},
onError: (e) => fail(e, 'ذخیرهٔ سیاست ناموفق بود'),
});
return {
policy: query.data?.data?.default ?? null,
overrides: query.data?.data?.overrides ?? [],
loading: query.isLoading,
save,
};
}
export function useCancellationPreview(appointmentUuid: string | undefined, by: 'user' | 'doctor') {
const query = useQuery({
queryKey: ['cancellation-preview', appointmentUuid, by],
queryFn: () =>
api.get<ApiResponse<CancellationPreview>>(
`/api/v1/appointment/${appointmentUuid}/cancellation-preview?by=${by}`,
),
enabled: !!appointmentUuid,
});
return { preview: query.data?.data, loading: query.isLoading };
}
export function useCancelAppointment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ uuid, by, reason }: { uuid: string; by: 'user' | 'doctor'; reason?: string }) =>
api.post<ApiResponse<CancellationResult>>(`/api/v1/appointment/${uuid}/cancel`, {
by,
api.patch<ApiResponse<unknown>>(`/api/v1/appointment/${uuid}/status`, {
status: by === 'user' ? 'cancelled_by_user' : 'cancelled_by_doctor',
...(reason?.trim() ? { reason: reason.trim() } : {}),
}),
onSuccess: (res) => {
const notified = res.data.waitlist_notified;
toast.success(
notified > 0 ? `نوبت لغو شد و ${notified} نفر از لیست انتظار خبر شدند` : 'نوبت لغو شد',
);
qc.invalidateQueries({ queryKey: ['appointments'] });
qc.invalidateQueries({ queryKey: WAITLIST_KEY });
},
onError: (e) => fail(e, 'لغو نوبت ناموفق بود'),
});
}
/**
* درخواست‌هایی که با یک زمان مشخص می‌خوانند — ابزار لحظهٔ آزاد شدن ظرفیت.
*
* جدا از فهرست است چون سؤال متفاوتی می‌پرسد: نه «چه کسی منتظر است» بلکه «چه کسی
* برای **این** وقت منتظر است».
*/
export function useWaitlistMatches(params: { serviceUuid: string; start: number; branchUuid?: string }) {
const query = useQuery({
queryKey: ['waitlist-matches', params],
queryFn: () =>
api.get<ApiResponse<WaitlistEntry[]>>(
`/api/v1/waitlist/matches?service_uuid=${params.serviceUuid}&start=${params.start}` +
(params.branchUuid ? `&branch_uuid=${params.branchUuid}` : ''),
),
enabled: !!params.serviceUuid && params.start > 0,
});
return { matches: Array.isArray(query.data?.data) ? query.data.data : [], loading: query.isFetching };
}
export function useWaitlist(status?: string) {
const qc = useQueryClient();
const key = [...WAITLIST_KEY, status ?? ''];
const query = useQuery({
queryKey: key,
queryFn: () =>
api.get<ApiResponse<WaitlistEntry[]>>(`/api/v1/waitlist${status ? `?status=${status}` : ''}`),
});
const remove = useMutation({
mutationFn: (uuid: string) => api.delete<ApiResponse<null>>(`/api/v1/waitlist/${uuid}`),
onSuccess: () => {
toast.success('از لیست انتظار حذف شد');
qc.invalidateQueries({ queryKey: WAITLIST_KEY });
toast.success('نوبت لغو شد');
qc.invalidateQueries({ queryKey: ['appointments'] });
},
onError: (e) => fail(e, 'حذف ناموفق بود'),
onError: (e: unknown) => toast.error(e instanceof ApiError ? e.message : 'لغو نوبت ناموفق بود'),
});
return { entries: query.data?.data ?? [], loading: query.isLoading, remove };
}
/** خلاصهٔ عدم حضور یک بیمار — نشان است نه مانع؛ مسدودسازی کارِ قانون `eligibility` است. */
export function usePatientNoShows(patientUuid: string | undefined) {
const query = useQuery({
queryKey: ['patient-no-shows', patientUuid],
queryFn: () =>
api.get<ApiResponse<{ count: number; threshold: number; window_days: number; at_risk: boolean }>>(
`/api/v1/patient/${patientUuid}/no-shows`,
),
enabled: !!patientUuid,
});
return { noShows: query.data?.data ?? null };
}