datasynth_core/models/stock_compensation.rs
1//! Stock-based compensation models — ASC 718 / IFRS 2.
2//!
3//! This module provides data models for equity-settled share-based payment
4//! arrangements, including stock option grants, restricted stock units (RSUs),
5//! performance share units (PSUs), vesting schedules, and period expense
6//! recognition.
7//!
8//! # Framework references
9//!
10//! | Topic | ASC 718 (US GAAP) | IFRS 2 |
11//! |---------------------------|------------------------------------|-----------------------------------|
12//! | Measurement date | Grant date (equity awards) | Grant date (equity awards) |
13//! | Fair value model | Option pricing model required | Option pricing model required |
14//! | Expense recognition | Straight-line or graded | Straight-line (tranche-by-tranche)|
15//! | Forfeiture estimate | Estimate at grant; true-up | Estimate at grant; true-up |
16//! | Vesting conditions | Service, performance, market | Service, performance, market |
17
18use chrono::NaiveDate;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21
22// ---------------------------------------------------------------------------
23// Enums
24// ---------------------------------------------------------------------------
25
26/// Type of equity instrument granted.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
28#[serde(rename_all = "snake_case")]
29pub enum InstrumentType {
30 /// Stock options — right to purchase shares at the exercise price.
31 Options,
32 /// Restricted Stock Units — shares vest on service / time conditions.
33 #[default]
34 RSUs,
35 /// Performance Share Units — vest subject to performance conditions.
36 PSUs,
37}
38
39impl std::fmt::Display for InstrumentType {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Options => write!(f, "Options"),
43 Self::RSUs => write!(f, "RSUs"),
44 Self::PSUs => write!(f, "PSUs"),
45 }
46 }
47}
48
49/// Method used to determine the vesting pattern.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
51#[serde(rename_all = "snake_case")]
52pub enum VestingType {
53 /// All shares vest at a single date (100% cliff).
54 Cliff,
55 /// Shares vest in equal tranches over multiple periods.
56 #[default]
57 Graded,
58 /// Vesting depends on achievement of performance targets.
59 Performance,
60}
61
62// ---------------------------------------------------------------------------
63// Vesting schedule
64// ---------------------------------------------------------------------------
65
66/// A single vesting event within a schedule.
67///
68/// Each entry captures the percentage of the total grant that vests on the
69/// given date, together with the cumulative percentage vested to that point.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct VestingEntry {
72 /// Sequential period number (1-indexed, e.g. Year 1 = 1, Year 2 = 2 …).
73 pub period: u32,
74 /// Date on which this tranche vests.
75 pub vesting_date: NaiveDate,
76 /// Percentage of the total grant vesting in this period (e.g. 0.25 = 25%).
77 #[serde(with = "rust_decimal::serde::str")]
78 pub percentage: Decimal,
79 /// Cumulative percentage vested through this entry (e.g. 0.50 after Year 2 of 4).
80 #[serde(with = "rust_decimal::serde::str")]
81 pub cumulative_percentage: Decimal,
82}
83
84/// Vesting schedule attached to a stock grant.
85///
86/// For graded vesting the entries have equal `percentage` each year.
87/// For cliff vesting there is a single entry with `percentage = 1.00`.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct VestingSchedule {
90 /// Vesting pattern type.
91 pub vesting_type: VestingType,
92 /// Total number of vesting periods (e.g. 4 for a standard 4-year schedule).
93 pub total_periods: u32,
94 /// Cliff period count — periods before any vesting occurs (may be 0).
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub cliff_periods: Option<u32>,
97 /// Ordered list of vesting events; percentages must sum to 1.00.
98 pub vesting_entries: Vec<VestingEntry>,
99}
100
101// ---------------------------------------------------------------------------
102// Stock grant
103// ---------------------------------------------------------------------------
104
105/// A single stock-based compensation grant awarded to an employee.
106///
107/// One `StockGrant` corresponds to one award agreement. For option grants
108/// the `exercise_price` field is populated; RSUs and PSUs typically have
109/// no exercise price.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct StockGrant {
112 /// Unique grant identifier (e.g. "GRANT-1000-EMP001-2024").
113 pub id: String,
114 /// Company / entity code that issued the grant.
115 pub entity_code: String,
116 /// Employee who received the grant.
117 pub employee_id: String,
118 /// Date on which the grant was approved and the fair value is fixed.
119 pub grant_date: NaiveDate,
120 /// Type of instrument (Options, RSUs, or PSUs).
121 pub instrument_type: InstrumentType,
122 /// Number of shares / units granted.
123 pub quantity: u32,
124 /// Strike / exercise price per share (only applicable to Options).
125 #[serde(
126 default,
127 skip_serializing_if = "Option::is_none",
128 with = "rust_decimal::serde::str_option"
129 )]
130 pub exercise_price: Option<Decimal>,
131 /// Fair value per share / unit at the grant date (measurement basis for expense).
132 #[serde(with = "rust_decimal::serde::str")]
133 pub fair_value_at_grant: Decimal,
134 /// Total grant-date fair value (`quantity × fair_value_at_grant`).
135 #[serde(with = "rust_decimal::serde::str")]
136 pub total_grant_value: Decimal,
137 /// Vesting schedule defining when each tranche vests.
138 pub vesting_schedule: VestingSchedule,
139 /// Expiration date of options (None for RSUs/PSUs which expire on vesting).
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub expiration_date: Option<NaiveDate>,
142 /// Estimated annual forfeiture rate applied to reduce total grant expense.
143 #[serde(with = "rust_decimal::serde::str")]
144 pub forfeiture_rate: Decimal,
145 /// Reporting currency code (e.g. "USD").
146 pub currency: String,
147}
148
149// ---------------------------------------------------------------------------
150// Period expense record
151// ---------------------------------------------------------------------------
152
153/// Stock-based compensation expense recognised for a grant in one period.
154///
155/// Generated for each active vesting period. The `cumulative_recognized`
156/// plus `remaining_unrecognized` equals the total expense budget for this
157/// grant after applying the forfeiture estimate.
158///
159/// # Identities
160///
161/// `cumulative_recognized + remaining_unrecognized
162/// ≈ total_grant_value × (1 − forfeiture_rate)` (within rounding)
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct StockCompExpense {
165 /// Foreign key to `StockGrant.id`.
166 pub grant_id: String,
167 /// Company / entity code.
168 pub entity_code: String,
169 /// Period label (e.g. "2024-Q1" or "2024-12").
170 pub period: String,
171 /// Expense recognised in this period.
172 #[serde(with = "rust_decimal::serde::str")]
173 pub expense_amount: Decimal,
174 /// Cumulative expense recognised through the end of this period.
175 #[serde(with = "rust_decimal::serde::str")]
176 pub cumulative_recognized: Decimal,
177 /// Remaining unrecognised expense after this period.
178 #[serde(with = "rust_decimal::serde::str")]
179 pub remaining_unrecognized: Decimal,
180 /// Forfeiture rate applied to this grant (snapshot at grant date).
181 #[serde(with = "rust_decimal::serde::str")]
182 pub forfeiture_rate: Decimal,
183}