Skip to main content

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}