Files
clinicpro/src/Shared/Constant/ErrorCodes.php
T
hamedandClaude Opus 4.8 27d088c6dd feat: port tauri create-service payment flow to admin session settlement
Port the tauri /files/create-service page (payment mode) to the admin SPA
and back it with real multi-part session settlement:

Backend:
- New SessionPayment entity (session_payments table): partial payments
  per session with method (wallet/pos/cash/card), amount, paid_at, actor
- PatientSession: settlement discount (percent/fixed), discount_rials,
  paid_at, payments relation; remaining debt derived from
  final - discount - paid total
- POST /api/v1/session/{uuid}/payments: register a partial payment;
  wallet method debits the patient wallet; zero remaining marks paid
- PATCH /api/v1/session/{uuid}: accepts discount_type/discount_value
  (null removes) and paid_at, backward compatible
- New error codes: ERR_SESSION_PAYMENT_INVALID/_EXCEEDS,
  ERR_SESSION_DISCOUNT_INVALID
- Migration + 14 functional tests (partial/full/wallet/exceed/discount)

Frontend (admin):
- SessionPaymentPage: two-step stepper (پرداخت ← جزییات) ported from
  tauri AddService payment mode — service cost, settlement discount
  input, Jalali payment date, wallet balance, 4-method payment accordion,
  paid-list box, details summary
- SessionStepper + stepper/payment icons ported verbatim from tauri SVGs
- «تکمیل پرداخت» on SessionServiceCard now navigates to the payment page
  (replaces the small settle modal on PatientDetailPage)
- Routes for patients/ and my-patients/ variants; vitest coverage
- docs/api/patient.md updated

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 13:32:06 +03:30

183 lines
11 KiB
PHP

<?php
namespace App\Shared\Constant;
class ErrorCodes
{
// Auth
public const ERR_AUTH_001 = 'ERR_AUTH_001';
public const ERR_AUTH_002 = 'ERR_AUTH_002';
public const ERR_AUTH_003 = 'ERR_AUTH_003';
public const ERR_AUTH_004 = 'ERR_AUTH_004';
public const ERR_AUTH_005 = 'ERR_AUTH_005';
public const ERR_AUTH_006 = 'ERR_AUTH_006';
// Validation
public const ERR_VALIDATION_001 = 'ERR_VALIDATION_001';
public const ERR_VALIDATION_002 = 'ERR_VALIDATION_002';
// Not Found
public const ERR_NOT_FOUND_001 = 'ERR_NOT_FOUND_001';
// Conflict
public const ERR_CONFLICT_001 = 'ERR_CONFLICT_001';
// Forbidden
public const ERR_FORBIDDEN_001 = 'ERR_FORBIDDEN_001';
// Payment
public const ERR_PAYMENT_001 = 'ERR_PAYMENT_001';
public const ERR_PAYMENT_002 = 'ERR_PAYMENT_002';
public const ERR_PAYMENT_003 = 'ERR_PAYMENT_003';
// Appointment
public const ERR_APPOINTMENT_001 = 'ERR_APPOINTMENT_001';
public const ERR_APPOINTMENT_002 = 'ERR_APPOINTMENT_002';
// File
public const ERR_FILE_001 = 'ERR_FILE_001';
public const ERR_FILE_002 = 'ERR_FILE_002';
// SMS
public const ERR_SMS_001 = 'ERR_SMS_001';
public const ERR_SMS_002 = 'ERR_SMS_002';
public const ERR_SMS_003 = 'ERR_SMS_003';
// Secretary
public const ERR_SECRETARY_001 = 'ERR_SECRETARY_001';
// Staff
public const ERR_STAFF_NOT_FOUND = 'ERR_STAFF_NOT_FOUND';
// Subscription
public const ERR_SUBSCRIPTION_REQUIRED = 'ERR_SUBSCRIPTION_REQUIRED';
public const ERR_TRIAL_ALREADY_USED = 'ERR_TRIAL_ALREADY_USED';
public const ERR_TRIAL_DISABLED = 'ERR_TRIAL_DISABLED';
public const ERR_SUBSCRIPTION_NOT_FOUND = 'ERR_SUBSCRIPTION_NOT_FOUND';
// Clinic Services
public const ERR_SERVICE_ITEM_IN_USE = 'ERR_SERVICE_ITEM_IN_USE';
public const ERR_SERVICE_NOT_FOUND = 'ERR_SERVICE_NOT_FOUND';
// Patient
public const ERR_PATIENT_NOT_FOUND = 'ERR_PATIENT_NOT_FOUND';
public const ERR_SESSION_NOT_FOUND = 'ERR_SESSION_NOT_FOUND';
public const ERR_SESSION_PAYMENT_INVALID = 'ERR_SESSION_PAYMENT_INVALID';
public const ERR_SESSION_PAYMENT_EXCEEDS = 'ERR_SESSION_PAYMENT_EXCEEDS';
public const ERR_SESSION_DISCOUNT_INVALID = 'ERR_SESSION_DISCOUNT_INVALID';
// Profile
public const ERR_PROFILE_NATIONAL_CODE_TAKEN = 'ERR_PROFILE_001';
public const ERR_PROFILE_MOBILE_TAKEN = 'ERR_PROFILE_002';
// SMS Wallet
public const ERR_SMS_WALLET_INSUFFICIENT = 'ERR_SMS_WALLET_INSUFFICIENT';
// Patient Wallet
public const ERR_WALLET_INSUFFICIENT = 'ERR_WALLET_INSUFFICIENT';
// Rate Limit
public const ERR_RATE_LIMIT_001 = 'ERR_RATE_LIMIT_001';
// Captcha (ALTCHA)
public const ERR_CAPTCHA_001 = 'ERR_CAPTCHA_001';
// Rating
public const ERR_RATING_NOT_ELIGIBLE = 'ERR_RATING_NOT_ELIGIBLE';
// External Inquiry (s.api.ir)
public const ERR_EXTERNAL_001 = 'ERR_EXTERNAL_001';
public const ERR_EXTERNAL_NOT_CONFIGURED = 'ERR_EXTERNAL_002';
public const ERR_NATIONAL_CODE_MISMATCH = 'ERR_IDENTITY_001';
public const ERR_IBAN_MISMATCH = 'ERR_IDENTITY_002';
public const ERR_IBAN_LIMIT = 'ERR_IDENTITY_003';
public const ERR_NATIONAL_CODE_UNVERIFIED = 'ERR_IDENTITY_004';
// Legacy domain string codes — centralised here so message() resolves them.
// Wire values are kept identical (e.g. 'SLOT_TAKEN') for client compatibility.
public const VALIDATION = 'VALIDATION';
public const FORBIDDEN = 'FORBIDDEN';
public const NOT_FOUND = 'NOT_FOUND';
public const USER_NOT_FOUND = 'USER_NOT_FOUND';
public const DOCTOR_NOT_FOUND = 'DOCTOR_NOT_FOUND';
public const DOCTOR_EXISTS = 'DOCTOR_EXISTS';
public const CLINIC_NOT_FOUND = 'CLINIC_NOT_FOUND';
public const SLOT_TAKEN = 'SLOT_TAKEN';
public const SLOT_PAST = 'SLOT_PAST';
public const INVALID_ROLE = 'INVALID_ROLE';
public const DUPLICATE_REQUEST = 'DUPLICATE_REQUEST';
public const ERR_GONE = 'ERR_GONE';
public const ERR_MOVED = 'ERR_MOVED';
public const ERR_ACCESS_DENIED = 'ERR_ACCESS_DENIED';
public const ERR_METHOD_NOT_ALLOWED_001 = 'ERR_METHOD_NOT_ALLOWED_001';
public static function message(string $code): string
{
return match ($code) {
self::ERR_AUTH_001 => 'توکن JWT منقضی شده یا نامعتبر است',
self::ERR_AUTH_002 => 'کد OTP نامعتبر است',
self::ERR_AUTH_003 => 'کد OTP منقضی شده است',
self::ERR_AUTH_004 => 'تعداد تلاش‌های OTP به حد مجاز رسیده است',
self::ERR_AUTH_005 => 'نام کاربری یا رمز عبور اشتباه است',
self::ERR_AUTH_006 => 'این نوع حساب فقط از طریق کد OTP وارد می‌شود',
self::ERR_VALIDATION_001 => 'ورودی نامعتبر است',
self::ERR_VALIDATION_002 => 'فیلد الزامی وارد نشده است',
self::ERR_NOT_FOUND_001 => 'منبع درخواستی یافت نشد',
self::ERR_FORBIDDEN_001 => 'دسترسی به این منبع مجاز نیست',
self::ERR_PAYMENT_001 => 'درگاه پرداخت در دسترس نیست',
self::ERR_PAYMENT_002 => 'مبلغ پرداخت نامعتبر است',
self::ERR_PAYMENT_003 => 'وضعیت نوبت برای پرداخت مناسب نیست',
self::ERR_APPOINTMENT_001 => 'اسلات انتخاب‌شده در دسترس نیست',
self::ERR_APPOINTMENT_002 => 'نوبت قابل لغو نیست',
self::ERR_FILE_001 => 'فرمت فایل مجاز نیست',
self::ERR_FILE_002 => 'حجم فایل بیش از حد مجاز است (حداکثر 5MB)',
self::ERR_SMS_001 => 'موجودی پیامک کافی نیست',
self::ERR_SMS_002 => 'متغیر نامعتبر در تمپلیت',
self::ERR_SMS_003 => 'تمپلیت قبلاً ارسال شده است',
self::ERR_SECRETARY_001 => 'پلن فعلی اجازه منشی بیشتر را نمی‌دهد',
self::ERR_CONFLICT_001 => 'تداخل: منبع در حال استفاده است یا قبلاً تغییر کرده است',
self::ERR_RATE_LIMIT_001 => 'درخواست‌های زیاد. لطفاً بعداً تلاش کنید',
self::ERR_CAPTCHA_001 => 'تأیید امنیتی ناموفق بود. لطفاً صفحه را رفرش کنید و دوباره تلاش کنید',
self::ERR_STAFF_NOT_FOUND => 'پرسنل یافت نشد',
self::ERR_SUBSCRIPTION_REQUIRED => 'این قابلیت نیاز به پنل Basic یا بالاتر دارد',
self::ERR_TRIAL_ALREADY_USED => 'قبلاً از تریال استفاده کرده‌اید',
self::ERR_TRIAL_DISABLED => 'تریال در حال حاضر غیرفعال است',
self::ERR_SUBSCRIPTION_NOT_FOUND => 'اشتراک یافت نشد',
self::ERR_SERVICE_ITEM_IN_USE => 'این سرویس در پرونده بیمار ثبت شده است',
self::ERR_SERVICE_NOT_FOUND => 'سرویس یافت نشد',
self::ERR_PATIENT_NOT_FOUND => 'پرونده بیمار یافت نشد',
self::ERR_PROFILE_NATIONAL_CODE_TAKEN => 'این کد ملی قبلاً برای کاربر دیگری ثبت شده است',
self::ERR_PROFILE_MOBILE_TAKEN => 'این شماره موبایل قبلاً برای کاربر دیگری ثبت شده است',
self::ERR_SESSION_NOT_FOUND => 'مراجعه یافت نشد',
self::ERR_SESSION_PAYMENT_INVALID => 'مبلغ یا روش پرداخت نامعتبر است',
self::ERR_SESSION_PAYMENT_EXCEEDS => 'مبلغ پرداخت از مانده بدهی بیشتر است',
self::ERR_SESSION_DISCOUNT_INVALID => 'مقدار تخفیف نامعتبر است',
self::ERR_SMS_WALLET_INSUFFICIENT => 'موجودی کیف پیامک کافی نیست',
self::ERR_WALLET_INSUFFICIENT => 'موجودی کیف پول کافی نیست',
self::ERR_RATING_NOT_ELIGIBLE => 'برای ثبت نظر یا امتیاز باید در یک ماه گذشته نوبت تایید‌شده نزد این پزشک داشته باشید',
self::ERR_EXTERNAL_001 => 'خطا در استعلام. لطفاً بعداً تلاش کنید',
self::ERR_EXTERNAL_NOT_CONFIGURED => 'سرویس استعلام پیکربندی نشده است',
self::ERR_NATIONAL_CODE_MISMATCH => 'کد ملی متعلق به این شماره موبایل نیست',
self::ERR_IBAN_MISMATCH => 'شماره شبا متعلق به شما نیست',
self::ERR_IBAN_LIMIT => 'حداکثر دو شماره شبا می‌توانید ثبت کنید',
self::ERR_NATIONAL_CODE_UNVERIFIED => 'ابتدا کد ملی خود را تأیید کنید',
self::VALIDATION => 'ورودی نامعتبر است',
self::FORBIDDEN => 'دسترسی مجاز نیست',
self::NOT_FOUND => 'منبع یافت نشد',
self::USER_NOT_FOUND => 'کاربر یافت نشد',
self::DOCTOR_NOT_FOUND => 'پزشک یافت نشد',
self::DOCTOR_EXISTS => 'این پزشک قبلاً ثبت شده است',
self::CLINIC_NOT_FOUND => 'کلینیک یافت نشد',
self::SLOT_TAKEN => 'این نوبت قبلاً رزرو شده است',
self::SLOT_PAST => 'زمان این اسلات گذشته است',
self::INVALID_ROLE => 'نقش نامعتبر است',
self::DUPLICATE_REQUEST => 'درخواست تکراری است',
self::ERR_GONE => 'این منبع دیگر در دسترس نیست',
self::ERR_MOVED => 'این منبع منتقل شده است',
self::ERR_ACCESS_DENIED => 'دسترسی مجاز نیست',
self::ERR_METHOD_NOT_ALLOWED_001 => 'متد درخواستی برای این آدرس مجاز نیست',
default => 'خطای ناشناخته',
};
}
}