diff --git a/app/admin/accounting/page.tsx b/app/admin/accounting/page.tsx index 86caef4..659c5d1 100644 --- a/app/admin/accounting/page.tsx +++ b/app/admin/accounting/page.tsx @@ -10,7 +10,7 @@ import PersianDatePicker from '@/components/admin/PersianDatePicker'; import { Purchase, PAYMENT_GATEWAYS } from '@/types/purchase'; import { PackageName } from '@/types/package'; import { Product } from '@/types/product'; -import { formatPrice, getPurchaseFinalPrice, parseStoreRate } from '@/lib/utils'; +import { formatPrice, getPurchaseFinalPrice, parseStoreRate, parseFee } from '@/lib/utils'; import { MagnifyingGlassIcon, ArrowPathIcon, @@ -29,7 +29,7 @@ interface StoreSummary { count: number; gross: number; rate: number; - commission: number; + fee: number; net: number; } @@ -55,24 +55,42 @@ interface AccountingResult { const computeAccounting = ( purchases: Purchase[], myketRate: number, - cafeRate: number + cafeRate: number, + transactionFee: number, ): AccountingResult => { const rows = new Map(); let totalCount = 0; let totalGross = 0; let totalCommission = 0; - const makeStore = (rate: number): StoreSummary => ({ count: 0, gross: 0, rate, commission: 0, net: 0 }); + // درصد سهم توسعه‌دهنده به‌صورت کسر (0.85) و کارمزد تراکنش به ریال + const makeStore = (rate: number): StoreSummary => ({ count: 0, gross: 0, rate, fee: 0, net: 0 }); const myket: StoreSummary = makeStore(myketRate); const cafe: StoreSummary = makeStore(cafeRate); const other: StoreSummary = makeStore(0); - const collect = (store: StoreSummary, price: number, rate: number) => { - const commission = (price * rate) / 100; + // سهم توسعه‌دهنده برای مایکت/کافه‌بازار: + // (P / 1.1 - T) × R + // P = قیمت فروش (با احتساب ۱۰٪ مالیات بر ارزش افزوده) + // /1.1 = حذف ارزش افزوده، T = کارمزد تراکنش به ریال، R = درصد سهم + const developerShare = (price: number, rate: number, feePerTransaction: number) => { + const beforeVat = price / 1.1; + const afterVatAndFee = beforeVat - feePerTransaction; + if (afterVatAndFee <= 0) return 0; + return afterVatAndFee * (rate / 100); + }; + + const collect = (store: StoreSummary, price: number, rate: number, feePerTransaction: number) => { + let net: number; + if (rate > 0) { + net = developerShare(price, rate, feePerTransaction); + } else { + net = price; // سایر درگاه‌ها: بدون کارمزد فروشگاه + } store.count += 1; store.gross += price; - store.commission += commission; - store.net += price - commission; + store.fee += price - net; + store.net += net; }; for (const purchase of purchases) { @@ -81,22 +99,26 @@ const computeAccounting = ( let store: StoreSummary; let rate: number; + let feePerTransaction: number; if (gateway === PAYMENT_GATEWAYS.cafe.code) { store = cafe; rate = cafeRate; + feePerTransaction = transactionFee; } else if (gateway === PAYMENT_GATEWAYS.myket.code) { store = myket; rate = myketRate; + feePerTransaction = transactionFee; } else { store = other; rate = 0; + feePerTransaction = 0; } - collect(store, price, rate); + collect(store, price, rate, feePerTransaction); totalCount += 1; totalGross += price; - totalCommission += (price * rate) / 100; + totalCommission += price - developerShare(price, rate, feePerTransaction); const productId = purchase.product_id ?? purchase.product?.id ?? -1; let row = rows.get(productId); @@ -117,8 +139,8 @@ const computeAccounting = ( } row.count += 1; row.gross += price; - row.commission += (price * rate) / 100; - row.net += price - (price * rate) / 100; + row.commission += price - developerShare(price, rate, feePerTransaction); + row.net += developerShare(price, rate, feePerTransaction); } return { @@ -151,9 +173,11 @@ export default function AccountingPage() { // Status filter — both paid statuses selected by default const [statuses, setStatuses] = useState([...PAID_STATUSES]); - // Store commission rates (editable percentages) - const [myketRate, setMyketRate] = useState('15'); - const [cafeRate, setCafeRate] = useState('15'); + // درصد سهم توسعه‌دهنده (R در فرمول) — مایکت و کافه‌بازار به‌طور پیش‌فرض ۸۵٪ + const [myketRate, setMyketRate] = useState('85'); + const [cafeRate, setCafeRate] = useState('85'); + // کارمزد تراکنش به ریال (T در فرمول سهم توسعه‌دهنده) + const [transactionFee, setTransactionFee] = useState('1200'); const [allPurchases, setAllPurchases] = useState(null); const [result, setResult] = useState(null); @@ -198,19 +222,27 @@ export default function AccountingPage() { // Fetch all purchases once, then filter/aggregate in-memory. const purchases = await purchasesApi.getAllPurchases({ per_page: 500 }, token); setAllPurchases(purchases); - setResult(computeAccounting(purchases, parseStoreRate(myketRate), parseStoreRate(cafeRate))); + setResult( + computeAccounting( + purchases, + parseStoreRate(myketRate), + parseStoreRate(cafeRate), + parseFee(transactionFee) + ) + ); } catch (err) { setError(err instanceof Error ? err.message : 'خطا در دریافت داده‌ها'); } finally { setIsLoading(false); } - }, [token, myketRate, cafeRate]); + }, [token, myketRate, cafeRate, transactionFee]); // Recompute whenever filters or commission rates change (uses cached data) const runAnalysis = useCallback(() => { if (!allPurchases) return; const myket = parseStoreRate(myketRate); const cafe = parseStoreRate(cafeRate); + const fee = parseFee(transactionFee); const filtered = allPurchases.filter((p) => { // Status @@ -233,8 +265,8 @@ export default function AccountingPage() { return true; }); - setResult(computeAccounting(filtered, myket, cafe)); - }, [allPurchases, statuses, selectedProductId, selectedPackageName, dateFrom, dateTo, myketRate, cafeRate]); + setResult(computeAccounting(filtered, myket, cafe, fee)); + }, [allPurchases, statuses, selectedProductId, selectedPackageName, dateFrom, dateTo, myketRate, cafeRate, transactionFee]); // Recompute on filter/rate changes (no refetch needed once data is cached) useEffect(() => { @@ -247,8 +279,9 @@ export default function AccountingPage() { setDateFrom(''); setDateTo(''); setStatuses([...PAID_STATUSES]); - setMyketRate('15'); - setCafeRate('15'); + setMyketRate('85'); + setCafeRate('85'); + setTransactionFee('1200'); }; const toggleStatus = (code: number) => { @@ -368,11 +401,11 @@ export default function AccountingPage() { - {/* Commission rates */} + {/* Commission settings */}
setMyketRate(e.target.value)} className={inputClass} - placeholder="۱۵" + placeholder="۸۵" />
setCafeRate(e.target.value)} className={inputClass} - placeholder="۱۵" + placeholder="۸۵" + /> +
+
+ + setTransactionFee(e.target.value)} + className={inputClass} + placeholder="۱۲۰۰" />

- مایکت معمولاً ۱۵٪ یا ۳۰٪ و کافه بازار ۱۵٪ (زیر یک میلیارد تومان) یا ۳۰٪ کارمزد می‌گیرد. + سهم توسعه‌دهنده = (قیمت ÷ ۱٫۱ − کارمزد تراکنش) × درصد سهم. پیش‌فرض مایکت و + کافه‌بازار ۸۵٪ (فروش زیر ۱۰ میلیارد ریال) و ۷۰٪ (بالای آن) است.

@@ -566,7 +614,7 @@ function StoreCard({ title, summary }: { title: string; summary: StoreSummary })

{title}

{summary.rate > 0 && ( - کارمزد {summary.rate}٪ + سهم توسعه‌دهنده {summary.rate}٪ )}
@@ -580,8 +628,8 @@ function StoreCard({ title, summary }: { title: string; summary: StoreSummary }) {formatPrice(summary.gross)} تومان
- کارمزد - {formatPrice(summary.commission)} تومان + سهم فروشگاه‌ها + {formatPrice(summary.fee)} تومان
سود خالص diff --git a/lib/utils.ts b/lib/utils.ts index 95d4c79..fdeb9d4 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -35,6 +35,16 @@ export const parseStoreRate = (value: string | number): number => { return Math.min(100, Math.max(0, n)); }; +// Parse a non-negative integer input (e.g. transaction fee in Rial). +export const parseFee = (value: string | number): number => { + if (value === null || value === undefined || value === '') return 0; + const str = String(value).replace(/[^\d]/g, ''); + if (!str) return 0; + const n = parseInt(str, 10); + if (Number.isNaN(n)) return 0; + return Math.max(0, n); +}; + export const formatDate = (dateString: string | null): string => { if (!dateString) return 'نامشخص';