Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
76 lines
4.9 KiB
TypeScript
76 lines
4.9 KiB
TypeScript
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<typeof AgentPay>;
|
|
|
|
/** Сколько месяцев в периоде: полный календарный месяц — 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<DealKind, number>, 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;
|
|
}
|