import { z } from 'zod'; import { CURRENCIES } from './filters.ts'; // Вознаграждение агента. Владелец компании задаёт каждому: оклад, процент с комиссии по его // сделкам или оклад + процент. По этим условиям считается, кому сколько платить за период. export const AgentPay = z.object({ /** fixed — оклад; percent — процент со сделок; mixed — оклад + процент. */ mode: z.enum(['fixed', 'percent', 'mixed']), /** Оклад в месяц. */ fixed: z.number().min(0).max(100_000_000).default(0), /** Процент от комиссии агентства по сделкам продажи (и аренды, если для неё не задан свой). */ percent: z.number().min(0).max(100).default(0), /** Процент по сделкам аренды; null — тот же, что для продажи. */ percentRent: z.number().min(0).max(100).nullable().default(null), /** Валюта оклада и итоговой выплаты. */ currency: z.enum(CURRENCIES).default('USD'), /** План на месяц: число сделок и сумма комиссии агента в валюте условий; null — показатель не задан. */ planDeals: z.number().int().min(1).max(10_000).nullable().default(null), planCommission: z.number().positive().max(100_000_000).nullable().default(null), /** Бонус за выполненный план: fixed — сумма; percent — надбавка к проценту на всю комиссию агента за месяц. */ bonusMode: z.enum(['none', 'fixed', 'percent']).default('none'), bonusValue: z.number().min(0).max(100_000_000).default(0), }); export type AgentPay = z.infer; /** Сколько месяцев в периоде: полный календарный месяц — 1, половина — 0,5. Даты — «ГГГГ-ММ-ДД», включительно. */ export function monthsBetween(from: string, to: string): number { const day = (s: string) => new Date(`${s}T00:00:00Z`); const end = day(to); let total = 0; for (let cursor = day(from); cursor <= end; ) { const y = cursor.getUTCFullYear(); const m = cursor.getUTCMonth(); const daysInMonth = new Date(Date.UTC(y, m + 1, 0)).getUTCDate(); const last = new Date(Math.min(Date.UTC(y, m, daysInMonth), end.getTime())); total += (last.getUTCDate() - cursor.getUTCDate() + 1) / daysInMonth; cursor = new Date(Date.UTC(y, m + 1, 1)); } return Math.round(total * 100) / 100; } /** Вид сделки: от него зависит процент агента. */ export const DEAL_KINDS = ['sale', 'rent'] as const; export type DealKind = (typeof DEAL_KINDS)[number]; /** Процент агента по сделке данного вида; 0 — процента в условиях нет. */ export function payPercent(pay: AgentPay | null | undefined, kind: DealKind): number { if (!pay || pay.mode === 'fixed') return 0; return kind === 'rent' ? (pay.percentRent ?? pay.percent) : pay.percent; } /** * Выплата агенту за период в валюте его условий: оклад за число месяцев и процент с его части * комиссии по сделкам продажи и аренды (`commission` — уже в валюте условий). */ export function agentPayout(pay: AgentPay | null | undefined, commission: Record, months: number, deals = 0) { const round = (n: number) => Math.round(n * 100) / 100; const fixed = pay && pay.mode !== 'percent' ? round(pay.fixed * months) : 0; const shareSale = round((commission.sale * payPercent(pay, 'sale')) / 100); const shareRent = round((commission.rent * payPercent(pay, 'rent')) / 100); const share = round(shareSale + shareRent); const all = commission.sale + commission.rent; const planDone = planIsDone(pay, deals, all, months); const bonus = !pay || !planDone || pay.bonusMode === 'none' ? 0 : round(pay.bonusMode === 'fixed' ? pay.bonusValue : (all * pay.bonusValue) / 100); return { fixed, shareSale, shareRent, share, planDone, bonus, total: round(fixed + share + bonus) }; } /** План выполнен, когда достигнуты все заданные показатели; null — плана нет. За период из нескольких месяцев план кратно больше. */ export function planIsDone(pay: AgentPay | null | undefined, deals: number, commission: number, months: number): boolean | null { if (!pay || (pay.planDeals == null && pay.planCommission == null)) return null; const dealsOk = pay.planDeals == null || deals >= pay.planDeals * months; const commissionOk = pay.planCommission == null || commission >= pay.planCommission * months; return dealsOk && commissionOk; }