From 89e4a424f8ff8d0451c642148aec8399a47f6930 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Wed, 24 Jun 2026 13:17:08 +0330 Subject: [PATCH] feat(tax): implement tax rate history tracking and API endpoints --- assets/admin/pages/SettingsPage.tsx | 111 ++++++------------ docs/api/admin.md | 16 +++ migrations/Version20260624093738.php | 33 ++++++ .../Controller/SiteConfigController.php | 31 ++++- src/Config/Entity/TaxRateHistory.php | 54 +++++++++ .../Repository/SiteConfigRepository.php | 2 - .../Repository/TaxRateHistoryRepository.php | 21 ++++ 7 files changed, 186 insertions(+), 82 deletions(-) create mode 100644 migrations/Version20260624093738.php create mode 100644 src/Config/Entity/TaxRateHistory.php create mode 100644 src/Config/Repository/TaxRateHistoryRepository.php diff --git a/assets/admin/pages/SettingsPage.tsx b/assets/admin/pages/SettingsPage.tsx index 9557aa5a..a0c36da2 100644 --- a/assets/admin/pages/SettingsPage.tsx +++ b/assets/admin/pages/SettingsPage.tsx @@ -5,18 +5,21 @@ import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; +import { formatDateTime } from '../lib/utils'; import { Cog6ToothIcon, CheckCircleIcon } from '@heroicons/react/24/outline'; +interface TaxHistoryRow { + tax_percent: number; + enabled: boolean; + changed_by_name: string | null; + changed_at: number; +} + // ── Schema ──────────────────────────────────────────────────────────────── const schema = z.object({ site_name: z.string().min(1, 'نام سایت الزامی است'), support_phone: z.string(), - commission_enabled: z.string(), - commission_percent: z.string().refine(v => { - const n = Number(v); - return !isNaN(n) && n >= 0 && n <= 100; - }, 'درصد باید بین ۰ تا ۱۰۰ باشد'), max_cancel_hours_before: z.string(), appointment_reminder_hours: z.string(), // financial engine @@ -44,8 +47,6 @@ type FormValues = z.infer; interface Settings { site_name: string; support_phone: string; - commission_enabled: string; - commission_percent: string; max_cancel_hours_before: string; appointment_reminder_hours: string; appointment_commission_enabled: string; @@ -81,6 +82,14 @@ export default function SettingsPage() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const settings: Settings | undefined = (data?.data as any)?.data ?? data?.data; + const taxHistoryQ = useQuery({ + queryKey: ['tax-history'], + queryFn: () => api.get>('/api/v1/admin/settings/tax-history'), + staleTime: 30_000, + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const taxHistory: TaxHistoryRow[] = (taxHistoryQ.data?.data as any)?.data ?? taxHistoryQ.data?.data ?? []; + const { register, handleSubmit, @@ -95,8 +104,6 @@ export default function SettingsPage() { reset({ site_name: settings.site_name ?? 'ClinicPro', support_phone: settings.support_phone ?? '', - commission_enabled: settings.commission_enabled ?? '0', - commission_percent: settings.commission_percent ?? '0', max_cancel_hours_before: settings.max_cancel_hours_before ?? '24', appointment_reminder_hours: settings.appointment_reminder_hours ?? '2', appointment_commission_enabled: settings.appointment_commission_enabled ?? '0', @@ -123,10 +130,10 @@ export default function SettingsPage() { api.patch>('/api/v1/admin/settings', values), onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-settings'] }); + qc.invalidateQueries({ queryKey: ['tax-history'] }); }, }); - const commissionEnabled = watch('commission_enabled') === '1'; const paymentTestMode = watch('payment_test_mode') === '1'; const apptCommissionEnabled = watch('appointment_commission_enabled') === '1'; const upgradeCommissionEnabled = watch('upgrade_commission_enabled') === '1'; @@ -210,71 +217,6 @@ export default function SettingsPage() { - {/* تنظیمات کمیسیون */} -
-
-
- ٪ -
-

کمیسیون سایت

-
- -
- {/* toggle فعال/غیرفعال */} -
- {/* موتور مالی نمایندگی */}
@@ -352,6 +294,23 @@ export default function SettingsPage() {
)} + + {taxHistory.length > 0 && ( +
+
تاریخچه تغییرات مالیات
+
+ {taxHistory.map((h, i) => ( +
+ + {h.enabled ? `${h.tax_percent}٪` : 'غیرفعال'} + {h.changed_by_name && — {h.changed_by_name}} + + {formatDateTime(h.changed_at)} +
+ ))} +
+
+ )}
{/* هزینه پنل پیامک */} @@ -555,8 +514,6 @@ export default function SettingsPage() { onClick={() => settings && reset({ site_name: settings.site_name, support_phone: settings.support_phone, - commission_enabled: settings.commission_enabled, - commission_percent: settings.commission_percent, max_cancel_hours_before: settings.max_cancel_hours_before, appointment_reminder_hours: settings.appointment_reminder_hours, appointment_commission_enabled: settings.appointment_commission_enabled, diff --git a/docs/api/admin.md b/docs/api/admin.md index 14f7450f..c69e2e42 100644 --- a/docs/api/admin.md +++ b/docs/api/admin.md @@ -984,6 +984,8 @@ Reject a pending request. **Permission:** `ROLE_ADMIN` **ترتیب محاسبه** (در `CommissionService`): ۱) کسر `sms_panel_fee_rials` ۲) مالیاتِ استخراجی `afterSms × tax/(100+tax)` ۳) پورسانت = `netAfterTax × percent/100`. سهم نماینده به کیف‌پولش (`WalletTransaction` credit) واریز و یک ردیف `FinancialBreakdown` ثبت می‌شود (idempotent بر اساس `payment_id`). +> هر تغییر `tax_percent`/`tax_enabled` در یک ردیف `TaxRateHistory` ثبت و از `GET /api/v1/admin/settings/tax-history` قابل مشاهده است. + ### GET `/api/v1/admin/financial-breakdowns` لیست تفکیک مالی تراکنش‌ها (paginated). **Permission:** `ROLE_ADMIN` @@ -1035,3 +1037,17 @@ Reject a pending request. **Permission:** `ROLE_ADMIN` } } ``` + +### GET `/api/v1/admin/settings/tax-history` + +تاریخچه‌ی تغییرات مالیات بر ارزش افزوده (۵۰ ردیف آخر، نزولی). هر بار که `tax_percent` یا `tax_enabled` از طریق `PATCH /api/v1/admin/settings` تغییر کند، یک ردیف با کاربرِ تغییردهنده ثبت می‌شود. **Permission:** `ROLE_ADMIN` + +**Response `200`:** +```json +{ + "success": true, + "data": [ + { "tax_percent": 10, "enabled": true, "changed_by_name": "مدیر سیستم", "changed_at": 1782800000 } + ] +} +``` diff --git a/migrations/Version20260624093738.php b/migrations/Version20260624093738.php new file mode 100644 index 00000000..a1e20f4f --- /dev/null +++ b/migrations/Version20260624093738.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE tax_rate_history (id INT AUTO_INCREMENT NOT NULL, tax_percent NUMERIC(5, 2) NOT NULL, enabled TINYINT NOT NULL, changed_at INT NOT NULL, changed_by INT DEFAULT NULL, INDEX IDX_6D489C3210BC6D9F (changed_by), INDEX idx_tax_history_date (changed_at), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE tax_rate_history ADD CONSTRAINT FK_6D489C3210BC6D9F FOREIGN KEY (changed_by) REFERENCES users (id) ON DELETE SET NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE tax_rate_history DROP FOREIGN KEY FK_6D489C3210BC6D9F'); + $this->addSql('DROP TABLE tax_rate_history'); + } +} diff --git a/src/Config/Controller/SiteConfigController.php b/src/Config/Controller/SiteConfigController.php index 8a846c29..8f3f6fab 100644 --- a/src/Config/Controller/SiteConfigController.php +++ b/src/Config/Controller/SiteConfigController.php @@ -16,8 +16,6 @@ use OpenApi\Attributes as OA; class SiteConfigController extends BaseController { private const ALLOWED_KEYS = [ - 'commission_enabled', - 'commission_percent', // financial engine 'appointment_commission_enabled', 'upgrade_commission_enabled', @@ -47,6 +45,7 @@ class SiteConfigController extends BaseController public function __construct( private readonly SiteConfigRepository $configRepo, + private readonly \App\Config\Repository\TaxRateHistoryRepository $taxHistoryRepo, private readonly EntityManagerInterface $em, ) {} @@ -57,10 +56,14 @@ class SiteConfigController extends BaseController } #[Route('/api/v1/admin/settings', methods: ['PATCH'])] - public function patch(Request $request): JsonResponse + public function patch(Request $request, #[\Symfony\Component\Security\Http\Attribute\CurrentUser] \App\Auth\Entity\User $admin): JsonResponse { $data = json_decode($request->getContent(), true) ?? []; + // مقادیر فعلی مالیات برای تشخیص تغییر و ثبت تاریخچه. + $prevTaxPercent = $this->configRepo->get('tax_percent'); + $prevTaxEnabled = $this->configRepo->get('tax_enabled'); + foreach ($data as $key => $value) { if (!in_array($key, self::ALLOWED_KEYS, true)) { continue; @@ -70,6 +73,28 @@ class SiteConfigController extends BaseController $this->em->flush(); + $newTaxPercent = $this->configRepo->get('tax_percent'); + $newTaxEnabled = $this->configRepo->get('tax_enabled'); + + if ($newTaxPercent !== $prevTaxPercent || $newTaxEnabled !== $prevTaxEnabled) { + $this->taxHistoryRepo->save(new \App\Config\Entity\TaxRateHistory( + (string) $newTaxPercent, + $newTaxEnabled === '1', + $admin, + )); + } + return $this->success($this->configRepo->getAll()); } + + #[Route('/api/v1/admin/settings/tax-history', methods: ['GET'])] + public function taxHistory(): JsonResponse + { + $rows = $this->taxHistoryRepo->findBy([], ['changedAt' => 'DESC'], 50); + + return $this->success(array_map( + fn(\App\Config\Entity\TaxRateHistory $h) => $h->toArray(), + $rows, + )); + } } diff --git a/src/Config/Entity/TaxRateHistory.php b/src/Config/Entity/TaxRateHistory.php new file mode 100644 index 00000000..45c8228b --- /dev/null +++ b/src/Config/Entity/TaxRateHistory.php @@ -0,0 +1,54 @@ +taxPercent = $taxPercent; + $this->enabled = $enabled; + $this->changedBy = $changedBy; + $this->changedAt = time(); + } + + public function getId(): ?int { return $this->id; } + public function getTaxPercent(): string { return $this->taxPercent; } + public function isEnabled(): bool { return $this->enabled; } + public function getChangedBy(): ?User { return $this->changedBy; } + public function getChangedAt(): int { return $this->changedAt; } + + public function toArray(): array + { + return [ + 'tax_percent' => (float) $this->taxPercent, + 'enabled' => $this->enabled, + 'changed_by_name' => $this->changedBy?->getRealName(), + 'changed_at' => $this->changedAt, + ]; + } +} diff --git a/src/Config/Repository/SiteConfigRepository.php b/src/Config/Repository/SiteConfigRepository.php index da22afbc..df283fe8 100644 --- a/src/Config/Repository/SiteConfigRepository.php +++ b/src/Config/Repository/SiteConfigRepository.php @@ -10,8 +10,6 @@ class SiteConfigRepository extends ServiceEntityRepository { // Default values returned when a key is missing from DB private const DEFAULTS = [ - 'commission_enabled' => '0', - 'commission_percent' => '0', // financial engine 'appointment_commission_enabled' => '0', 'upgrade_commission_enabled' => '0', diff --git a/src/Config/Repository/TaxRateHistoryRepository.php b/src/Config/Repository/TaxRateHistoryRepository.php new file mode 100644 index 00000000..4fcf1955 --- /dev/null +++ b/src/Config/Repository/TaxRateHistoryRepository.php @@ -0,0 +1,21 @@ +getEntityManager()->persist($entity); + if ($flush) $this->getEntityManager()->flush(); + } +}