diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml index ca6c29b4..43dff6b0 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockConsolidation where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/consolidation/StockConsolidation.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,4 +62,5 @@ validateStockConsolidationOcfData d = -- Arrays element validation (security_ids required per Consolidation base schema) validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.security_ids && + validateUniqueStringsArray d.security_ids && not (null d.security_ids) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml index dcda2c04..016dc20f 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml @@ -8,6 +8,7 @@ module Fairmint.OpenCapTable.Types.Validation where import qualified DA.Text as Text import qualified DA.Crypto.Text as CryptoText +import qualified DA.Set as Set import Fairmint.Shared.TypeHelpers (validateRequiredText) -- Shared Text array validator @@ -15,6 +16,19 @@ import Fairmint.Shared.TypeHelpers (validateRequiredText) validateNonEmptyStringsArray : [Text] -> Bool validateNonEmptyStringsArray xs = all validateRequiredText xs +hasDuplicateTexts : [Text] -> Bool +hasDuplicateTexts = hasDuplicateTextsWith Set.empty + +hasDuplicateTextsWith : Set.Set Text -> [Text] -> Bool +hasDuplicateTextsWith seen remaining = case remaining of + [] -> False + text :: rest -> + text `Set.member` seen || hasDuplicateTextsWith (Set.insert text seen) rest + +-- Ensures no element appears more than once; empty arrays are allowed +validateUniqueStringsArray : [Text] -> Bool +validateUniqueStringsArray xs = not (hasDuplicateTexts xs) + -- Country code -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/CountryCode.schema.json validCountryCodes : [Text] diff --git a/Test/daml/OpenCapTable/Setup.daml b/Test/daml/OpenCapTable/Setup.daml index 764e6754..65ac7d33 100644 --- a/Test/daml/OpenCapTable/Setup.daml +++ b/Test/daml/OpenCapTable/Setup.daml @@ -1,13 +1,11 @@ module OpenCapTable.Setup where -import Fairmint.OpenCapTable.OCF.Issuer import Fairmint.OpenCapTable.CapTable import Fairmint.OpenCapTable.IssuerAuthorization import Fairmint.OpenCapTable.Types.Core (Context(..)) import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..)) +import OpenCapTable.TestHelpers (defaultIssuerWithAuthorizedShares) import Daml.Script -import DA.Date -import DA.Time data TestUsers = TestUsers{ tu_system_operator: Party, tu_issuer: Party, tu_alice: Party, tu_bob: Party, tu_charlie: Party, tu_rando: Party } @@ -42,20 +40,7 @@ setupTestOcp = script do capTableCid <- submit issuer do exerciseCmd authCid CreateCapTable with - issuer_data = IssuerOcfData with - id = "ISSUER_1" - legal_name = "TestIssuer Inc." - formation_date = time (DA.Date.date 2020 Jan 15) 12 0 0 - country_of_formation = "US" - dba = None - country_subdivision_of_formation = None - country_subdivision_name_of_formation = None - tax_ids = [] - email = None - phone = None - address = None - initial_shares_authorized = Some (OcfInitialSharesNumeric 1000.0) - comments = [] + issuer_data = defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 100_000_000.0)) return TestOcp { system_operator = system_operator diff --git a/Test/daml/OpenCapTable/TestConvertibleConversion.daml b/Test/daml/OpenCapTable/TestConvertibleConversion.daml index 83452d47..d92638d2 100644 --- a/Test/daml/OpenCapTable/TestConvertibleConversion.daml +++ b/Test/daml/OpenCapTable/TestConvertibleConversion.daml @@ -6,7 +6,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), OcfConvertibleConversionMechanism(..), OcfSAFEConversionMechanism(..), OcfConvertibleType(..), OcfCapitalizationDefinition(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (defaultStockIssuance, setupForStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -59,22 +59,33 @@ createConvertibleIssuance issuer capTableCid securityId amount = -- Based on prod: SAFE conversion to Series Seed-2 Preferred testConvertibleConversion_SAFEConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + (capTableCid, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let securityId = "7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e" + let resultSecurityId = "5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c" + let conversionDate = DT.time (DA.date 2025 Nov 10) 0 0 0 result <- createConvertibleIssuance issuer capTableCid securityId 100000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "25d377f8-a8de-4d8d-853c-2d75927e7927" - date = DT.time (DA.date 2025 Nov 10) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Automatic conversion upon Series Seed-2 equity financing" - resulting_security_ids = ["5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c"] - comments = ["SAFE conversion to Series Seed-2 Preferred for Investor A"] - balance_security_id = None - capitalization_definition = None - quantity_converted = None] + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "25d377f8-a8de-4d8d-853c-2d75927e7927" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Automatic conversion upon Series Seed-2 equity financing" + resulting_security_ids = [resultSecurityId] + comments = ["SAFE conversion to Series Seed-2 Preferred for Investor A"] + balance_security_id = None + capitalization_definition = None + quantity_converted = None + , CT.OcfCreateStockIssuance ((defaultStockIssuance + "TX_CONV_RESULT_1" + resultSecurityId + stakeholderId + stockClassId) with + date = conversionDate + quantity = 100.0) + ] edits = [] deletes = [] pure () @@ -82,26 +93,37 @@ testConvertibleConversion_SAFEConversion = script do -- Test 2: Convertible conversion with capitalization definition testConvertibleConversion_WithCapDefinition = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + (capTableCid, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let securityId = "CONV-SEC-002" + let resultSecurityId = "SSEC-PREFERRED-001" + let conversionDate = DT.time (DA.date 2025 Dec 01) 0 0 0 result <- createConvertibleIssuance issuer capTableCid securityId 250000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "TX_CONV_CONVERSION_2" - date = DT.time (DA.date 2025 Dec 01) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Conversion based on fully diluted capitalization at $16M valuation cap" - resulting_security_ids = ["SSEC-PREFERRED-001"] - comments = ["Conversion of $250,000 SAFE at $16M post-money valuation cap"] - balance_security_id = None - capitalization_definition = Some (OcfCapitalizationDefinition with - exclude_security_ids = [] - include_security_ids = [] - include_stock_class_ids = ["COMMON", "PREFERRED"] - include_stock_plans_ids = ["ESOP-2024"]) - quantity_converted = Some 15625.0] -- $250K / $16 per share + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_2" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Conversion based on fully diluted capitalization at $16M valuation cap" + resulting_security_ids = [resultSecurityId] + comments = ["Conversion of $250,000 SAFE at $16M post-money valuation cap"] + balance_security_id = None + capitalization_definition = Some (OcfCapitalizationDefinition with + exclude_security_ids = [] + include_security_ids = [] + include_stock_class_ids = ["COMMON", "PREFERRED"] + include_stock_plans_ids = ["ESOP-2024"]) + quantity_converted = Some 15625.0 + , CT.OcfCreateStockIssuance ((defaultStockIssuance + "TX_CONV_RESULT_2" + resultSecurityId + stakeholderId + stockClassId) with + date = conversionDate + quantity = 15625.0) + ] edits = [] deletes = [] pure () @@ -109,22 +131,33 @@ testConvertibleConversion_WithCapDefinition = script do -- Test 3: Partial conversion with balance security testConvertibleConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + (capTableCid, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let securityId = "CONV-SEC-003" + let resultSecurityId = "SSEC-PREFERRED-002" + let conversionDate = DT.time (DA.date 2025 Nov 15) 0 0 0 result <- createConvertibleIssuance issuer capTableCid securityId 500000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "TX_CONV_CONVERSION_3" - date = DT.time (DA.date 2025 Nov 15) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Partial conversion - investor electing to convert $300,000 of $500,000 SAFE" - resulting_security_ids = ["SSEC-PREFERRED-002"] - comments = [] - balance_security_id = Some "CONV-SEC-003-REMAINDER" - capitalization_definition = None - quantity_converted = Some 300000.0] + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_3" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Partial conversion - investor electing to convert $300,000 of $500,000 SAFE" + resulting_security_ids = [resultSecurityId] + comments = [] + balance_security_id = Some "CONV-SEC-003-REMAINDER" + capitalization_definition = None + quantity_converted = Some 300000.0 + , CT.OcfCreateStockIssuance ((defaultStockIssuance + "TX_CONV_RESULT_3" + resultSecurityId + stakeholderId + stockClassId) with + date = conversionDate + quantity = 300000.0) + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml b/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml index 93196948..7b914598 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml @@ -25,7 +25,7 @@ createEquityCompensationIssuance issuer capTableCid = stockholder_approval_date = None consideration_text = Some "Services rendered" security_law_exemptions = [] - stock_plan_id = Some "SP_2024" + stock_plan_id = None stock_class_id = Some "SC_COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = 10000.0 diff --git a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml index 7e0aaf6e..fe63b090 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml @@ -25,7 +25,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "PLAN-2020" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeOptionNSO quantity = quantity @@ -90,17 +90,43 @@ testEquityCompensationCancellation_PartialWithOptionals = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "SEC-EC-PARTIAL" + let balanceSecurityId = "SEC-EC-PARTIAL-BAL" + let cancellationDate = DT.time (DA.date 2024 Jun 30) 0 0 0 result <- createEquityCompensationIssuance issuer capTableCid securityId 500000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with - id = "TX_EC_CANCEL_PARTIAL" - date = DT.time (DA.date 2024 Jun 30) 0 0 0 - security_id = securityId - quantity = 150000.0 - reason_text = "Partial forfeiture due to early departure" - balance_security_id = Some "SEC-EC-PARTIAL-BAL" - comments = ["Partial cancellation per separation agreement", "Remaining vested options: 350,000"]] + creates = + [ CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with + id = "TX_EC_CANCEL_PARTIAL" + date = cancellationDate + security_id = securityId + quantity = 150000.0 + reason_text = "Partial forfeiture due to early departure" + balance_security_id = Some balanceSecurityId + comments = ["Partial cancellation per separation agreement", "Remaining vested options: 350,000"] + , CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = "TX_EC_CANCEL_PARTIAL_BALANCE" + date = cancellationDate + security_id = balanceSecurityId + custom_id = "GRANT-001-BALANCE" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + stock_plan_id = None + stock_class_id = Some "COMMON" + compensation_type = OcfCompensationTypeOptionNSO + quantity = 350000.0 + exercise_price = Some (OcfMonetary with amount = 0.10, currency = "USD") + base_price = None + early_exercisable = Some False + vesting_terms_id = None + vestings = [] + expiration_date = None + termination_exercise_windows = [] + comments = [] + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml index 48b46d44..81cec73d 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml @@ -2,7 +2,7 @@ module OpenCapTable.TestEquityCompensationExercise where import Fairmint.OpenCapTable.OCF.EquityCompensationExercise import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addEquityCompensationIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -11,7 +11,7 @@ import Daml.Script testCreateAndArchiveEquityCompensationExercise = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before exercising - cap_table <- addDefaultStakeholder issuer cap_table + cap_table <- addPrerequisites issuer cap_table cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-1" "EC-1" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with @@ -30,7 +30,7 @@ testCreateAndArchiveEquityCompensationExercise = script do testCreateAndArchiveEquityCompensationExerciseBuiltIn = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before exercising - cap_table <- addDefaultStakeholder issuer cap_table + cap_table <- addPrerequisites issuer cap_table cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-1" "EC-1" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with @@ -50,7 +50,7 @@ testCreateAndArchiveEquityCompensationExerciseBuiltIn = script do testEquityCompensationExercise_OptionalsSome = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before exercising - cap_table <- addDefaultStakeholder issuer cap_table + cap_table <- addPrerequisites issuer cap_table cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-2" "EC-2" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with diff --git a/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml b/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml index 197b697f..19ea5815 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml @@ -122,7 +122,7 @@ testEquityCompensationIssuance_OptionalsSome = script do exercise_price = Some (OcfMonetary with amount = 2.5, currency = "USD") expiration_date = Some (DT.time (DA.date 2034 Feb 01) 0 0 0) stock_class_id = Some "SC-1" - stock_plan_id = Some "PLAN-1" + stock_plan_id = None stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 20) 0 0 0) vesting_terms_id = Some "VT-1" security_law_exemptions = [OcfSecurityExemption with description = "ISO", jurisdiction = "US"] diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index e676d86e..84c3815a 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -1,35 +1,72 @@ module OpenCapTable.TestEquityCompensationRelease where +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance import Fairmint.OpenCapTable.OCF.EquityCompensationRelease import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +addEquityCompensationReleaseIssuance : Party -> ContractId CT.CapTable -> Text -> Text -> Text -> Decimal -> Script (ContractId CT.CapTable) +addEquityCompensationReleaseIssuance issuer capTableCid issuanceId securityId stakeholderId quantity = do + result <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = issuanceId + custom_id = "EC-" <> issuanceId + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + compensation_type = OcfCompensationTypeRSU + quantity = quantity + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = None + exercise_price = None + expiration_date = None + stock_class_id = Some "SC_COMMON" + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None] + edits = [] + deletes = [] + pure result.updatedCapTableCid + -- Test: Create and archive RSU release/settlement testCreateAndArchiveEquityCompensationRelease = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before releasing - cap_table <- addDefaultStakeholder issuer cap_table - cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-RSU-1" "SEC_RSU_001" "SH-1" + cap_table <- addPrerequisites issuer cap_table + cap_table <- addEquityCompensationReleaseIssuance issuer cap_table "EC-ISS-RSU-1" "SEC_RSU_001" "SH-1" 20000.0 -- RSU vesting and release _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_1" - date = DT.time (DA.date 2024 Apr 01) 0 0 0 - security_id = "SEC_RSU_001" - settlement_date = DT.time (DA.date 2024 Apr 03) 0 0 0 - release_price = OcfMonetary with amount = 45.50; currency = "USD" - quantity = 5000.0 - resulting_security_ids = ["SEC_STOCK_101"] - consideration_text = Some "RSU cliff vest - 25% of total grant" - comments = ["First anniversary cliff vest for Employee A"]] + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-1" "SEC_STOCK_101" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Apr 03) 0 0 0 + quantity = 5000.0) + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_1" + date = DT.time (DA.date 2024 Apr 01) 0 0 0 + security_id = "SEC_RSU_001" + settlement_date = DT.time (DA.date 2024 Apr 03) 0 0 0 + release_price = OcfMonetary with amount = 45.50; currency = "USD" + quantity = 5000.0 + resulting_security_ids = ["SEC_STOCK_101"] + consideration_text = Some "RSU cliff vest - 25% of total grant" + comments = ["First anniversary cliff vest for Employee A"] + ] edits = [] deletes = [] pure () @@ -39,21 +76,29 @@ testCreateAndArchiveEquityCompensationRelease = script do testCreateAndArchiveEquityCompensationReleaseQuarterly = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before releasing - cap_table <- addDefaultStakeholder issuer cap_table - cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-RSU-2" "SEC_RSU_002" "SH-1" + cap_table <- addPrerequisites issuer cap_table + cap_table <- addEquityCompensationReleaseIssuance issuer cap_table "EC-ISS-RSU-2" "SEC_RSU_002" "SH-1" 5000.0 -- Quarterly RSU vest with tax withholding _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_2" - date = DT.time (DA.date 2024 Jul 01) 0 0 0 - security_id = "SEC_RSU_002" - settlement_date = DT.time (DA.date 2024 Jul 05) 0 0 0 - release_price = OcfMonetary with amount = 52.00; currency = "USD" - quantity = 1250.0 - resulting_security_ids = ["SEC_STOCK_102", "SEC_STOCK_103"] - consideration_text = None - comments = ["Q3 2024 quarterly vest", "Net settlement after tax withholding"]] + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-2A" "SEC_STOCK_102" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Jul 05) 0 0 0 + quantity = 625.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-2B" "SEC_STOCK_103" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Jul 05) 0 0 0 + quantity = 625.0) + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_2" + date = DT.time (DA.date 2024 Jul 01) 0 0 0 + security_id = "SEC_RSU_002" + settlement_date = DT.time (DA.date 2024 Jul 05) 0 0 0 + release_price = OcfMonetary with amount = 52.00; currency = "USD" + quantity = 1250.0 + resulting_security_ids = ["SEC_STOCK_102", "SEC_STOCK_103"] + consideration_text = None + comments = ["Q3 2024 quarterly vest", "Net settlement after tax withholding"] + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml b/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml index 2103da62..95f38f7e 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml @@ -25,7 +25,7 @@ createEquityCompensationIssuance issuer capTableCid = stockholder_approval_date = None consideration_text = Some "Services rendered" security_law_exemptions = [] - stock_plan_id = Some "SP_2024" + stock_plan_id = None stock_class_id = Some "SC_COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = 10000.0 diff --git a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml index a3055eac..6aa27394 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml @@ -35,7 +35,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = early_exercisable = None expiration_date = Some (DT.time (DA.date 2032 Jan 15) 0 0 0) stock_class_id = Some "COMMON" - stock_plan_id = Some "PLAN-2020" + stock_plan_id = None vesting_terms_id = None stockholder_approval_date = None] edits = [] diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 1e4db3a0..dccb931c 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -1,14 +1,18 @@ module OpenCapTable.TestHelpers where -import DA.Assert (assertMsg) +import Fairmint.OpenCapTable.OCF.Issuer import Fairmint.OpenCapTable.OCF.Stakeholder import Fairmint.OpenCapTable.OCF.StockClass +import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment +import Fairmint.OpenCapTable.OCF.StockClassSplit (StockClassSplitOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlan (StockPlanOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfInitialSharesAuthorized(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType(..), OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), OcfConvertibleConversionMechanism(..), OcfSAFEConversionMechanism(..)) import qualified Fairmint.OpenCapTable.CapTable as CT @@ -24,6 +28,23 @@ import DA.Time (time) -- Functions that return valid default OCF data with sensible defaults. -- Use these to reduce boilerplate in tests that don't care about specific values. +-- | Returns valid IssuerOcfData with sensible defaults. +defaultIssuerWithAuthorizedShares : Text -> Optional OcfInitialSharesAuthorized -> IssuerOcfData +defaultIssuerWithAuthorizedShares issuerId initialSharesAuthorized = IssuerOcfData with + id = issuerId + legal_name = "TestIssuer Inc." + formation_date = DT.time (DA.date 2020 Jan 15) 12 0 0 + country_of_formation = "US" + dba = None + country_subdivision_of_formation = None + country_subdivision_name_of_formation = None + tax_ids = [] + email = None + phone = None + address = None + initial_shares_authorized = initialSharesAuthorized + comments = [] + -- | Default initial shares authorized for test stock classes. -- Configurable constant to avoid magic numbers. defaultInitialSharesAuthorized : Decimal @@ -53,6 +74,64 @@ defaultStockClass classId = StockClassOcfData with conversion_rights = [] comments = [] +limitedStockClass : Text -> Decimal -> StockClassOcfData +limitedStockClass classId authorizedShares = + (defaultStockClass classId) with + initial_shares_authorized = OcfInitialSharesNumeric authorizedShares + +stockClassAuthorizedSharesAdjustment : Text -> Text -> Decimal -> StockClassAuthorizedSharesAdjustmentOcfData +stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares = + StockClassAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_class_id = stockClassId + new_shares_authorized = authorizedShares + board_approval_date = None + stockholder_approval_date = None + comments = [] + +stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData +stockCancellation cancellationId securityId quantity = StockCancellationOcfData with + id = cancellationId + date = defaultTestDate + security_id = securityId + quantity = quantity + balance_security_id = None + reason_text = "Cancelled shares" + comments = [] + +stockClassSplit : Text -> Text -> Decimal -> Decimal -> StockClassSplitOcfData +stockClassSplit splitId stockClassId numerator denominator = StockClassSplitOcfData with + id = splitId + date = defaultTestDate + stock_class_id = stockClassId + split_ratio = OcfRatio with numerator = numerator, denominator = denominator + comments = [] + +stockPlanEquityCompensationIssuance : Text -> Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +stockPlanEquityCompensationIssuance issuanceId securityId stakeholderId stockClassId stockPlanId quantity = EquityCompensationIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "EC-" <> issuanceId + stakeholder_id = stakeholderId + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = Some False + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + stockholder_approval_date = None + vesting_terms_id = None + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + compensation_type = OcfCompensationTypeOptionNSO + quantity = quantity + comments = [] + -- | Returns valid StakeholderOcfData with sensible defaults. -- Provide ID and name; all other fields use reasonable test values. -- @@ -112,6 +191,18 @@ defaultStockIssuance issuanceId securityId stakeholderId stockClassId = StockIss issuance_type = None comments = [] +-- | Returns valid StockPlanOcfData with sensible defaults for a stock class. +defaultStockPlan : Text -> Text -> StockPlanOcfData +defaultStockPlan planId stockClassId = StockPlanOcfData with + id = planId + initial_shares_reserved = 1000.0 + plan_name = "Equity Incentive Plan" + comments = [] + stock_class_ids = [stockClassId] + board_approval_date = None + default_cancellation_behavior = None + stockholder_approval_date = None + -- ============================================================================= -- SETUP COMBINATORS -- ============================================================================= @@ -355,7 +446,7 @@ addDefaultStockIssuance : Party -> ContractId CT.CapTable -> Script (ContractId addDefaultStockIssuance issuer capTableCid = addStockIssuance issuer capTableCid "STOCK-ISS-1" "SEC-STOCK-1" "SH-1" "SC_COMMON" --- Helper to add an equity compensation issuance (requires stakeholder) +-- Helper to add an equity compensation issuance (requires stakeholder and SC_COMMON stock class) addEquityCompensationIssuance : Party -> ContractId CT.CapTable -> Text -> Text -> Text -> Script (ContractId CT.CapTable) addEquityCompensationIssuance issuer capTableCid issuanceId securityId stakeholderId = do result <- submit issuer do @@ -378,7 +469,7 @@ addEquityCompensationIssuance issuer capTableCid issuanceId securityId stakehold early_exercisable = None exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") expiration_date = None - stock_class_id = None + stock_class_id = Some "SC_COMMON" stock_plan_id = None stockholder_approval_date = None vesting_terms_id = None] diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml new file mode 100644 index 00000000..2f7b7515 --- /dev/null +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -0,0 +1,314 @@ +module OpenCapTable.TestIssuerAuthorizedShares where + +import Fairmint.OpenCapTable.OCF.IssuerAuthorizedSharesAdjustment +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +issuerAuthorizedSharesAdjustment : Text -> Decimal -> IssuerAuthorizedSharesAdjustmentOcfData +issuerAuthorizedSharesAdjustment adjustmentId sharesAuthorized = + IssuerAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + issuer_id = "ISSUER_1" + new_shares_authorized = sharesAuthorized + board_approval_date = None + stockholder_approval_date = None + comments = [] + +setIssuerAuthorizedShares : Party -> ContractId CT.CapTable -> Decimal -> Script (ContractId CT.CapTable) +setIssuerAuthorizedShares issuer capTable sharesAuthorized = do + result <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric sharesAuthorized)))] + deletes = [] + pure result.updatedCapTableCid + +testStockClassCannotExceedIssuerInitialAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (limitedStockClass "SC_TOO_MANY_FOR_ISSUER" 101.0)] + edits = [] + deletes = [] + +testIssuerAuthorizedSharesAdjustmentAllowsSameBatchStockClassIncrease = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + + _ <- submit issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_FOR_CLASS" 150.0) + , CT.OcfCreateStockClass ((limitedStockClass "SC_ALLOWED_BY_ISSUER_RAISE" 150.0) with board_approval_date = Some defaultTestDate) + ] + edits = [] + deletes = [] + pure () + +testIssuerAuthorizedSharesAdjustmentForOtherIssuerFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment + ((issuerAuthorizedSharesAdjustment "TX_ISSUER_OTHER_ISSUER" 100.0) with issuer_id = "OTHER_ISSUER") + ] + edits = [] + deletes = [] + +testIssuerHistoricalTotalsExcludeFutureEffectiveStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithExistingClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_EXISTING_EFFECTIVE" 100.0) + let jan1 = DT.time (DA.date 2024 Jan 01) 0 0 0 + let jan2 = DT.time (DA.date 2024 Jan 02) 0 0 0 + let futureClass = (limitedStockClass "SC_ISSUER_FUTURE_EFFECTIVE" 100.0) with + board_approval_date = Some jan2 + + _ <- submit issuer do + exerciseCmd capTableWithExistingClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_BEFORE_FUTURE_CLASS" 150.0) with date = jan1) + , CT.OcfCreateStockClass futureClass + , CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_WITH_FUTURE_CLASS" 200.0) with date = jan2) + ] + edits = [] + deletes = [] + pure () + +testIssuerInitialAuthorizationAppliesToUndatedStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + let laterAdjustment = (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_AFTER_UNDATED_CLASS" 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_UNDATED_OVER_INITIAL" 150.0) + , CT.OcfCreateIssuerAuthorizedSharesAdjustment laterAdjustment + ] + edits = [] + deletes = [] + +testIssuerHistoricalTotalsWaitForBothStockClassApprovals = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithExistingClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_BOTH_APPROVALS_EXISTING" 100.0) + let jan1 = DT.time (DA.date 2024 Jan 01) 0 0 0 + let jan2 = DT.time (DA.date 2024 Jan 02) 0 0 0 + let pendingClass = (limitedStockClass "SC_ISSUER_BOTH_APPROVALS_PENDING" 100.0) with + board_approval_date = Some jan1 + stockholder_approval_date = Some jan2 + + _ <- submit issuer do + exerciseCmd capTableWithExistingClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_AFTER_BOARD_APPROVAL" 150.0) with date = jan1) + , CT.OcfCreateStockClass pendingClass + , CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_AFTER_STOCKHOLDER_APPROVAL" 200.0) with date = jan2) + ] + edits = [] + deletes = [] + pure () + +testIssuerAuthorizedSharesAdjustmentCannotDropBelowStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_DROP" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_DROP_TOO_LOW" 99.0)] + edits = [] + deletes = [] + +testSameDateLowerIssuerAuthorizedSharesAdjustmentWins = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_SAME_DATE" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_LOW" 100.0) + , CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_HIGH" 150.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_SAME_DATE_EXTRA" 50.0)] + edits = [] + deletes = [] + +testSameDateLowerIssuerAuthorizedSharesAdjustmentWinsWithReversedCreateOrder = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_SAME_DATE_REVERSED" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_HIGH_FIRST" 150.0) + , CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_LOW_SECOND" 100.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_SAME_DATE_REVERSED_EXTRA" 50.0)] + edits = [] + deletes = [] + +testIssuerEditCannotDropBelowStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_EDIT_DROP" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 99.0)))] + deletes = [] + +testIssuerEditCannotDropInitialBelowStockClassesWhenAdjustmentExists = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 125.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_EDIT_HISTORICAL_DROP" 100.0) + let classAdjustment = stockClassAuthorizedSharesAdjustment "TX_CLASS_RAISE_BEFORE_ISSUER_EDIT" stockClassId 125.0 + let laterIssuerAdjustment = (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_AFTER_CLASS_EDIT" 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment classAdjustment + , CT.OcfCreateIssuerAuthorizedSharesAdjustment laterIssuerAdjustment + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 100.0)))] + deletes = [] + +testIssuerEditAllowsInitialBelowCurrentTotalWhenAdjustmentCoversHistory = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + result <- submit issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_HISTORY_OK" 150.0) + , CT.OcfCreateStockClass ((limitedStockClass "SC_ISSUER_EDIT_HISTORY_OK" 125.0) with board_approval_date = Some defaultTestDate) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 110.0)))] + deletes = [] + pure () + +testIssuerMetadataEditAllowedWhenAdjustmentRaisesAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + result <- submit issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_METADATA" 150.0) + , CT.OcfCreateStockClass ((limitedStockClass "SC_ISSUER_METADATA_WITH_ADJUSTMENT" 125.0) with board_approval_date = Some defaultTestDate) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer ((defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 100.0))) with legal_name = "Renamed TestIssuer Inc.")] + deletes = [] + pure () + +testStockClassAuthorizedSharesAdjustmentCannotExceedIssuerAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_CLASS_RAISE_TOO_HIGH" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_CLASS_RAISE_TOO_HIGH" stockClassId 101.0)] + edits = [] + deletes = [] + +testStockClassAuthorizedSharesAdjustmentCannotUseLaterIssuerAuthorizedSharesAdjustment = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_CLASS_BEFORE_ISSUER_RAISE" 100.0) + let classAdjustment = stockClassAuthorizedSharesAdjustment "TX_CLASS_BEFORE_ISSUER_RAISE" stockClassId 150.0 + let laterIssuerAdjustment = (issuerAuthorizedSharesAdjustment "TX_ISSUER_AFTER_CLASS_RAISE" 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment classAdjustment + , CT.OcfCreateIssuerAuthorizedSharesAdjustment laterIssuerAdjustment + ] + edits = [] + deletes = [] + +testIssuerAuthorizedSharesAdjustmentDeleteCannotInvalidateStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_DELETE_DROP" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_DELETE" 150.0) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_CLASS_RAISE_FOR_DELETE" stockClassId 150.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteIssuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_DELETE"] + +testFiniteIssuerRejectsUnlimitedStockClassAuthorization = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + let unlimitedClass = (defaultStockClass "SC_UNLIMITED_ISSUER_REJECT") with + initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesUnlimited + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass unlimitedClass] + edits = [] + deletes = [] + +testFiniteIssuerRejectsNotApplicableStockClassAuthorization = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + let notApplicableClass = (defaultStockClass "SC_NOT_APPLICABLE_ISSUER") with + initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesNotApplicable + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass notApplicableClass] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml new file mode 100644 index 00000000..4e55399e --- /dev/null +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -0,0 +1,588 @@ +module OpenCapTable.TestLifecycleInvariants where + +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer +import Fairmint.OpenCapTable.OCF.StockConsolidation +import Fairmint.OpenCapTable.OCF.StockReissuance +import Fairmint.OpenCapTable.OCF.StockRetraction +import Fairmint.OpenCapTable.OCF.StockTransfer +import Fairmint.OpenCapTable.OCF.WarrantExercise +import Fairmint.OpenCapTable.OCF.WarrantIssuance +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +lifecycleWarrantTrigger : Text -> Text -> OcfConversionTrigger +lifecycleWarrantTrigger triggerId stockClassId = + OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0 + converts_to_future_round = None + converts_to_stock_class_id = Some stockClassId + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = triggerId + type_ = OcfTriggerTypeTypeElectiveAtWill + +lifecycleWarrantIssuance : Text -> Text -> Text -> Text -> WarrantIssuanceOcfData +lifecycleWarrantIssuance issuanceId securityId stakeholderId stockClassId = + WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = defaultTestDate + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [lifecycleWarrantTrigger "TRIGGER_FIXED" stockClassId] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some 100.0 + quantity_source = Some OcfQuantityUnspecified + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + +lifecycleWarrantExercise : Text -> Text -> Optional Decimal -> [Text] -> WarrantExerciseOcfData +lifecycleWarrantExercise exerciseId securityId quantity resultingSecurityIds = + WarrantExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + trigger_id = "TRIGGER_FIXED" + comments = [] + resulting_security_ids = resultingSecurityIds + consideration_text = None + quantity = quantity + +lifecycleStockTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> StockTransferOcfData +lifecycleStockTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = + StockTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + balance_security_id = balanceSecurityId + consideration_text = None + comments = [] + +lifecycleStockReissuance : Text -> Text -> [Text] -> StockReissuanceOcfData +lifecycleStockReissuance reissuanceId securityId resultingSecurityIds = + StockReissuanceOcfData with + id = reissuanceId + date = defaultTestDate + security_id = securityId + resulting_security_ids = resultingSecurityIds + split_transaction_id = None + reason_text = Some "Lifecycle replacement" + comments = [] + +lifecycleStockConsolidation : Text -> [Text] -> Text -> StockConsolidationOcfData +lifecycleStockConsolidation consolidationId securityIds resultingSecurityId = + StockConsolidationOcfData with + id = consolidationId + date = defaultTestDate + security_ids = securityIds + resulting_security_id = resultingSecurityId + reason_text = Some "Lifecycle consolidation" + comments = [] + +lifecycleEquityIssuance : Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +lifecycleEquityIssuance issuanceId securityId stakeholderId stockClassId quantity = + EquityCompensationIssuanceOcfData with + id = issuanceId + custom_id = "EC-" <> issuanceId + date = defaultTestDate + compensation_type = OcfCompensationTypeOptionNSO + quantity + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = Some stockClassId + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None + +lifecycleEquityCancellation : Text -> Text -> Decimal -> Text -> EquityCompensationCancellationOcfData +lifecycleEquityCancellation cancellationId securityId quantity balanceSecurityId = + EquityCompensationCancellationOcfData with + id = cancellationId + date = defaultTestDate + security_id = securityId + quantity + balance_security_id = Some balanceSecurityId + reason_text = "Lifecycle cancellation" + comments = [] + +lifecycleEquityTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> EquityCompensationTransferOcfData +lifecycleEquityTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = + EquityCompensationTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + balance_security_id = balanceSecurityId + consideration_text = None + comments = [] + +lifecycleEquityExercise : Text -> Text -> Decimal -> [Text] -> EquityCompensationExerciseOcfData +lifecycleEquityExercise exerciseId securityId quantity resultingSecurityIds = + EquityCompensationExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + consideration_text = None + comments = [] + +lifecycleEquityRelease : Text -> Text -> Decimal -> [Text] -> EquityCompensationReleaseOcfData +lifecycleEquityRelease releaseId securityId quantity resultingSecurityIds = + EquityCompensationReleaseOcfData with + id = releaseId + date = defaultTestDate + security_id = securityId + settlement_date = defaultTestDate + release_price = OcfMonetary with amount = 1.0, currency = "USD" + quantity + resulting_security_ids = resultingSecurityIds + consideration_text = None + comments = [] + +testWarrantExerciseCannotExceedRemainingQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_REMAINING" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_REMAINING" "Warrant Remaining Holder" + let securityId = "WSEC_REMAINING" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_REMAINING_ISSUE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + firstExercise <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_REMAINING_FIRST" securityId (Some 60.0) ["SEC_WARRANT_REMAINING_FIRST"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_REMAINING_FIRST_RESULT" "SEC_WARRANT_REMAINING_FIRST" stakeholderId stockClassId) with quantity = 60.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd firstExercise.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_REMAINING_SECOND" securityId (Some 50.0) ["SEC_WARRANT_REMAINING_SECOND"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_REMAINING_SECOND_RESULT" "SEC_WARRANT_REMAINING_SECOND" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testTwoFullWarrantExercisesAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_DOUBLE_FULL" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_DOUBLE_FULL" "Warrant Double Full Holder" + let securityId = "WSEC_DOUBLE_FULL" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_DOUBLE_FULL_ISSUE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_DOUBLE_FULL_A" securityId None ["SEC_WARRANT_DOUBLE_FULL_A"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_WARRANT_DOUBLE_FULL_RESULT_A" "SEC_WARRANT_DOUBLE_FULL_A" stakeholderId stockClassId) + , CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_DOUBLE_FULL_B" securityId None ["SEC_WARRANT_DOUBLE_FULL_B"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_WARRANT_DOUBLE_FULL_RESULT_B" "SEC_WARRANT_DOUBLE_FULL_B" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testTwoStockReissuancesAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_DOUBLE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_DOUBLE" "Reissuance Double Holder" + let sourceSecurityId = "SEC_REISSUE_DOUBLE_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_REISSUE_DOUBLE_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_DOUBLE_A" sourceSecurityId ["SEC_REISSUE_DOUBLE_A"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_REISSUE_DOUBLE_RESULT_A" "SEC_REISSUE_DOUBLE_A" stakeholderId stockClassId) + , CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_DOUBLE_B" sourceSecurityId ["SEC_REISSUE_DOUBLE_B"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_REISSUE_DOUBLE_RESULT_B" "SEC_REISSUE_DOUBLE_B" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testMixedStockTerminalEventsAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TERMINAL_MIXED" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TERMINAL_MIXED" "Mixed Terminal Holder" + let sourceSecurityId = "SEC_TERMINAL_MIXED_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_TERMINAL_MIXED_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_TERMINAL_MIXED_REISSUE" sourceSecurityId ["SEC_TERMINAL_MIXED_REISSUE"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_TERMINAL_MIXED_REISSUE_RESULT" "SEC_TERMINAL_MIXED_REISSUE" stakeholderId stockClassId) + , CT.OcfCreateStockConsolidation (lifecycleStockConsolidation "TX_TERMINAL_MIXED_CONSOLIDATE" [sourceSecurityId] "SEC_TERMINAL_MIXED_CONSOLIDATE") + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_TERMINAL_MIXED_CONSOLIDATE_RESULT" "SEC_TERMINAL_MIXED_CONSOLIDATE" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testRepeatedEquityTerminalEventsAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_DOUBLE_TERMINAL" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_DOUBLE_TERMINAL" "Equity Double Terminal Holder" + let sourceSecurityId = "ESEC_DOUBLE_TERMINAL_SOURCE" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_SOURCE" sourceSecurityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_DOUBLE_TERMINAL_A" sourceSecurityId 50.0 "ESEC_DOUBLE_TERMINAL_A") + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_RESULT_A" "ESEC_DOUBLE_TERMINAL_A" stakeholderId stockClassId 50.0) + , CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_DOUBLE_TERMINAL_B" sourceSecurityId 50.0 "ESEC_DOUBLE_TERMINAL_B") + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_RESULT_B" "ESEC_DOUBLE_TERMINAL_B" stakeholderId stockClassId 50.0) + ] + edits = [] + deletes = [] + +testBalanceSecurityCannotAlsoBeResultingSecurity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_RESULT_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_RESULT_ALIAS" "Balance Result Alias Holder" + let sourceSecurityId = "SEC_BALANCE_RESULT_ALIAS_SOURCE" + let aliasSecurityId = "SEC_BALANCE_RESULT_ALIAS" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_BALANCE_RESULT_ALIAS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_BALANCE_RESULT_ALIAS" sourceSecurityId 50.0 (Some aliasSecurityId) [aliasSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_BALANCE_RESULT_ALIAS_OUTPUT" aliasSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockReissuanceCannotReuseSourceAsResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_SOURCE_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_SOURCE_ALIAS" "Reissuance Source Alias Holder" + let sourceSecurityId = "SEC_REISSUE_SOURCE_ALIAS" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_REISSUE_SOURCE_ALIAS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_SOURCE_ALIAS" sourceSecurityId [sourceSecurityId])] + edits = [] + deletes = [] + +testDuplicateResultIdsFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DUPLICATE_RESULTS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_DUPLICATE_RESULTS" "Duplicate Results Holder" + let sourceSecurityId = "SEC_DUPLICATE_RESULTS_SOURCE" + let resultSecurityId = "SEC_DUPLICATE_RESULTS_RESULT" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_DUPLICATE_RESULTS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_DUPLICATE_RESULTS" sourceSecurityId [resultSecurityId, resultSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_DUPLICATE_RESULTS_OUTPUT" resultSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testSuccessorSecurityCannotBeReusedAcrossTransactions = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SHARED_SUCCESSOR" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SHARED_SUCCESSOR" "Shared Successor Holder" + let firstSourceId = "SEC_SHARED_SUCCESSOR_SOURCE_A" + let secondSourceId = "SEC_SHARED_SUCCESSOR_SOURCE_B" + let resultSecurityId = "SEC_SHARED_SUCCESSOR_RESULT" + firstIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_SHARED_SUCCESSOR_SOURCE_A" firstSourceId stakeholderId stockClassId) with quantity = 50.0) + secondIssued <- createStockIssuanceWith issuer firstIssued ((defaultStockIssuance "TX_SHARED_SUCCESSOR_SOURCE_B" secondSourceId stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd secondIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_SHARED_SUCCESSOR_TRANSFER" firstSourceId 50.0 None [resultSecurityId]) + , CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_SHARED_SUCCESSOR_REISSUE" secondSourceId [resultSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SHARED_SUCCESSOR_RESULT" resultSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testCrossTypeSourceResultAliasFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CROSS_TYPE_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CROSS_TYPE_ALIAS" "Cross Type Alias Holder" + let sharedSecurityId = "SEC_CROSS_TYPE_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_CROSS_TYPE_ALIAS_WARRANT" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_CROSS_TYPE_ALIAS_EXERCISE" sharedSecurityId (Some 100.0) [sharedSecurityId]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_CROSS_TYPE_ALIAS_RESULT" sharedSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testUnmaterializedStockResultCannotAliasLiveWarrant = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_LIVE_CROSS_TYPE_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_LIVE_CROSS_TYPE_ALIAS" "Live Cross Type Alias Holder" + let stockSecurityId = "SEC_LIVE_CROSS_TYPE_ALIAS_STOCK" + let warrantSecurityId = "SEC_LIVE_CROSS_TYPE_ALIAS_WARRANT" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_LIVE_CROSS_TYPE_ALIAS_STOCK" stockSecurityId stakeholderId stockClassId) with quantity = 100.0) + , CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_LIVE_CROSS_TYPE_ALIAS_WARRANT" warrantSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_LIVE_CROSS_TYPE_ALIAS_TRANSFER" stockSecurityId 50.0 None [warrantSecurityId])] + edits = [] + deletes = [] + +testUnmaterializedPartialTransferWithBalancePreservesShareCount = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_UNMATERIALIZED_TRANSFER_BALANCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNMATERIALIZED_TRANSFER_BALANCE" "Unmaterialized Transfer Holder" + let sourceSecurityId = "SEC_UNMATERIALIZED_TRANSFER_SOURCE" + let balanceSecurityId = "SEC_UNMATERIALIZED_TRANSFER_BALANCE" + issued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_UNMATERIALIZED_TRANSFER_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + let balanceIssuance = (defaultStockIssuance "TX_UNMATERIALIZED_TRANSFER_BALANCE" balanceSecurityId stakeholderId stockClassId) with quantity = 60.0 + transferred <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_UNMATERIALIZED_TRANSFER" sourceSecurityId 40.0 (Some balanceSecurityId) ["SEC_UNMATERIALIZED_TRANSFER_MISSING_RESULT"]) + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_UNMATERIALIZED_TRANSFER_EXCESS" "SEC_UNMATERIALIZED_TRANSFER_EXCESS" stakeholderId stockClassId) with quantity = 1.0)] + edits = [] + deletes = [] + +testUnmaterializedReissuanceCannotOutliveSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_UNMATERIALIZED_REISSUANCE_SOURCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNMATERIALIZED_REISSUANCE_SOURCE" "Unmaterialized Reissuance Holder" + let sourceTransactionId = "TX_UNMATERIALIZED_REISSUANCE_SOURCE" + let sourceSecurityId = "SEC_UNMATERIALIZED_REISSUANCE_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance sourceTransactionId sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + reissued <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_UNMATERIALIZED_REISSUANCE" sourceSecurityId ["SEC_UNMATERIALIZED_REISSUANCE_MISSING_RESULT"])] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd reissued.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockIssuance sourceTransactionId] + +testUnmaterializedConsolidationCannotOutliveSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_UNMATERIALIZED_CONSOLIDATION_SOURCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNMATERIALIZED_CONSOLIDATION_SOURCE" "Unmaterialized Consolidation Holder" + let sourceTransactionId = "TX_UNMATERIALIZED_CONSOLIDATION_SOURCE" + let sourceSecurityId = "SEC_UNMATERIALIZED_CONSOLIDATION_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance sourceTransactionId sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + consolidated <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConsolidation (lifecycleStockConsolidation "TX_UNMATERIALIZED_CONSOLIDATION" [sourceSecurityId] "SEC_UNMATERIALIZED_CONSOLIDATION_MISSING_RESULT")] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd consolidated.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockIssuance sourceTransactionId] + +testSameSecurityIdCorrectionRetainsRetractionHistory = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CORRECTED_RETRACTED_SECURITY" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CORRECTED_RETRACTED_SECURITY" "Corrected Retracted Holder" + let originalTransactionId = "TX_CORRECTED_RETRACTED_ORIGINAL" + let securityId = "SEC_CORRECTED_RETRACTED" + issued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance originalTransactionId securityId stakeholderId stockClassId) with quantity = 100.0) + retracted <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockRetraction StockRetractionOcfData with + id = "TX_CORRECTED_RETRACTED_RETRACTION" + date = defaultTestDate + security_id = securityId + reason_text = "Corrected security remains retracted" + comments = []] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd retracted.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CORRECTED_RETRACTED_REPLACEMENT" securityId stakeholderId stockClassId) with quantity = 100.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CORRECTED_RETRACTED_NEW_SECURITY" "SEC_CORRECTED_RETRACTED_NEW_SECURITY" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [CT.OcfDeleteStockIssuance originalTransactionId] + pure () + +testStockBalanceSuccessorIncludesPriorUnmaterializedReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_STOCK_UNMATERIALIZED_HISTORY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_STOCK_UNMATERIALIZED_HISTORY" "Stock Unmaterialized History Holder" + let sourceSecurityId = "SEC_STOCK_UNMATERIALIZED_HISTORY_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_STOCK_UNMATERIALIZED_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId) + transferred <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_STOCK_UNMATERIALIZED_HISTORY_TRANSFER" sourceSecurityId 30.0 None ["SEC_STOCK_UNMATERIALIZED_HISTORY_MISSING"])] + edits = [] + deletes = [] + + let balanceSecurityId = "SEC_STOCK_UNMATERIALIZED_HISTORY_BALANCE" + let cancellation = (stockCancellation "TX_STOCK_UNMATERIALIZED_HISTORY_CANCEL" sourceSecurityId 20.0) with + balance_security_id = Some balanceSecurityId + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_STOCK_UNMATERIALIZED_HISTORY_BALANCE" balanceSecurityId stakeholderId stockClassId) with quantity = 80.0) + ] + edits = [] + deletes = [] + +testEquityBalanceSuccessorIncludesPriorUnmaterializedReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_UNMATERIALIZED_HISTORY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_UNMATERIALIZED_HISTORY" "Equity Unmaterialized History Holder" + let sourceSecurityId = "ESEC_UNMATERIALIZED_HISTORY_SOURCE" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_UNMATERIALIZED_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + transferred <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationTransfer (lifecycleEquityTransfer "TX_EQUITY_UNMATERIALIZED_HISTORY_TRANSFER" sourceSecurityId 30.0 None ["ESEC_UNMATERIALIZED_HISTORY_MISSING"])] + edits = [] + deletes = [] + + let balanceSecurityId = "ESEC_UNMATERIALIZED_HISTORY_BALANCE" + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_UNMATERIALIZED_HISTORY_CANCEL" sourceSecurityId 20.0 balanceSecurityId) + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_UNMATERIALIZED_HISTORY_BALANCE" balanceSecurityId stakeholderId stockClassId 80.0) + ] + edits = [] + deletes = [] + +testUnmaterializedWarrantExerciseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_UNMATERIALIZED_ALIAS" "Warrant Unmaterialized Alias Holder" + let securityId = "WSEC_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_UNMATERIALIZED_ALIAS_EXERCISE" securityId (Some 50.0) [securityId])] + edits = [] + deletes = [] + +testUnmaterializedEquityExerciseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" "Equity Exercise Alias Holder" + let securityId = "ESEC_EXERCISE_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (lifecycleEquityExercise "TX_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" securityId 50.0 [securityId])] + edits = [] + deletes = [] + +testUnmaterializedEquityReleaseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" "Equity Release Alias Holder" + let securityId = "ESEC_RELEASE_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_RELEASE_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRelease (lifecycleEquityRelease "TX_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" securityId 50.0 [securityId])] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml b/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml new file mode 100644 index 00000000..1380703f --- /dev/null +++ b/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml @@ -0,0 +1,121 @@ +module OpenCapTable.TestObjectIdUniqueness where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import qualified Fairmint.OpenCapTable.OCF.Document as Document +import OpenCapTable.Setup +import OpenCapTable.TestHelpers (defaultStakeholder, setupStakeholderWithId) +import qualified DA.Map as Map +import Daml.Script + +documentData : Text -> Document.DocumentOcfData +documentData objectId = Document.DocumentOcfData with + id = objectId + md5 = "0123456789abcdef0123456789abcdef" + comments = [] + related_objects = [] + path = Some ("documents/" <> objectId <> ".pdf") + uri = None + +testObjectIdUniqueness_SameBatchSameTypeDuplicateFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateDocument (documentData "DUPLICATE-DOCUMENT-ID") + , CT.OcfCreateDocument (documentData "DUPLICATE-DOCUMENT-ID") + ] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_PriorBatchSameTypeDuplicateFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + createResult <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "PRIOR-DOCUMENT-ID")] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "PRIOR-DOCUMENT-ID")] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_SameBatchCrossTypeDuplicateSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStakeholder (defaultStakeholder "SHARED-CROSS-TYPE-ID" "Shared ID Holder") + , CT.OcfCreateDocument (documentData "SHARED-CROSS-TYPE-ID") + ] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_PriorBatchCrossTypeDuplicateSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithStakeholder, _) <- setupStakeholderWithId issuer cap_table "PRIOR-CROSS-TYPE-ID" "Prior Holder" + + _ <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "PRIOR-CROSS-TYPE-ID")] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_IssuerIdMayBeReusedByOtherType = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "ISSUER_1")] + edits = [] + deletes = [] + pure () + +-- Validate fetched object data rather than trusting map keys on cap tables +-- upgraded from a package that did not enforce per-type object-id uniqueness. +testObjectIdUniqueness_CorruptedSameTypeFinalStateFails = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + capTableDataOpt <- queryContractId issuer cap_table + let Some capTableData = capTableDataOpt + + firstDocumentCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ Document.Document with + context = ctx + document_data = documentData "UPGRADED-DUPLICATE-DOCUMENT-ID" + secondDocumentCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ Document.Document with + context = ctx + document_data = documentData "UPGRADED-DUPLICATE-DOCUMENT-ID" + + let corruptedCapTable = capTableData with + documents = + Map.insert "CORRUPTED-DOCUMENT-MAP-KEY-1" firstDocumentCid + (Map.insert "CORRUPTED-DOCUMENT-MAP-KEY-2" secondDocumentCid capTableData.documents) + corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable + + submitMustFail issuer do + exerciseCmd corruptedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_DeleteThenReuseWithinTypeSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + createResult <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "REUSED-DOCUMENT-ID")] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "REUSED-DOCUMENT-ID")] + edits = [] + deletes = [CT.OcfDeleteDocument "REUSED-DOCUMENT-ID"] + pure () diff --git a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml index f43dcb17..cec9dbd8 100644 --- a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml +++ b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml @@ -11,17 +11,40 @@ module OpenCapTable.TestSecurityIdValidation where import qualified Fairmint.OpenCapTable.OCF.StockTransfer as ST import qualified Fairmint.OpenCapTable.OCF.WarrantTransfer as WT +import qualified Fairmint.OpenCapTable.OCF.WarrantIssuance as WI import qualified Fairmint.OpenCapTable.OCF.ConvertibleTransfer as CVT import qualified Fairmint.OpenCapTable.OCF.EquityCompensationTransfer as ECT import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance, addStockIssuance, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) +import qualified DA.Map as Map import qualified DA.Time as DT import Daml.Script +warrantIssuanceData : Text -> Text -> Text -> WI.WarrantIssuanceOcfData +warrantIssuanceData issuanceId securityId stakeholderId = WI.WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some 100.0 + quantity_source = None + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + -- ============================================================================= -- Test: Non-existent security_id fails with clear error @@ -204,6 +227,149 @@ testEquityCompensationTransfer_WrongSecurityTypeFails = script do deletes = [] pure () +-- ============================================================================= +-- Test: security_id values are globally unique across security issuance types +-- ============================================================================= + +testSecurityIssuance_DuplicateSecurityIdAcrossTypesFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultStockIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuanceData "WR-DUP-STOCK-SECURITY" "SEC-STOCK-1" "SH-1")] + edits = [] + deletes = [] + pure () + +testSecurityIssuance_DuplicateSecurityIdAcrossTypesFailsInSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX-BATCH-STOCK" "SEC-BATCH-DUP" "SH-1" "SC_COMMON") + , CT.OcfCreateWarrantIssuance (warrantIssuanceData "TX-BATCH-WARRANT" "SEC-BATCH-DUP" "SH-1") + ] + edits = [] + deletes = [] + pure () + +testSecurityIssuance_EditCannotReuseSecurityIdFromAnotherType = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultWarrantIssuance issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-EDIT-SRC" "SEC-STOCK-EDIT" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (defaultStockIssuance "STOCK-EDIT-SRC" "SEC-WR-1" "SH-1" "SC_COMMON")] + deletes = [] + pure () + +-- Validate issuance contracts rather than trusting potentially stale indexes +-- on cap tables upgraded from an earlier package. +testSecurityIssuance_FinalStateDuplicateSecurityIdsFail = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultStockIssuance issuer cap_table + capTableDataOpt <- queryContractId issuer cap_table + let Some capTableData = capTableDataOpt + + duplicateWarrantCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ WI.WarrantIssuance with + context = ctx + issuance_data = warrantIssuanceData "WR-UPGRADED-DUPLICATE" "SEC-STOCK-1" "SH-1" + + let corruptedCapTable = capTableData with + warrant_issuances = Map.insert "WR-UPGRADED-DUPLICATE" duplicateWarrantCid capTableData.warrant_issuances + corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable + + submitMustFail issuer do + exerciseCmd corruptedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [] + pure () + +testSuccessorSecurityId_CannotReuseSourceSecurityId = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-SOURCE-REUSE" "SEC-SOURCE-REUSE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-SOURCE-REUSE" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-SOURCE-REUSE" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SOURCE-REUSE"] + balance_security_id = None + consideration_text = None] + edits = [] + deletes = [] + pure () + +testSuccessorSecurityId_BalanceCannotAlsoBeResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-BALANCE-RESULT-SOURCE" "SEC-BALANCE-RESULT-SOURCE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance + ((defaultStockIssuance "STOCK-BALANCE-RESULT" "SEC-BALANCE-RESULT" "SH-1" "SC_COMMON") with quantity = 50.0) + , CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-BALANCE-RESULT" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-BALANCE-RESULT-SOURCE" + quantity = 50.0 + comments = [] + resulting_security_ids = ["SEC-BALANCE-RESULT"] + balance_security_id = Some "SEC-BALANCE-RESULT" + consideration_text = None + ] + edits = [] + deletes = [] + pure () + +testSuccessorSecurityId_CannotBeProducedByMultipleTransactionTypes = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-SHARED-RESULT-A" "SEC-SHARED-RESULT-A" "SH-1" "SC_COMMON" + cap_table <- addDefaultWarrantIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-SHARED-RESULT-A" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-SHARED-RESULT-A" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SHARED-RESULT"] + balance_security_id = None + consideration_text = None + , CT.OcfCreateWarrantTransfer WT.WarrantTransferOcfData with + id = "TX-SHARED-RESULT-B" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-WR-1" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SHARED-RESULT"] + balance_security_id = None + consideration_text = None + ] + edits = [] + deletes = [] + pure () + -- ============================================================================= -- Test: Valid security_id succeeds diff --git a/Test/daml/OpenCapTable/TestStockCancellation.daml b/Test/daml/OpenCapTable/TestStockCancellation.daml index e10b5cf1..d468ebe4 100644 --- a/Test/daml/OpenCapTable/TestStockCancellation.daml +++ b/Test/daml/OpenCapTable/TestStockCancellation.daml @@ -6,7 +6,7 @@ import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfInitialSharesAuthorized(..)) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -95,16 +95,23 @@ testStockCancellation_OptionalsSome = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table result <- createStockIssuance issuer capTableCid + let cancellationDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_CANCEL_2_BALANCE" "SEC-1-BAL2" "SH-1" "SC_COMMON") with + date = cancellationDate + quantity = 75.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockCancellation StockCancellationOcfData with - id = "TX_STOCK_CANCEL_2" - date = DT.time (DA.date 2024 Jan 03) 0 0 0 - security_id = "SEC-1" - quantity = 25.0 - balance_security_id = Some "SEC-1-BAL2" - reason_text = "Return to pool" - comments = ["Admin correction"]] + creates = + [ CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX_STOCK_CANCEL_2" + date = cancellationDate + security_id = "SEC-1" + quantity = 25.0 + balance_security_id = Some balanceIssuance.security_id + reason_text = "Return to pool" + comments = ["Admin correction"] + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml new file mode 100644 index 00000000..dee539f2 --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -0,0 +1,2439 @@ +module OpenCapTable.TestStockClassAuthorizedShares where + +import Fairmint.OpenCapTable.OCF.ConvertibleConversion +import Fairmint.OpenCapTable.OCF.ConvertibleIssuance +import Fairmint.OpenCapTable.OCF.StockConversion +import Fairmint.OpenCapTable.OCF.StockConsolidation +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease +import Fairmint.OpenCapTable.OCF.StockRepurchase +import Fairmint.OpenCapTable.OCF.StockRetraction +import Fairmint.OpenCapTable.OCF.StockReissuance +import Fairmint.OpenCapTable.OCF.StockTransfer +import Fairmint.OpenCapTable.OCF.StockClassConversionRatioAdjustment +import Fairmint.OpenCapTable.OCF.WarrantExercise +import Fairmint.OpenCapTable.OCF.WarrantIssuance +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfQuantitySourceType(..), OcfRatio(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +stockRepurchase : Text -> Text -> Decimal -> StockRepurchaseOcfData +stockRepurchase repurchaseId securityId quantity = + StockRepurchaseOcfData with + id = repurchaseId + date = defaultTestDate + security_id = securityId + price = OcfMonetary with amount = 1.0, currency = "USD" + quantity = quantity + balance_security_id = None + consideration_text = None + comments = [] + +stockConversion : Text -> Text -> Decimal -> StockConversionOcfData +stockConversion conversionId securityId quantity = + StockConversionOcfData with + id = conversionId + date = defaultTestDate + security_id = securityId + quantity_converted = quantity + resulting_security_ids = [conversionId <> "_RESULT"] + balance_security_id = None + comments = [] + +stockRetraction : Text -> Text -> StockRetractionOcfData +stockRetraction retractionId securityId = + StockRetractionOcfData with + id = retractionId + date = defaultTestDate + security_id = securityId + reason_text = "Retracted stock issuance" + comments = [] + +stockTransfer : Text -> Text -> Decimal -> [Text] -> StockTransferOcfData +stockTransfer transferId securityId quantity resultingSecurityIds = + StockTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = resultingSecurityIds + balance_security_id = None + consideration_text = None + comments = [] + +stockReissuance : Text -> Text -> [Text] -> StockReissuanceOcfData +stockReissuance reissuanceId securityId resultingSecurityIds = + StockReissuanceOcfData with + id = reissuanceId + date = defaultTestDate + security_id = securityId + resulting_security_ids = resultingSecurityIds + reason_text = Some "Reissued stock certificate" + split_transaction_id = None + comments = [] + +stockConsolidation : Text -> [Text] -> Text -> StockConsolidationOcfData +stockConsolidation consolidationId securityIds resultingSecurityId = + StockConsolidationOcfData with + id = consolidationId + date = defaultTestDate + security_ids = securityIds + resulting_security_id = resultingSecurityId + reason_text = Some "Consolidated stock certificates" + comments = [] + +stockClassConversionRightTo : Text -> OcfStockClassConversionRight +stockClassConversionRightTo stockClassId = + OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = None + conversion_trigger = ocfTriggerElectiveAtWill + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = None + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None + +stockClassConversionRatioAdjustment : Text -> Text -> Decimal -> StockClassConversionRatioAdjustmentOcfData +stockClassConversionRatioAdjustment adjustmentId stockClassId numerator = + StockClassConversionRatioAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_class_id = stockClassId + new_ratio_conversion_mechanism = OcfRatioConversionMechanism with + conversion_price = OcfMonetary with amount = 1.0, currency = "USD" + ratio = OcfRatio with numerator = numerator, denominator = 1.0 + rounding_type = OcfRoundingNormal + comments = [] + +warrantExerciseTriggerWith : Text -> OcfWarrantConversionMechanism -> Optional Text -> OcfConversionTrigger +warrantExerciseTriggerWith triggerId conversionMechanism stockClassId = + OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = conversionMechanism + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = triggerId + type_ = OcfTriggerTypeTypeElectiveAtWill + +warrantFixedExerciseTrigger : Text -> Text -> Decimal -> OcfConversionTrigger +warrantFixedExerciseTrigger triggerId stockClassId convertsToQuantity = + warrantExerciseTriggerWith + triggerId + (OcfWarrantMechanismFixedAmount with converts_to_quantity = convertsToQuantity) + (Some stockClassId) + +warrantCustomExerciseTrigger : Text -> Text -> OcfConversionTrigger +warrantCustomExerciseTrigger triggerId stockClassId = + warrantExerciseTriggerWith + triggerId + (OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant output") + (Some stockClassId) + +warrantIssuance : Text -> Text -> Text -> Text -> Decimal -> WarrantIssuanceOcfData +warrantIssuance issuanceId securityId stakeholderId stockClassId convertsToQuantity = + WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = defaultTestDate + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [warrantFixedExerciseTrigger "TRIGGER_FIXED" stockClassId convertsToQuantity] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some convertsToQuantity + quantity_source = Some OcfQuantityUnspecified + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + +warrantExercise : Text -> Text -> Decimal -> WarrantExerciseOcfData +warrantExercise exerciseId securityId quantity = + WarrantExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + trigger_id = "TRIGGER_FIXED" + comments = [] + resulting_security_ids = [exerciseId <> "_RESULT"] + consideration_text = None + quantity = Some quantity + +convertibleConversionTriggerWith : Text -> OcfConvertibleConversionMechanism -> Optional Text -> OcfConvertibleConversionTrigger +convertibleConversionTriggerWith triggerId conversionMechanism stockClassId = + OcfConvertibleConversionTrigger with + conversion_right = OcfConvertibleConversionRight with + conversion_mechanism = conversionMechanism + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + type_ = "CONVERTIBLE_CONVERSION_RIGHT" + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = triggerId + type_ = OcfTriggerTypeTypeElectiveAtWill + +convertibleIssuanceWith : Text -> Text -> Text -> Optional Text -> OcfConvertibleConversionMechanism -> Decimal -> ConvertibleIssuanceOcfData +convertibleIssuanceWith issuanceId securityId stakeholderId stockClassId conversionMechanism units = + ConvertibleIssuanceOcfData with + id = issuanceId + convertible_type = OcfConvertibleSafe + custom_id = "CV-" <> issuanceId + date = defaultTestDate + investment_amount = OcfMonetary with amount = units, currency = "USD" + security_id = securityId + seniority = 1 + stakeholder_id = stakeholderId + comments = [] + conversion_triggers = [convertibleConversionTriggerWith "TRIGGER_CONVERTIBLE" conversionMechanism stockClassId] + security_law_exemptions = [] + board_approval_date = None + consideration_text = None + pro_rata = None + stockholder_approval_date = None + +convertibleConversionWith : Text -> Text -> Optional Decimal -> [Text] -> ConvertibleConversionOcfData +convertibleConversionWith conversionId securityId quantityConverted resultingSecurityIds = + ConvertibleConversionOcfData with + id = conversionId + date = defaultTestDate + security_id = securityId + trigger_id = "TRIGGER_CONVERTIBLE" + reason_text = "Convertible converted into stock" + resulting_security_ids = resultingSecurityIds + comments = [] + balance_security_id = None + capitalization_definition = None + quantity_converted = quantityConverted + +equityCompensationIssuance : Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +equityCompensationIssuance issuanceId securityId stakeholderId stockClassId quantity = + EquityCompensationIssuanceOcfData with + id = issuanceId + custom_id = "EC-" <> issuanceId + date = defaultTestDate + compensation_type = OcfCompensationTypeOptionNSO + quantity = quantity + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = Some stockClassId + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None + +equityCompensationExercise : Text -> Text -> Decimal -> EquityCompensationExerciseOcfData +equityCompensationExercise exerciseId securityId quantity = + EquityCompensationExerciseOcfData with + id = exerciseId + date = defaultTestDate + quantity = quantity + security_id = securityId + comments = [] + resulting_security_ids = [exerciseId <> "_RESULT"] + consideration_text = None + +equityCompensationRelease : Text -> Text -> Time -> Time -> Decimal -> EquityCompensationReleaseOcfData +equityCompensationRelease releaseId securityId releaseDate settlementDate quantity = + EquityCompensationReleaseOcfData with + id = releaseId + date = releaseDate + security_id = securityId + settlement_date = settlementDate + release_price = OcfMonetary with amount = 1.0, currency = "USD" + quantity = quantity + resulting_security_ids = [releaseId <> "_RESULT"] + consideration_text = None + comments = [] + +testStockIssuanceCannotExceedStockClassInitialAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_LIMITED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_LIMITED" "Limited Holder" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TOO_MANY" "SEC_TOO_MANY" stakeholderId stockClassId) with quantity = 101.0)] + edits = [] + deletes = [] + +testStockIssuanceEditCannotExceedStockClassAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_ISSUANCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_ISSUANCE" "Edit Issuance Holder" + let originalIssuance = (defaultStockIssuance "TX_EDIT_ISSUANCE" "SEC_EDIT_ISSUANCE" stakeholderId stockClassId) with quantity = 75.0 + capTableIssued <- createStockIssuanceWith issuer capTableReady originalIssuance + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (originalIssuance with quantity = 101.0)] + deletes = [] + +testStockClassAuthorizedSharesAdjustmentAllowsAdditionalIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_ADJUSTABLE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_ADJUSTABLE" "Adjustable Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_INITIAL" "SEC_INITIAL" stakeholderId stockClassId) with quantity = 100.0) + + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_RAISE" stockClassId 150.0)] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_EXTRA" "SEC_EXTRA" stakeholderId stockClassId) with quantity = 50.0) + pure () + +testStockIssuanceCannotUseLaterStockClassAuthorizedSharesAdjustment = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BACKDATED_ISSUANCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BACKDATED_ISSUANCE" "Backdated Holder" + let laterAdjustment = (stockClassAuthorizedSharesAdjustment "TX_AUTH_AFTER_ISSUANCE" stockClassId 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment laterAdjustment + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_BACKDATED_TOO_MANY" "SEC_BACKDATED_TOO_MANY" stakeholderId stockClassId) with quantity = 125.0) + ] + edits = [] + deletes = [] + +testStockClassAuthorizedSharesAdjustmentCannotDropBelowIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_NO_DROP" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_NO_DROP" "No Drop Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_ISSUED" "SEC_ISSUED" stakeholderId stockClassId) with quantity = 75.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_DROP" stockClassId 50.0)] + edits = [] + deletes = [] + +testStockClassEditCannotDropInitialAuthorizedSharesBelowIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT" "Edit Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_EDIT_ISSUED" "SEC_EDIT_ISSUED" stakeholderId stockClassId) with quantity = 75.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 50.0)] + deletes = [] + +testStockClassEditCannotDropInitialAuthorizedSharesBelowIssuedSharesWhenAdjustmentExists = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_WITH_ADJUSTMENT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_WITH_ADJUSTMENT" "Edit With Adjustment Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_EDIT_WITH_ADJUSTMENT_EARLY_ISSUED" "SEC_EDIT_WITH_ADJUSTMENT_EARLY_ISSUED" stakeholderId stockClassId) with quantity = 75.0) + let laterAdjustment = (stockClassAuthorizedSharesAdjustment "TX_AUTH_EDIT_MASK" stockClassId 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment laterAdjustment] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 50.0)] + deletes = [] + +testStockClassEditAllowsInitialBelowCurrentSharesWhenAdjustmentCoversHistory = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_HISTORY_OK" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_HISTORY_OK" "Edit History Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_EDIT_HISTORY_OK" stockClassId 150.0)] + edits = [] + deletes = [] + + let laterIssuance = ((defaultStockIssuance "TX_EDIT_HISTORY_ISSUED" "SEC_EDIT_HISTORY_ISSUED" stakeholderId stockClassId) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 125.0) + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid laterIssuance + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 110.0)] + deletes = [] + pure () + +testStockClassMetadataEditAllowedWhenAdjustmentRaisesAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_METADATA_WITH_ADJUSTMENT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_METADATA_WITH_ADJUSTMENT" "Metadata With Adjustment Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_METADATA_EDIT" stockClassId 150.0)] + edits = [] + deletes = [] + + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_METADATA_WITH_ADJUSTMENT_ISSUED" "SEC_METADATA_WITH_ADJUSTMENT_ISSUED" stakeholderId stockClassId) with quantity = 125.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass ((limitedStockClass stockClassId 100.0) with name = "Edited Common Stock")] + deletes = [] + pure () + +testStockClassAuthorizedSharesAdjustmentDeleteCannotInvalidateIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DELETE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_DELETE" "Delete Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_DELETE" stockClassId 150.0)] + edits = [] + deletes = [] + + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_DELETE_ISSUED" "SEC_DELETE_ISSUED" stakeholderId stockClassId) with quantity = 125.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClassAuthorizedSharesAdjustment "TX_AUTH_DELETE"] + +testStockClassAuthorizedSharesReplacementAllowedInSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REPLACE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REPLACE" "Replacement Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_REPLACE_OLD" stockClassId 150.0)] + edits = [] + deletes = [] + + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_REPLACE_ISSUED" "SEC_REPLACE_ISSUED" stakeholderId stockClassId) with quantity = 125.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_REPLACE_NEW" stockClassId 200.0)] + edits = [] + deletes = [CT.OcfDeleteStockClassAuthorizedSharesAdjustment "TX_AUTH_REPLACE_OLD"] + pure () + +testStockClassDeleteWithLiveStockIssuanceFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DELETE_LIVE_LOTS" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_DELETE_LIVE_LOTS" "Delete Live Lots Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_DELETE_LIVE_LOTS" "SEC_DELETE_LIVE_LOTS" stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass stockClassId] + +testStockClassDeleteWithAuthorizedSharesAdjustmentFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DELETE_WITH_ADJUSTMENT" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment + (stockClassAuthorizedSharesAdjustment "TX_DELETE_WITH_ADJUSTMENT" stockClassId 150.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass stockClassId] + +testSameDateLowerStockClassAuthorizedSharesAdjustmentWins = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SAME_DATE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SAME_DATE" "Same Date Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_HIGH" stockClassId 200.0) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_LOW" stockClassId 100.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SAME_DATE_ISSUED" "SEC_SAME_DATE_ISSUED" stakeholderId stockClassId) with quantity = 150.0)] + edits = [] + deletes = [] + +testSameDateLowerStockClassAuthorizedSharesAdjustmentWinsWithReversedCreateOrder = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SAME_DATE_REVERSED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SAME_DATE_REVERSED" "Same Date Reversed Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_LOW_FIRST" stockClassId 100.0) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_HIGH_SECOND" stockClassId 200.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SAME_DATE_REVERSED_ISSUED" "SEC_SAME_DATE_REVERSED_ISSUED" stakeholderId stockClassId) with quantity = 150.0)] + edits = [] + deletes = [] + +testStockClassSplitCannotLeaveIssuedSharesAboveAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SPLIT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SPLIT" "Split Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_SPLIT_ISSUED" "SEC_SPLIT_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassSplit (stockClassSplit "TX_SPLIT_TOO_MANY" stockClassId 2.0 1.0)] + edits = [] + deletes = [] + +testPostSplitIssuanceUsesPostSplitQuantityForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_POST_SPLIT" 150.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_POST_SPLIT" "Post Split Holder" + splitResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit ((stockClassSplit "TX_POST_SPLIT" stockClassId 2.0 1.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0) + ] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith + issuer + splitResult.updatedCapTableCid + ((defaultStockIssuance "TX_AFTER_SPLIT" "SEC_AFTER_SPLIT" stakeholderId stockClassId) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + quantity = 125.0) + pure () + +testSameDateCancellationAppliesBeforeSplitForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CANCEL_SPLIT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CANCEL_SPLIT" "Cancel Split Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CANCEL_SPLIT_ISSUED" "SEC_CANCEL_SPLIT_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_BEFORE_SPLIT" "SEC_CANCEL_SPLIT_ISSUED" 50.0) + , CT.OcfCreateStockClassSplit (stockClassSplit "TX_SPLIT_AFTER_CANCEL" stockClassId 2.0 1.0) + ] + edits = [] + deletes = [] + pure () + +testStockCancellationReducesIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CANCEL" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CANCEL" "Cancellation Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CANCEL_ISSUED" "SEC_CANCEL_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_REDUCE" "SEC_CANCEL_ISSUED" 50.0)] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_CANCEL_REISSUE" "SEC_CANCEL_REISSUE" stakeholderId stockClassId) with quantity = 50.0) + pure () + +testStockCancellationCannotReduceSecurityBelowZero = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CANCEL_OVER_REDUCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CANCEL_OVER_REDUCE" "Cancel Over Reduce Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CANCEL_OVER_REDUCE_ISSUED" "SEC_CANCEL_OVER_REDUCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_OVER_REDUCE" "SEC_CANCEL_OVER_REDUCE" 150.0)] + edits = [] + deletes = [] + +testStockClassSplitCannotUseLaterAuthorizedSharesAdjustment = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SPLIT_LATER_AUTH" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SPLIT_LATER_AUTH" "Split Later Auth Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_SPLIT_LATER_AUTH_ISSUED" "SEC_SPLIT_LATER_AUTH_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + let laterAdjustment = (stockClassAuthorizedSharesAdjustment "TX_AUTH_AFTER_SPLIT" stockClassId 200.0) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + let earlierSplit = (stockClassSplit "TX_SPLIT_BEFORE_AUTH" stockClassId 2.0 1.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment laterAdjustment + , CT.OcfCreateStockClassSplit earlierSplit + ] + edits = [] + deletes = [] + +testStockRepurchaseReducesIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REPURCHASE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REPURCHASE" "Repurchase Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_REPURCHASE_ISSUED" "SEC_REPURCHASE_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockRepurchase (stockRepurchase "TX_REPURCHASE_REDUCE" "SEC_REPURCHASE_ISSUED" 50.0)] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_REPURCHASE_REISSUE" "SEC_REPURCHASE_REISSUE" stakeholderId stockClassId) with quantity = 50.0) + pure () + +testStockTransferWithResultingIssuanceDoesNotDoubleCountForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_RESULTING" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_RESULTING" "Transfer Resulting Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_SOURCE" "SEC_TRANSFER_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_RESULTING" "SEC_TRANSFER_SOURCE" 100.0 ["SEC_TRANSFER_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_RESULT" "SEC_TRANSFER_RESULT" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + +testStockTransferResultQuantityMustMatchTransferredQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_QUANTITY" "Transfer Quantity Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_QUANTITY_SOURCE" "SEC_TRANSFER_QUANTITY_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_QUANTITY" "SEC_TRANSFER_QUANTITY_SOURCE" 50.0 ["SEC_TRANSFER_QUANTITY_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_QUANTITY_RESULT" "SEC_TRANSFER_QUANTITY_RESULT" stakeholderId stockClassId) with quantity = 10.0) + ] + edits = [] + deletes = [] + +testStockTransferResultMustRemainInSourceStockClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithSource, sourceStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_SOURCE_CLASS" 1_000.0) + (capTableWithResult, resultStockClassId) <- setupStockClassWith issuer capTableWithSource (limitedStockClass "SC_TRANSFER_RESULT_CLASS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithResult "SH_TRANSFER_CLASS" "Transfer Class Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_CLASS_SOURCE" "SEC_TRANSFER_CLASS_SOURCE" stakeholderId sourceStockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_CLASS" "SEC_TRANSFER_CLASS_SOURCE" 50.0 ["SEC_TRANSFER_CLASS_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_CLASS_RESULT" "SEC_TRANSFER_CLASS_RESULT" stakeholderId resultStockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockTransferResultDateMustMatchTransferDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_RESULT_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_RESULT_DATE" "Transfer Result Date Holder" + let sourceDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_RESULT_DATE_SOURCE" "SEC_TRANSFER_RESULT_DATE_SOURCE" stakeholderId stockClassId) with + date = sourceDate + quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer ((stockTransfer "TX_TRANSFER_RESULT_DATE" "SEC_TRANSFER_RESULT_DATE_SOURCE" 100.0 ["SEC_TRANSFER_RESULT_DATE_RESULT"]) with date = transferDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_RESULT_DATE_RESULT" "SEC_TRANSFER_RESULT_DATE_RESULT" stakeholderId stockClassId) with + date = resultDate + quantity = 100.0) + ] + edits = [] + deletes = [] + +testStockReissuanceWithResultingIssuanceDoesNotDoubleCountForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_RESULTING" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUANCE_RESULTING" "Reissuance Resulting Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_REISSUANCE_SOURCE" "SEC_REISSUANCE_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUANCE_RESULTING" "SEC_REISSUANCE_SOURCE" ["SEC_REISSUANCE_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_RESULT" "SEC_REISSUANCE_RESULT" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + +testStockReissuanceResultQuantityMustMatchSourceQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUANCE_QUANTITY" "Reissuance Quantity Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUANCE_QUANTITY_SOURCE" "SEC_REISSUANCE_QUANTITY_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUANCE_QUANTITY" "SEC_REISSUANCE_QUANTITY_SOURCE" ["SEC_REISSUANCE_QUANTITY_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_QUANTITY_RESULT" "SEC_REISSUANCE_QUANTITY_RESULT" stakeholderId stockClassId) with quantity = 10.0) + ] + edits = [] + deletes = [] + +testStockReissuanceResultMustRemainInSourceStockClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithSource, sourceStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_CLASS_SOURCE" 1_000.0) + (capTableWithResult, resultStockClassId) <- setupStockClassWith issuer capTableWithSource (limitedStockClass "SC_REISSUANCE_CLASS_RESULT" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithResult "SH_REISSUANCE_CLASS" "Reissuance Class Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUANCE_CLASS_SOURCE" "SEC_REISSUANCE_CLASS_SOURCE" stakeholderId sourceStockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUANCE_CLASS" "SEC_REISSUANCE_CLASS_SOURCE" ["SEC_REISSUANCE_CLASS_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_CLASS_RESULT" "SEC_REISSUANCE_CLASS_RESULT" stakeholderId resultStockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + +testStockReissuanceResultDateMustMatchReissuanceDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUANCE_DATE" "Reissuance Date Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUANCE_DATE_SOURCE" "SEC_REISSUANCE_DATE_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + let reissuanceDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance ((stockReissuance "TX_REISSUANCE_DATE" "SEC_REISSUANCE_DATE_SOURCE" ["SEC_REISSUANCE_DATE_RESULT"]) with date = reissuanceDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_DATE_RESULT" "SEC_REISSUANCE_DATE_RESULT" stakeholderId stockClassId) with + date = resultDate + quantity = 100.0) + ] + edits = [] + deletes = [] + +testStockReissuanceSameTimestampSplitUsesAdjustedSourceQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_SPLIT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUANCE_SPLIT" "Reissuance Split Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUANCE_SPLIT_SOURCE" "SEC_REISSUANCE_SPLIT_SOURCE" stakeholderId stockClassId) with quantity = 50.0) + let eventDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit ((stockClassSplit "TX_REISSUANCE_SPLIT_EVENT" stockClassId 2.0 1.0) with date = eventDate) + , CT.OcfCreateStockReissuance ((stockReissuance "TX_REISSUANCE_SPLIT" "SEC_REISSUANCE_SPLIT_SOURCE" ["SEC_REISSUANCE_SPLIT_RESULT"]) with date = eventDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_SPLIT_RESULT" "SEC_REISSUANCE_SPLIT_RESULT" stakeholderId stockClassId) with + date = eventDate + quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + +testStockConsolidationWithResultingIssuanceDoesNotDoubleCountForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_RESULTING" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONSOLIDATION_RESULTING" "Consolidation Resulting Holder" + capTableOne <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONSOLIDATION_SOURCE_A" "SEC_CONSOLIDATION_SOURCE_A" stakeholderId stockClassId) with quantity = 50.0) + capTableTwo <- createStockIssuanceWith issuer capTableOne ((defaultStockIssuance "TX_CONSOLIDATION_SOURCE_B" "SEC_CONSOLIDATION_SOURCE_B" stakeholderId stockClassId) with quantity = 50.0) + + _ <- submit issuer do + exerciseCmd capTableTwo CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation (stockConsolidation "TX_CONSOLIDATION_RESULTING" ["SEC_CONSOLIDATION_SOURCE_A", "SEC_CONSOLIDATION_SOURCE_B"] "SEC_CONSOLIDATION_RESULT") + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_RESULT" "SEC_CONSOLIDATION_RESULT" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + +testStockConsolidationResultQuantityMustEqualSourceTotal = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONSOLIDATION_QUANTITY" "Consolidation Quantity Holder" + capTableOne <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONSOLIDATION_QUANTITY_SOURCE_A" "SEC_CONSOLIDATION_QUANTITY_SOURCE_A" stakeholderId stockClassId) with quantity = 50.0) + capTableTwo <- createStockIssuanceWith issuer capTableOne ((defaultStockIssuance "TX_CONSOLIDATION_QUANTITY_SOURCE_B" "SEC_CONSOLIDATION_QUANTITY_SOURCE_B" stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableTwo CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation (stockConsolidation "TX_CONSOLIDATION_QUANTITY" ["SEC_CONSOLIDATION_QUANTITY_SOURCE_A", "SEC_CONSOLIDATION_QUANTITY_SOURCE_B"] "SEC_CONSOLIDATION_QUANTITY_RESULT") + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_QUANTITY_RESULT" "SEC_CONSOLIDATION_QUANTITY_RESULT" stakeholderId stockClassId) with quantity = 10.0) + ] + edits = [] + deletes = [] + +testStockConsolidationResultDateMustMatchConsolidationDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONSOLIDATION_DATE" "Consolidation Date Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONSOLIDATION_DATE_SOURCE" "SEC_CONSOLIDATION_DATE_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation ((stockConsolidation "TX_CONSOLIDATION_DATE" ["SEC_CONSOLIDATION_DATE_SOURCE"] "SEC_CONSOLIDATION_DATE_RESULT") with date = consolidationDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_DATE_RESULT" "SEC_CONSOLIDATION_DATE_RESULT" stakeholderId stockClassId) with + date = resultDate + quantity = 100.0) + ] + edits = [] + deletes = [] + +testStockConsolidationSourcesMustMatchResultStockClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithSource, sourceStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_SOURCE_CLASS" 1_000.0) + (capTableWithResult, resultStockClassId) <- setupStockClassWith issuer capTableWithSource (limitedStockClass "SC_CONSOLIDATION_RESULT_CLASS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithResult "SH_CONSOLIDATION_CLASS" "Consolidation Class Holder" + capTableOne <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONSOLIDATION_CLASS_SOURCE_A" "SEC_CONSOLIDATION_CLASS_SOURCE_A" stakeholderId sourceStockClassId) with quantity = 25.0) + capTableTwo <- createStockIssuanceWith issuer capTableOne ((defaultStockIssuance "TX_CONSOLIDATION_CLASS_SOURCE_B" "SEC_CONSOLIDATION_CLASS_SOURCE_B" stakeholderId sourceStockClassId) with quantity = 25.0) + + submitMustFail issuer do + exerciseCmd capTableTwo CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation (stockConsolidation "TX_CONSOLIDATION_CLASS" ["SEC_CONSOLIDATION_CLASS_SOURCE_A", "SEC_CONSOLIDATION_CLASS_SOURCE_B"] "SEC_CONSOLIDATION_CLASS_RESULT") + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_CLASS_RESULT" "SEC_CONSOLIDATION_CLASS_RESULT" stakeholderId resultStockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockConsolidationSameTimestampSplitDoesNotResplitResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_SAME_TIMESTAMP" 50.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONSOLIDATION_SAME_TIMESTAMP" "Same Timestamp Consolidation Holder" + capTableOne <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_A" "SEC_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_A" stakeholderId stockClassId) with quantity = 10.0) + capTableTwo <- createStockIssuanceWith issuer capTableOne ((defaultStockIssuance "TX_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_B" "SEC_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_B" stakeholderId stockClassId) with quantity = 15.0) + let eventDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + + _ <- submit issuer do + exerciseCmd capTableTwo CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit ((stockClassSplit "TX_CONSOLIDATION_SAME_TIMESTAMP_SPLIT" stockClassId 2.0 1.0) with date = eventDate) + , CT.OcfCreateStockConsolidation ((stockConsolidation "TX_CONSOLIDATION_SAME_TIMESTAMP" ["SEC_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_A", "SEC_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_B"] "SEC_CONSOLIDATION_SAME_TIMESTAMP_RESULT") with date = eventDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_SAME_TIMESTAMP_RESULT" "SEC_CONSOLIDATION_SAME_TIMESTAMP_RESULT" stakeholderId stockClassId) with + date = eventDate + quantity = 50.0) + ] + edits = [] + deletes = [] + pure () + +testStockConversionReducesIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithDestination, destinationClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_DEST" 100.0) + let sourceClassId = "SC_CONVERSION" + let sourceClass = (limitedStockClass sourceClassId 100.0) with + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableWithDestination sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERSION" "Conversion Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_ISSUED" "SEC_CONVERSION_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_REDUCE" "SEC_CONVERSION_ISSUED" 50.0)] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_CONVERSION_REISSUE" "SEC_CONVERSION_REISSUE" stakeholderId stockClassId) with quantity = 50.0) + pure () + +testStockConversionCreditsDestinationClassForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_PREFERRED" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_DESTINATION" "Conversion Destination Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_DESTINATION_SOURCE" "SEC_CONVERSION_DESTINATION_SOURCE" stakeholderId preferredClassId) with quantity = 150.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_DESTINATION" "SEC_CONVERSION_DESTINATION_SOURCE" 150.0)] + edits = [] + deletes = [] + +testStockConversionMissingDestinationClassFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + let preferredClassId = "SC_CONVERSION_MISSING_DEST_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo "SC_CONVERSION_MISSING_DEST"] + (capTableWithPreferred, _) <- setupStockClassWith issuer cap_table preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MISSING_DEST" "Conversion Missing Destination Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_MISSING_DEST_SOURCE" "SEC_CONVERSION_MISSING_DEST_SOURCE" stakeholderId preferredClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_MISSING_DEST" "SEC_CONVERSION_MISSING_DEST_SOURCE" 50.0)] + edits = [] + deletes = [] + +testStockConversionMissingSourceIssuanceFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_MISSING_SOURCE_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_MISSING_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MISSING_SOURCE" "Conversion Missing Source Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_MISSING_SOURCE_ISSUED" "SEC_CONVERSION_MISSING_SOURCE" stakeholderId preferredClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_MISSING_SOURCE" "SEC_CONVERSION_MISSING_SOURCE" 50.0)] + edits = [] + deletes = [CT.OcfDeleteStockIssuance "TX_CONVERSION_MISSING_SOURCE_ISSUED"] + +testStockConversionWithPartialResultingIssuancesFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_PARTIAL_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_PARTIAL_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_PARTIAL" "Conversion Partial Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_PARTIAL_SOURCE" "SEC_CONVERSION_PARTIAL_SOURCE" stakeholderId preferredClassId) with quantity = 75.0) + let conversion = (stockConversion "TX_CONVERSION_PARTIAL" "SEC_CONVERSION_PARTIAL_SOURCE" 75.0) with + resulting_security_ids = ["SEC_CONVERSION_PARTIAL_RESULT", "SEC_CONVERSION_PARTIAL_MISSING"] + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONVERSION_PARTIAL_RESULT" "SEC_CONVERSION_PARTIAL_RESULT" stakeholderId commonClassId) with quantity = 75.0) + ] + edits = [] + deletes = [] + +testStockConversionResultStockClassMustMatchConversionRight = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithExpected, expectedClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RESULT_EXPECTED" 1_000.0) + (capTableWithWrong, wrongClassId) <- setupStockClassWith issuer capTableWithExpected (limitedStockClass "SC_CONVERSION_RESULT_WRONG" 1_000.0) + let sourceClassId = "SC_CONVERSION_RESULT_SOURCE" + let sourceClass = (limitedStockClass sourceClassId 1_000.0) with + conversion_rights = [stockClassConversionRightTo expectedClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithWrong sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_CONVERSION_RESULT_CLASS" "Conversion Result Class Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_RESULT_CLASS_SOURCE" "SEC_CONVERSION_RESULT_CLASS_SOURCE" stakeholderId sourceClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_RESULT_CLASS" "SEC_CONVERSION_RESULT_CLASS_SOURCE" 50.0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_RESULT_CLASS_OUTPUT" "TX_CONVERSION_RESULT_CLASS_RESULT" stakeholderId wrongClassId) with quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockConversionResultQuantityMustMatchConversionOutput = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithDestination, destinationClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RESULT_QUANTITY_DEST" 1_000.0) + let sourceClassId = "SC_CONVERSION_RESULT_QUANTITY_SOURCE" + let sourceClass = (limitedStockClass sourceClassId 1_000.0) with + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_CONVERSION_RESULT_QUANTITY" "Conversion Result Quantity Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_RESULT_QUANTITY_SOURCE" "SEC_CONVERSION_RESULT_QUANTITY_SOURCE" stakeholderId sourceClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_RESULT_QUANTITY" "SEC_CONVERSION_RESULT_QUANTITY_SOURCE" 50.0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_RESULT_QUANTITY_OUTPUT" "TX_CONVERSION_RESULT_QUANTITY_RESULT" stakeholderId destinationClassId) with quantity = 25.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockConversionResultDateMustMatchConversionDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithDestination, destinationClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RESULT_DATE_DEST" 1_000.0) + let sourceClassId = "SC_CONVERSION_RESULT_DATE_SOURCE" + let sourceClass = (limitedStockClass sourceClassId 1_000.0) with + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_CONVERSION_RESULT_DATE" "Conversion Result Date Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_RESULT_DATE_SOURCE" "SEC_CONVERSION_RESULT_DATE_SOURCE" stakeholderId sourceClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_RESULT_DATE" "SEC_CONVERSION_RESULT_DATE_SOURCE" 50.0 + let laterDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_RESULT_DATE_OUTPUT" "TX_CONVERSION_RESULT_DATE_RESULT" stakeholderId destinationClassId) with + date = laterDate + quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockConversionCannotReduceSecurityBelowZero = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithDestination, destinationClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_OVER_REDUCE_DEST" 200.0) + let stockClassData = (limitedStockClass "SC_CONVERSION_OVER_REDUCE" 100.0) with + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableWithDestination stockClassData + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERSION_OVER_REDUCE" "Conversion Over Reduce Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_OVER_REDUCE_ISSUED" "SEC_CONVERSION_OVER_REDUCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_OVER_REDUCE" "SEC_CONVERSION_OVER_REDUCE" 150.0)] + edits = [] + deletes = [] + +testStockConversionWithAmbiguousConversionRightsFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_AMBIG_COMMON" 100.0) + (capTableWithAlternative, alternativeClassId) <- setupStockClassWith issuer capTableWithCommon (limitedStockClass "SC_CONVERSION_AMBIG_ALT" 100.0) + let preferredClassId = "SC_CONVERSION_AMBIG_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 100.0) with + conversion_rights = + [ stockClassConversionRightTo commonClassId + , stockClassConversionRightTo alternativeClassId + ] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithAlternative preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_AMBIG" "Ambiguous Conversion Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_AMBIG_ISSUED" "SEC_CONVERSION_AMBIG" stakeholderId preferredClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_AMBIG" "SEC_CONVERSION_AMBIG" 50.0)] + edits = [] + deletes = [] + +testStockConversionWithMultipleRightsAcceptsMatchingExplicitResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirst, firstClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_MULTI_FIRST" 1_000.0) + (capTableWithSecond, secondClassId) <- setupStockClassWith issuer capTableWithFirst (limitedStockClass "SC_CONVERSION_MULTI_SECOND" 1_000.0) + let sourceClassId = "SC_CONVERSION_MULTI_SOURCE" + let sourceClass = (limitedStockClass sourceClassId 1_000.0) with + conversion_rights = + [ stockClassConversionRightTo firstClassId + , stockClassConversionRightTo secondClassId + ] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithSecond sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_CONVERSION_MULTI" "Conversion Multi Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_MULTI_SOURCE" "SEC_CONVERSION_MULTI_SOURCE" stakeholderId sourceClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_MULTI" "SEC_CONVERSION_MULTI_SOURCE" 50.0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_MULTI_OUTPUT" "TX_CONVERSION_MULTI_RESULT" stakeholderId firstClassId) with quantity = 50.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockConversionWithoutConversionRightsFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_NO_RIGHTS" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERSION_NO_RIGHTS" "Conversion No Rights Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_NO_RIGHTS_ISSUED" "SEC_CONVERSION_NO_RIGHTS" stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NO_RIGHTS" "SEC_CONVERSION_NO_RIGHTS" 50.0)] + edits = [] + deletes = [] + +testStockConversionUsesRatioAdjustmentForDestinationCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RATIO_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_RATIO_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_RATIO" "Conversion Ratio Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_RATIO_ISSUED" "SEC_CONVERSION_RATIO" stakeholderId preferredClassId) with quantity = 75.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_ADJUST" preferredClassId 2.0) + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_RATIO" "SEC_CONVERSION_RATIO" 75.0) + ] + edits = [] + deletes = [] + +testSameDateHigherStockConversionRatioAdjustmentWinsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RATIO_TIE_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_RATIO_TIE_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_RATIO_TIE" "Conversion Ratio Tie Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_RATIO_TIE_ISSUED" "SEC_CONVERSION_RATIO_TIE" stakeholderId preferredClassId) with quantity = 75.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_LOW" preferredClassId 1.0) + , CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_HIGH" preferredClassId 2.0) + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_RATIO_TIE" "SEC_CONVERSION_RATIO_TIE" 75.0) + ] + edits = [] + deletes = [] + +testStockConversionWithNonRatioRightFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_NON_RATIO_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_NON_RATIO_SOURCE" + let ppsRight = (stockClassConversionRightTo commonClassId) with + conversion_mechanism = OcfConversionMechanismPpsBasedConversion + ratio = None + reference_share_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [ppsRight] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_NON_RATIO" "Conversion Non Ratio Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_NON_RATIO_ISSUED" "SEC_CONVERSION_NON_RATIO" stakeholderId preferredClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NON_RATIO" "SEC_CONVERSION_NON_RATIO" 50.0)] + edits = [] + deletes = [] + +testStockConversionSkipsNonComputableRightWhenRatioRightMatchesResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_MIXED_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_MIXED_SOURCE" + let ppsRight = (stockClassConversionRightTo commonClassId) with + conversion_mechanism = OcfConversionMechanismPpsBasedConversion + ratio = None + reference_share_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + let preferredClass = (limitedStockClass preferredClassId 100.0) with + conversion_rights = [ppsRight, stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MIXED" "Conversion Mixed Rights Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_MIXED_SOURCE" "SEC_CONVERSION_MIXED_SOURCE" stakeholderId preferredClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_MIXED" "SEC_CONVERSION_MIXED_SOURCE" 50.0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_MIXED_RESULT" "TX_CONVERSION_MIXED_RESULT" stakeholderId commonClassId) with quantity = 50.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testFixedConvertibleConversionEnforcesCeilingWithoutMaterializedResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_FIXED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_FIXED" "Convertible Fixed Holder" + let excessiveIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_FIXED_EXCESS_SOURCE" + "CONV-CONVERTIBLE-FIXED-EXCESS" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 101.0)) + 1_000.0 + let excessiveConversion = convertibleConversionWith + "TX_CONVERTIBLE_FIXED_EXCESS" + excessiveIssuance.security_id + None + ["SEC-CONVERTIBLE-FIXED-EXCESS-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance excessiveIssuance + , CT.OcfCreateConvertibleConversion excessiveConversion + ] + edits = [] + deletes = [] + + let boundaryIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_FIXED_BOUNDARY_SOURCE" + "CONV-CONVERTIBLE-FIXED-BOUNDARY" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0)) + 1_000.0 + let boundaryConversion = convertibleConversionWith + "TX_CONVERTIBLE_FIXED_BOUNDARY" + boundaryIssuance.security_id + None + ["SEC-CONVERTIBLE-FIXED-BOUNDARY-RESULT"] + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance boundaryIssuance + , CT.OcfCreateConvertibleConversion boundaryConversion + ] + edits = [] + deletes = [] + pure () + +testPartialFixedConvertibleConversionScalesOutput = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_PARTIAL_FIXED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_PARTIAL_FIXED" "Convertible Partial Fixed Holder" + let excessiveIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_PARTIAL_FIXED_EXCESS_SOURCE" + "CONV-CONVERTIBLE-PARTIAL-FIXED-EXCESS" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 202.0)) + 200.0 + let excessiveConversion = (convertibleConversionWith + "TX_CONVERTIBLE_PARTIAL_FIXED_EXCESS" + excessiveIssuance.security_id + (Some 100.0) + ["SEC-CONVERTIBLE-PARTIAL-FIXED-EXCESS-RESULT"]) with + balance_security_id = Some "CONV-CONVERTIBLE-PARTIAL-FIXED-EXCESS-BALANCE" + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance excessiveIssuance + , CT.OcfCreateConvertibleConversion excessiveConversion + ] + edits = [] + deletes = [] + + let boundaryIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_PARTIAL_FIXED_BOUNDARY_SOURCE" + "CONV-CONVERTIBLE-PARTIAL-FIXED-BOUNDARY" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 200.0)) + 200.0 + let boundaryConversion = (convertibleConversionWith + "TX_CONVERTIBLE_PARTIAL_FIXED_BOUNDARY" + boundaryIssuance.security_id + (Some 100.0) + ["SEC-CONVERTIBLE-PARTIAL-FIXED-BOUNDARY-RESULT"]) with + balance_security_id = Some "CONV-CONVERTIBLE-PARTIAL-FIXED-BOUNDARY-BALANCE" + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance boundaryIssuance + , CT.OcfCreateConvertibleConversion boundaryConversion + ] + edits = [] + deletes = [] + pure () + +testConvertibleConversionValidatesConvertedUnitBounds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_UNIT_BOUNDS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_UNIT_BOUNDS" "Convertible Unit Bounds Holder" + let conversionMechanism = OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0) + let excessiveIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_UNIT_EXCESS_SOURCE" + "CONV-CONVERTIBLE-UNIT-EXCESS" + stakeholderId + (Some stockClassId) + conversionMechanism + 100.0 + let excessiveConversion = convertibleConversionWith + "TX_CONVERTIBLE_UNIT_EXCESS" + excessiveIssuance.security_id + (Some 101.0) + ["SEC-CONVERTIBLE-UNIT-EXCESS-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance excessiveIssuance + , CT.OcfCreateConvertibleConversion excessiveConversion + ] + edits = [] + deletes = [] + + let unknownPartialIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_UNIT_UNKNOWN_SOURCE" + "CONV-CONVERTIBLE-UNIT-UNKNOWN" + stakeholderId + (Some stockClassId) + conversionMechanism + 100.0 + let unknownPartialConversion = (convertibleConversionWith + "TX_CONVERTIBLE_UNIT_UNKNOWN" + unknownPartialIssuance.security_id + None + ["SEC-CONVERTIBLE-UNIT-UNKNOWN-RESULT"]) with + balance_security_id = Some "CONV-CONVERTIBLE-UNIT-UNKNOWN-BALANCE" + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance unknownPartialIssuance + , CT.OcfCreateConvertibleConversion unknownPartialConversion + ] + edits = [] + deletes = [] + +testRatioConvertibleConversionEnforcesCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_RATIO" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_RATIO" "Convertible Ratio Holder" + let ratio = OcfRatio with numerator = 2.0, denominator = 1.0 + let excessiveIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_RATIO_EXCESS_SOURCE" + "CONV-CONVERTIBLE-RATIO-EXCESS" + stakeholderId + (Some stockClassId) + (OcfConvMechRatio ratio) + 100.0 + let excessiveConversion = convertibleConversionWith + "TX_CONVERTIBLE_RATIO_EXCESS" + excessiveIssuance.security_id + (Some 51.0) + ["SEC-CONVERTIBLE-RATIO-EXCESS-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance excessiveIssuance + , CT.OcfCreateConvertibleConversion excessiveConversion + ] + edits = [] + deletes = [] + + let boundaryIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_RATIO_BOUNDARY_SOURCE" + "CONV-CONVERTIBLE-RATIO-BOUNDARY" + stakeholderId + (Some stockClassId) + (OcfConvMechRatio ratio) + 100.0 + let boundaryConversion = convertibleConversionWith + "TX_CONVERTIBLE_RATIO_BOUNDARY" + boundaryIssuance.security_id + (Some 50.0) + ["SEC-CONVERTIBLE-RATIO-BOUNDARY-RESULT"] + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance boundaryIssuance + , CT.OcfCreateConvertibleConversion boundaryConversion + ] + edits = [] + deletes = [] + pure () + +testIndeterminateConvertibleConversionRequiresMaterializedResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_CUSTOM" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_CUSTOM" "Convertible Custom Holder" + let customMechanism = OcfConvMechCustom (OcfCustomConversionMechanism with custom_conversion_description = "Custom negotiated conversion") + let unmaterializedIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_CUSTOM_MISSING_SOURCE" + "CONV-CONVERTIBLE-CUSTOM-MISSING" + stakeholderId + (Some stockClassId) + customMechanism + 100.0 + let unmaterializedConversion = convertibleConversionWith + "TX_CONVERTIBLE_CUSTOM_MISSING" + unmaterializedIssuance.security_id + None + ["SEC-CONVERTIBLE-CUSTOM-MISSING-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance unmaterializedIssuance + , CT.OcfCreateConvertibleConversion unmaterializedConversion + ] + edits = [] + deletes = [] + + let materializedIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_CUSTOM_RECORDED_SOURCE" + "CONV-CONVERTIBLE-CUSTOM-RECORDED" + stakeholderId + (Some stockClassId) + customMechanism + 100.0 + let resultSecurityId = "SEC-CONVERTIBLE-CUSTOM-RECORDED-RESULT" + let materializedConversion = convertibleConversionWith + "TX_CONVERTIBLE_CUSTOM_RECORDED" + materializedIssuance.security_id + None + [resultSecurityId] + let resultIssuance = (defaultStockIssuance + "TX_CONVERTIBLE_CUSTOM_RECORDED_RESULT" + resultSecurityId + stakeholderId + stockClassId) with quantity = 100.0 + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance materializedIssuance + , CT.OcfCreateConvertibleConversion materializedConversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testMaterializedConvertibleResultMustMatchDerivableTerms = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_RECORDED" 200.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_RECORDED" "Convertible Recorded Holder" + let conversionMechanism = OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0) + let sourceIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_RECORDED_SOURCE" + "CONV-CONVERTIBLE-RECORDED" + stakeholderId + (Some stockClassId) + conversionMechanism + 1_000.0 + let resultSecurityId = "SEC-CONVERTIBLE-RECORDED-RESULT" + let conversion = convertibleConversionWith + "TX_CONVERTIBLE_RECORDED" + sourceIssuance.security_id + None + [resultSecurityId] + let mismatchedResult = (defaultStockIssuance + "TX_CONVERTIBLE_RECORDED-MISMATCH" + resultSecurityId + stakeholderId + stockClassId) with quantity = 99.0 + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance sourceIssuance + , CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance mismatchedResult + ] + edits = [] + deletes = [] + + let matchingResult = mismatchedResult with + id = "TX_CONVERTIBLE_RECORDED-MATCH" + quantity = 100.0 + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance sourceIssuance + , CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance matchingResult + ] + edits = [] + deletes = [] + pure () + +testUnmaterializedConvertibleResultRequiresTargetClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_NO_TARGET" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_NO_TARGET" "Convertible No Target Holder" + let sourceIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_NO_TARGET_SOURCE" + "CONV-CONVERTIBLE-NO-TARGET" + stakeholderId + None + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0)) + 1_000.0 + let conversion = convertibleConversionWith + "TX_CONVERTIBLE_NO_TARGET" + sourceIssuance.security_id + None + ["SEC-CONVERTIBLE-NO-TARGET-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance sourceIssuance + , CT.OcfCreateConvertibleConversion conversion + ] + edits = [] + deletes = [] + +testConvertibleConversionRejectsPartialResultMaterialization = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_PARTIAL_RESULT" 200.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_PARTIAL_RESULT" "Convertible Partial Result Holder" + let sourceIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_PARTIAL_RESULT_SOURCE" + "CONV-CONVERTIBLE-PARTIAL-RESULT" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0)) + 1_000.0 + let firstResultSecurityId = "SEC-CONVERTIBLE-PARTIAL-RESULT-A" + let conversion = convertibleConversionWith + "TX_CONVERTIBLE_PARTIAL_RESULT" + sourceIssuance.security_id + None + [firstResultSecurityId, "SEC-CONVERTIBLE-PARTIAL-RESULT-B"] + let firstResult = (defaultStockIssuance + "TX_CONVERTIBLE_PARTIAL-RESULT-A" + firstResultSecurityId + stakeholderId + stockClassId) with quantity = 50.0 + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance sourceIssuance + , CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance firstResult + ] + edits = [] + deletes = [] + +testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_OUTPUT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_OUTPUT" "Warrant Output Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX_WARRANT_OUTPUT_ISSUED" "WSEC_OUTPUT" stakeholderId stockClassId 150.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX_WARRANT_OUTPUT_EXERCISE" "WSEC_OUTPUT" 150.0)] + edits = [] + deletes = [] + +testFixedWarrantPartialExerciseUsesExerciseQuantityForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_PARTIAL" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_PARTIAL" "Warrant Partial Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX_WARRANT_PARTIAL_ISSUED" "WSEC_PARTIAL" stakeholderId stockClassId 150.0)] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX_WARRANT_PARTIAL_EXERCISE" "WSEC_PARTIAL" 50.0)] + edits = [] + deletes = [] + pure () + +testFixedWarrantExerciseScalesDeclaredShareOutput = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_FIXED_SCALE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_FIXED_SCALE" "Warrant Fixed Scale Holder" + let securityId = "WSEC_FIXED_SCALE" + let issuance = (warrantIssuance "TX_WARRANT_FIXED_SCALE_ISSUED" securityId stakeholderId stockClassId 100.0) with + exercise_triggers = [warrantFixedExerciseTrigger "TRIGGER_FIXED_SCALE" stockClassId 1_000.0] + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let exerciseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let exercise = (warrantExercise "TX_WARRANT_FIXED_SCALE_EXERCISE" securityId 50.0) with + date = exerciseDate + trigger_id = "TRIGGER_FIXED_SCALE" + let resultIssuance = (defaultStockIssuance "TX_WARRANT_FIXED_SCALE_RESULT" "TX_WARRANT_FIXED_SCALE_EXERCISE_RESULT" stakeholderId stockClassId) with + date = exerciseDate + quantity = 500.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testFixedWarrantFullExerciseScalesToRemainingBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_FIXED_REMAINDER" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_FIXED_REMAINDER" "Warrant Fixed Remainder Holder" + let securityId = "WSEC_FIXED_REMAINDER" + let issuance = (warrantIssuance "TX_WARRANT_FIXED_REMAINDER_ISSUED" securityId stakeholderId stockClassId 100.0) with + exercise_triggers = [warrantFixedExerciseTrigger "TRIGGER_FIXED_REMAINDER" stockClassId 1_000.0] + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let partialDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let partialExercise = (warrantExercise "TX_WARRANT_FIXED_REMAINDER_PARTIAL" securityId 50.0) with + date = partialDate + trigger_id = "TRIGGER_FIXED_REMAINDER" + let partialResult = (defaultStockIssuance "TX_WARRANT_FIXED_REMAINDER_PARTIAL_RESULT" "TX_WARRANT_FIXED_REMAINDER_PARTIAL_RESULT" stakeholderId stockClassId) with + date = partialDate + quantity = 500.0 + partiallyExercised <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise partialExercise + , CT.OcfCreateStockIssuance partialResult + ] + edits = [] + deletes = [] + + let finalDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let finalExercise = (warrantExercise "TX_WARRANT_FIXED_REMAINDER_FINAL" securityId 0.0) with + date = finalDate + trigger_id = "TRIGGER_FIXED_REMAINDER" + quantity = None + let finalResult = (defaultStockIssuance "TX_WARRANT_FIXED_REMAINDER_FINAL_RESULT" "TX_WARRANT_FIXED_REMAINDER_FINAL_RESULT" stakeholderId stockClassId) with + date = finalDate + quantity = 500.0 + _ <- submit issuer do + exerciseCmd partiallyExercised.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise finalExercise + , CT.OcfCreateStockIssuance finalResult + ] + edits = [] + deletes = [] + pure () + +testWarrantExerciseWithUnknownOutputQuantityFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNKNOWN" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_UNKNOWN" "Warrant Unknown Holder" + let issuance = (warrantIssuance "TX_WARRANT_UNKNOWN_ISSUED" "WSEC_UNKNOWN" stakeholderId stockClassId 100.0) with + quantity = None + exercise_triggers = [warrantCustomExerciseTrigger "TRIGGER_CUSTOM" stockClassId] + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_UNKNOWN_EXERCISE" "WSEC_UNKNOWN" 50.0) with trigger_id = "TRIGGER_CUSTOM", quantity = None)] + edits = [] + deletes = [] + +testNonFixedWarrantExerciseRequiresResultingStockIssuanceForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_NON_FIXED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_NON_FIXED" "Warrant Non Fixed Holder" + let issuance = (warrantIssuance "TX_WARRANT_NON_FIXED_ISSUED" "WSEC_NON_FIXED" stakeholderId stockClassId 1.0) with + exercise_triggers = [warrantCustomExerciseTrigger "TRIGGER_CUSTOM_NON_FIXED" stockClassId] + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise + ((warrantExercise "TX_WARRANT_NON_FIXED_EXERCISE" "WSEC_NON_FIXED" 1.0) with + trigger_id = "TRIGGER_CUSTOM_NON_FIXED") + ] + edits = [] + deletes = [] + +testNonFixedWarrantExerciseUsesRecordedStockQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_NON_FIXED_RESULT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_NON_FIXED_RESULT" "Warrant Non Fixed Result Holder" + let securityId = "WSEC_NON_FIXED_RESULT" + let issuance = (warrantIssuance "TX_WARRANT_NON_FIXED_RESULT_ISSUED" securityId stakeholderId stockClassId 100.0) with + exercise_triggers = [warrantCustomExerciseTrigger "TRIGGER_NON_FIXED_RESULT" stockClassId] + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let exerciseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let exercise = (warrantExercise "TX_WARRANT_NON_FIXED_RESULT_EXERCISE" securityId 100.0) with + date = exerciseDate + trigger_id = "TRIGGER_NON_FIXED_RESULT" + let resultIssuance = (defaultStockIssuance "TX_WARRANT_NON_FIXED_RESULT" "TX_WARRANT_NON_FIXED_RESULT_EXERCISE_RESULT" stakeholderId stockClassId) with + date = exerciseDate + quantity = 25.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testWarrantExerciseWithUnspecifiedTargetClassUsesRecordedStock = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNSPECIFIED_TARGET" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_UNSPECIFIED_TARGET" "Warrant Unspecified Target Holder" + let securityId = "WSEC_UNSPECIFIED_TARGET" + let issuance = (warrantIssuance "TX_WARRANT_UNSPECIFIED_TARGET_ISSUED" securityId stakeholderId stockClassId 100.0) with + exercise_triggers = + [ warrantExerciseTriggerWith + "TRIGGER_UNSPECIFIED_TARGET" + (OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0) + None + ] + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let exerciseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let exercise = (warrantExercise "TX_WARRANT_UNSPECIFIED_TARGET_EXERCISE" securityId 100.0) with + date = exerciseDate + trigger_id = "TRIGGER_UNSPECIFIED_TARGET" + let resultIssuance = (defaultStockIssuance "TX_WARRANT_UNSPECIFIED_TARGET_RESULT" "TX_WARRANT_UNSPECIFIED_TARGET_EXERCISE_RESULT" stakeholderId stockClassId) with + date = exerciseDate + quantity = 100.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testWarrantExerciseResultStockClassMustMatchTrigger = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithExpected, expectedClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_RESULT_EXPECTED" 1_000.0) + (capTableWithWrong, wrongClassId) <- setupStockClassWith issuer capTableWithExpected (limitedStockClass "SC_WARRANT_RESULT_WRONG" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithWrong "SH_WARRANT_RESULT_CLASS" "Warrant Result Class Holder" + let issuance = (warrantIssuance "TX_WARRANT_RESULT_CLASS_ISSUED" "WSEC_RESULT_CLASS" stakeholderId expectedClassId 100.0) with + quantity = None + exercise_triggers = [warrantCustomExerciseTrigger "TRIGGER_RESULT_CLASS" expectedClassId] + issuanceResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let exercise = (warrantExercise "TX_WARRANT_RESULT_CLASS_EXERCISE" "WSEC_RESULT_CLASS" 50.0) with + trigger_id = "TRIGGER_RESULT_CLASS" + quantity = None + let resultIssuance = (defaultStockIssuance "TX_WARRANT_RESULT_CLASS_OUTPUT" "TX_WARRANT_RESULT_CLASS_EXERCISE_RESULT" stakeholderId wrongClassId) with quantity = 50.0 + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testWarrantExerciseWithUnknownTriggerFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_MISSING_TRIGGER" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_MISSING_TRIGGER" "Warrant Missing Trigger Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX_WARRANT_MISSING_TRIGGER_ISSUED" "WSEC_MISSING_TRIGGER" stakeholderId stockClassId 50.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_MISSING_TRIGGER_EXERCISE" "WSEC_MISSING_TRIGGER" 50.0) with trigger_id = "TRIGGER_DOES_NOT_EXIST")] + edits = [] + deletes = [] + +testEquityCompensationExerciseCannotExceedStockClassAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_OUTPUT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_OUTPUT" "Equity Comp Output Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_GRANTED" "ECSEC_OUTPUT" stakeholderId stockClassId 150.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_EQUITY_COMP_EXERCISE" "ECSEC_OUTPUT" 150.0)] + edits = [] + deletes = [] + +testClasslessEquityCompensationExerciseUsesSinglePlanClassForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_EQUITY_OUTPUT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CLASSLESS_EQUITY_OUTPUT" "Classless Equity Output Holder" + let stockPlanId = "PLAN_CLASSLESS_EQUITY_OUTPUT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = (equityCompensationIssuance "TX_CLASSLESS_EQUITY_GRANTED" "ECSEC_CLASSLESS_OUTPUT" stakeholderId stockClassId 150.0) with + stock_class_id = None + stock_plan_id = Some stockPlanId + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CLASSLESS_EQUITY_EXERCISE" "ECSEC_CLASSLESS_OUTPUT" 150.0) with resulting_security_ids = [])] + edits = [] + deletes = [] + +testClasslessEquityCompensationMaterializedExerciseMustUseSinglePlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithExpected, expectedStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_MATERIALIZED_EXERCISE" 1_000.0) + (capTableWithWrong, wrongStockClassId) <- setupStockClassWith issuer capTableWithExpected (limitedStockClass "SC_CLASSLESS_MATERIALIZED_EXERCISE_WRONG" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithWrong "SH_CLASSLESS_MATERIALIZED_EXERCISE" "Classless Materialized Exercise Holder" + let stockPlanId = "PLAN_CLASSLESS_MATERIALIZED_EXERCISE" + let securityId = "ECSEC_CLASSLESS_MATERIALIZED_EXERCISE" + let plan = (defaultStockPlan stockPlanId expectedStockClassId) with initial_shares_reserved = 100.0 + let issuance = (equityCompensationIssuance "TX_CLASSLESS_MATERIALIZED_EXERCISE_GRANTED" securityId stakeholderId expectedStockClassId 50.0) with + stock_class_id = None + stock_plan_id = Some stockPlanId + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let wrongExercise = equityCompensationExercise "TX_CLASSLESS_MATERIALIZED_EXERCISE_WRONG" securityId 50.0 + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise wrongExercise + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CLASSLESS_MATERIALIZED_EXERCISE_WRONG_RESULT" "TX_CLASSLESS_MATERIALIZED_EXERCISE_WRONG_RESULT" stakeholderId wrongStockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + + let validExercise = equityCompensationExercise "TX_CLASSLESS_MATERIALIZED_EXERCISE_VALID" securityId 50.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise validExercise + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CLASSLESS_MATERIALIZED_EXERCISE_VALID_RESULT" "TX_CLASSLESS_MATERIALIZED_EXERCISE_VALID_RESULT" stakeholderId expectedStockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + pure () + +testClasslessEquityCompensationExerciseRejectsAmbiguousPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_EQUITY_FIRST" 1_000.0) + (capTableWithSecondClass, secondStockClassId) <- setupStockClassWith issuer capTableWithFirstClass (limitedStockClass "SC_CLASSLESS_EQUITY_SECOND" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSecondClass "SH_CLASSLESS_EQUITY_AMBIGUOUS" "Ambiguous Classless Equity Holder" + let stockPlanId = "PLAN_CLASSLESS_EQUITY_AMBIGUOUS" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = (equityCompensationIssuance "TX_CLASSLESS_EQUITY_AMBIGUOUS_GRANTED" "ECSEC_CLASSLESS_AMBIGUOUS" stakeholderId firstStockClassId 50.0) with + stock_class_id = None + stock_plan_id = Some stockPlanId + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CLASSLESS_EQUITY_AMBIGUOUS_EXERCISE" "ECSEC_CLASSLESS_AMBIGUOUS" 50.0) with resulting_security_ids = [])] + edits = [] + deletes = [] + +testClasslessCashSettledSarExerciseAllowsNoStockOutput = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CASH_SETTLED_SAR" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CASH_SETTLED_SAR" "Cash Settled SAR Holder" + let issuance = (equityCompensationIssuance "TX_CASH_SETTLED_SAR_GRANTED" "ECSEC_CASH_SETTLED_SAR" stakeholderId stockClassId 50.0) with + compensation_type = OcfCompensationTypeCSAR + base_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + stock_class_id = None + stock_plan_id = None + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + + let stockExercise = equityCompensationExercise "TX_CASH_SETTLED_SAR_STOCK_EXERCISE" "ECSEC_CASH_SETTLED_SAR" 50.0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise stockExercise + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CASH_SETTLED_SAR_STOCK_RESULT" "TX_CASH_SETTLED_SAR_STOCK_EXERCISE_RESULT" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CASH_SETTLED_SAR_EXERCISE" "ECSEC_CASH_SETTLED_SAR" 50.0) with resulting_security_ids = [])] + edits = [] + deletes = [] + pure () + +testNegativeCashSettledSarExerciseCannotIncreaseGrantBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CASH_SETTLED_SAR_NEGATIVE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CASH_SETTLED_SAR_NEGATIVE" "Cash Settled SAR Negative Holder" + let issuance = (equityCompensationIssuance "TX_CASH_SETTLED_SAR_NEGATIVE_GRANTED" "ECSEC_CASH_SETTLED_SAR_NEGATIVE" stakeholderId stockClassId 50.0) with + compensation_type = OcfCompensationTypeCSAR + base_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + stock_class_id = None + stock_plan_id = None + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CASH_SETTLED_SAR_NEGATIVE_EXERCISE" "ECSEC_CASH_SETTLED_SAR_NEGATIVE" (-1.0)) with resulting_security_ids = [])] + edits = [] + deletes = [] + +testClasslessEquityCompensationReleaseUsesSinglePlanClassForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_EQUITY_RELEASE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CLASSLESS_EQUITY_RELEASE" "Classless Equity Release Holder" + let stockPlanId = "PLAN_CLASSLESS_EQUITY_RELEASE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = (equityCompensationIssuance "TX_CLASSLESS_EQUITY_RELEASE_GRANTED" "ECSEC_CLASSLESS_RELEASE" stakeholderId stockClassId 150.0) with + compensation_type = OcfCompensationTypeRSU + exercise_price = None + stock_class_id = None + stock_plan_id = Some stockPlanId + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_CLASSLESS_EQUITY_RELEASE" "ECSEC_CLASSLESS_RELEASE" releaseDate settlementDate 150.0)] + edits = [] + deletes = [] + +testClasslessEquityCompensationMaterializedReleaseMustUseSinglePlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithExpected, expectedStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_MATERIALIZED_RELEASE" 1_000.0) + (capTableWithWrong, wrongStockClassId) <- setupStockClassWith issuer capTableWithExpected (limitedStockClass "SC_CLASSLESS_MATERIALIZED_RELEASE_WRONG" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithWrong "SH_CLASSLESS_MATERIALIZED_RELEASE" "Classless Materialized Release Holder" + let stockPlanId = "PLAN_CLASSLESS_MATERIALIZED_RELEASE" + let securityId = "ECSEC_CLASSLESS_MATERIALIZED_RELEASE" + let plan = (defaultStockPlan stockPlanId expectedStockClassId) with initial_shares_reserved = 100.0 + let issuance = (equityCompensationIssuance "TX_CLASSLESS_MATERIALIZED_RELEASE_GRANTED" securityId stakeholderId expectedStockClassId 50.0) with + compensation_type = OcfCompensationTypeRSU + exercise_price = None + stock_class_id = None + stock_plan_id = Some stockPlanId + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let wrongRelease = equityCompensationRelease "TX_CLASSLESS_MATERIALIZED_RELEASE_WRONG" securityId releaseDate settlementDate 50.0 + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationRelease wrongRelease + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CLASSLESS_MATERIALIZED_RELEASE_WRONG_RESULT" "TX_CLASSLESS_MATERIALIZED_RELEASE_WRONG_RESULT" stakeholderId wrongStockClassId) with + date = settlementDate + quantity = 50.0) + ] + edits = [] + deletes = [] + + let validRelease = equityCompensationRelease "TX_CLASSLESS_MATERIALIZED_RELEASE_VALID" securityId releaseDate settlementDate 50.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationRelease validRelease + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CLASSLESS_MATERIALIZED_RELEASE_VALID_RESULT" "TX_CLASSLESS_MATERIALIZED_RELEASE_VALID_RESULT" stakeholderId expectedStockClassId) with + date = settlementDate + quantity = 50.0) + ] + edits = [] + deletes = [] + pure () + +testClasslessEquityCompensationReleaseRejectsAmbiguousPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_RELEASE_FIRST" 1_000.0) + (capTableWithSecondClass, secondStockClassId) <- setupStockClassWith issuer capTableWithFirstClass (limitedStockClass "SC_CLASSLESS_RELEASE_SECOND" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSecondClass "SH_CLASSLESS_RELEASE_AMBIGUOUS" "Ambiguous Classless Release Holder" + let stockPlanId = "PLAN_CLASSLESS_RELEASE_AMBIGUOUS" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = (equityCompensationIssuance "TX_CLASSLESS_RELEASE_AMBIGUOUS_GRANTED" "ECSEC_CLASSLESS_RELEASE_AMBIGUOUS" stakeholderId firstStockClassId 50.0) with + compensation_type = OcfCompensationTypeRSU + exercise_price = None + stock_class_id = None + stock_plan_id = Some stockPlanId + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_CLASSLESS_RELEASE_AMBIGUOUS" "ECSEC_CLASSLESS_RELEASE_AMBIGUOUS" releaseDate settlementDate 50.0)] + edits = [] + deletes = [] + +testNegativeEquityCompensationExerciseCannotOffsetIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_NEGATIVE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_NEGATIVE" "Equity Comp Negative Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_NEGATIVE_GRANTED" "ECSEC_NEGATIVE" stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_EQUITY_COMP_NEGATIVE_EXERCISE" "ECSEC_NEGATIVE" (-50.0))] + edits = [] + deletes = [] + +testEquityCompensationExerciseWithResultingStockIssuanceDoesNotDoubleCount = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_RESULTING" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_RESULTING" "Equity Comp Resulting Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_RESULTING_GRANTED" "ECSEC_RESULTING" stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + let exercise = equityCompensationExercise "TX_EQUITY_COMP_RESULTING_EXERCISE" "ECSEC_RESULTING" 100.0 + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise exercise + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_EQUITY_COMP_RESULTING_STOCK" "TX_EQUITY_COMP_RESULTING_EXERCISE_RESULT" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + +testEquityCompensationExerciseResultQuantityMustMatchExercise = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_RESULT_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_RESULT_QUANTITY" "Equity Comp Result Quantity Holder" + issuanceResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_RESULT_QUANTITY_GRANTED" "ECSEC_RESULT_QUANTITY" stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + let exercise = equityCompensationExercise "TX_EQUITY_COMP_RESULT_QUANTITY_EXERCISE" "ECSEC_RESULT_QUANTITY" 50.0 + let resultIssuance = (defaultStockIssuance "TX_EQUITY_COMP_RESULT_QUANTITY_OUTPUT" "TX_EQUITY_COMP_RESULT_QUANTITY_EXERCISE_RESULT" stakeholderId stockClassId) with quantity = 25.0 + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testEquityCompensationReleaseUsesSettlementDateForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_RELEASE_DATE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_RELEASE_DATE" "Equity Comp Release Date Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_RELEASE_GRANTED" "ECSEC_RELEASE_DATE" stakeholderId stockClassId 150.0)] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment ((stockClassAuthorizedSharesAdjustment "TX_EQUITY_COMP_RELEASE_AUTH" stockClassId 150.0) with date = settlementDate) + , CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_EQUITY_COMP_RELEASE" "ECSEC_RELEASE_DATE" releaseDate settlementDate 150.0) + ] + edits = [] + deletes = [] + pure () + +testEquityCompensationReleaseResultDateMustMatchSettlementDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_RESULT_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_RESULT_DATE" "Equity Comp Result Date Holder" + issuanceResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_RESULT_DATE_GRANTED" "ECSEC_RESULT_DATE" stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let laterDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let release = equityCompensationRelease "TX_EQUITY_COMP_RESULT_DATE_RELEASE" "ECSEC_RESULT_DATE" releaseDate settlementDate 50.0 + let resultIssuance = (defaultStockIssuance "TX_EQUITY_COMP_RESULT_DATE_OUTPUT" "TX_EQUITY_COMP_RESULT_DATE_RELEASE_RESULT" stakeholderId stockClassId) with + date = laterDate + quantity = 50.0 + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationRelease release + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockRetractionReducesIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_RETRACTION" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_RETRACTION" "Retraction Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_RETRACTION_ISSUED" "SEC_RETRACTION_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockRetraction (stockRetraction "TX_RETRACTION_REDUCE" "SEC_RETRACTION_ISSUED")] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_RETRACTION_REISSUE" "SEC_RETRACTION_REISSUE" stakeholderId stockClassId) with quantity = 100.0) + pure () + +testStockClassAuthorizedSharesAdjustmentCreatedWithIssuanceEditInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_AND_ADJUST" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_AND_ADJUST" "Edit And Adjust Holder" + let originalIssuance = (defaultStockIssuance "TX_EDIT_AND_ADJUST" "SEC_EDIT_AND_ADJUST" stakeholderId stockClassId) with quantity = 100.0 + capTableIssued <- createStockIssuanceWith issuer capTableReady originalIssuance + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_FOR_EDIT" stockClassId 150.0)] + edits = [CT.OcfEditStockIssuance (originalIssuance with quantity = 125.0)] + deletes = [] + pure () + +testUnlimitedStockClassAuthorizedSharesAllowsLargeIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + issuerResult <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" None)] + deletes = [] + + let unlimitedClass = (defaultStockClass "SC_UNLIMITED") with + initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesUnlimited + (capTableWithClass, stockClassId) <- setupStockClassWith issuer issuerResult.updatedCapTableCid unlimitedClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNLIMITED" "Unlimited Holder" + + _ <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_UNLIMITED" "SEC_UNLIMITED" stakeholderId stockClassId) with quantity = 2_000_000.0) + pure () + +testBalanceSecurityReplacementPreservesAuthorizedShareHeadroom = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_SUCCESSOR" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_SUCCESSOR" "Balance Successor Holder" + let sourceSecurityId = "SEC_BALANCE_SUCCESSOR_SOURCE" + let balanceSecurityId = "SEC_BALANCE_SUCCESSOR_REMAINDER" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_BALANCE_SUCCESSOR_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + let cancellation = (stockCancellation "TX_BALANCE_SUCCESSOR_CANCEL" sourceSecurityId 50.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = (defaultStockIssuance "TX_BALANCE_SUCCESSOR_REMAINDER" balanceSecurityId stakeholderId stockClassId) with quantity = 50.0 + let replacementIssuance = (defaultStockIssuance "TX_BALANCE_SUCCESSOR_REPLACEMENT" "SEC_BALANCE_SUCCESSOR_REPLACEMENT" stakeholderId stockClassId) with quantity = 50.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance balanceIssuance + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testBalanceSecurityQuantityMustMatchSourceRemainder = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_QUANTITY" "Balance Quantity Holder" + let sourceSecurityId = "SEC_BALANCE_QUANTITY_SOURCE" + let balanceSecurityId = "SEC_BALANCE_QUANTITY_RESULT" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_BALANCE_QUANTITY_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + let cancellation = (stockCancellation "TX_BALANCE_QUANTITY_CANCEL" sourceSecurityId 50.0) with + balance_security_id = Some balanceSecurityId + let wrongBalance = (defaultStockIssuance "TX_BALANCE_QUANTITY_RESULT" balanceSecurityId stakeholderId stockClassId) with quantity = 40.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance wrongBalance + ] + edits = [] + deletes = [] + +testBalanceSecurityStockClassMustMatchSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithSource, sourceClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_CLASS_SOURCE" 1_000.0) + (capTableWithClasses, wrongClassId) <- setupStockClassWith issuer capTableWithSource (limitedStockClass "SC_BALANCE_CLASS_WRONG" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_BALANCE_CLASS" "Balance Class Holder" + let sourceSecurityId = "SEC_BALANCE_CLASS_SOURCE" + let balanceSecurityId = "SEC_BALANCE_CLASS_RESULT" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_BALANCE_CLASS_SOURCE" sourceSecurityId stakeholderId sourceClassId) with quantity = 100.0) + let cancellation = (stockCancellation "TX_BALANCE_CLASS_CANCEL" sourceSecurityId 50.0) with + balance_security_id = Some balanceSecurityId + let wrongBalance = (defaultStockIssuance "TX_BALANCE_CLASS_RESULT" balanceSecurityId stakeholderId wrongClassId) with quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance wrongBalance + ] + edits = [] + deletes = [] + +testBalanceSecurityDateMustMatchReductionDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_DATE" "Balance Date Holder" + let sourceSecurityId = "SEC_BALANCE_DATE_SOURCE" + let balanceSecurityId = "SEC_BALANCE_DATE_RESULT" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_BALANCE_DATE_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + let cancellation = (stockCancellation "TX_BALANCE_DATE_CANCEL" sourceSecurityId 50.0) with + balance_security_id = Some balanceSecurityId + let laterDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let lateBalance = (defaultStockIssuance "TX_BALANCE_DATE_RESULT" balanceSecurityId stakeholderId stockClassId) with + date = laterDate + quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance lateBalance + ] + edits = [] + deletes = [] + +testStockTransferPartialResultingIssuanceCoverageFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_PARTIAL_RESULTS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_PARTIAL_RESULTS" "Partial Transfer Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_TRANSFER_PARTIAL_SOURCE" "SEC_TRANSFER_PARTIAL_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_PARTIAL_RESULTS" "SEC_TRANSFER_PARTIAL_SOURCE" 100.0 ["SEC_TRANSFER_PARTIAL_PRESENT", "SEC_TRANSFER_PARTIAL_MISSING"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_PARTIAL_PRESENT" "SEC_TRANSFER_PARTIAL_PRESENT" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockReissuancePartialResultingIssuanceCoverageFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_PARTIAL_RESULTS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_PARTIAL_RESULTS" "Partial Reissuance Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUE_PARTIAL_SOURCE" "SEC_REISSUE_PARTIAL_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUE_PARTIAL_RESULTS" "SEC_REISSUE_PARTIAL_SOURCE" ["SEC_REISSUE_PARTIAL_PRESENT", "SEC_REISSUE_PARTIAL_MISSING"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUE_PARTIAL_PRESENT" "SEC_REISSUE_PARTIAL_PRESENT" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockConsolidation.daml b/Test/daml/OpenCapTable/TestStockConsolidation.daml index 4598bf2c..dfce1464 100644 --- a/Test/daml/OpenCapTable/TestStockConsolidation.daml +++ b/Test/daml/OpenCapTable/TestStockConsolidation.daml @@ -59,6 +59,23 @@ testStockConsolidation_TwoSecurities = script do deletes = [] pure () +testStockConsolidation_DuplicateSecurityIdsRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + result <- createStockIssuance issuer capTableCid "SSEC-DUP-CONSOLIDATE" 100.0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockConsolidation StockConsolidationOcfData with + id = "TX_STOCK_CONSOLIDATION_DUP" + date = DT.time (DA.date 2025 Apr 15) 0 0 0 + security_ids = ["SSEC-DUP-CONSOLIDATE", "SSEC-DUP-CONSOLIDATE"] + resulting_security_id = "SSEC-DUP-CONSOLIDATED" + comments = [] + reason_text = Some "Duplicate source security IDs are invalid"] + edits = [] + deletes = [] + pure () + -- Test 2: Consolidation of multiple securities with comments testStockConsolidation_MultipleSecurities = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockConversion.daml b/Test/daml/OpenCapTable/TestStockConversion.daml index ec29ea17..98dcca7b 100644 --- a/Test/daml/OpenCapTable/TestStockConversion.daml +++ b/Test/daml/OpenCapTable/TestStockConversion.daml @@ -3,14 +3,52 @@ module OpenCapTable.TestStockConversion where import Fairmint.OpenCapTable.OCF.StockIssuance import Fairmint.OpenCapTable.OCF.StockConversion import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..), OcfStockClassType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockClass, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +stockClassConversionRightTo : Text -> OcfStockClassConversionRight +stockClassConversionRightTo stockClassId = + OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = None + conversion_trigger = ocfTriggerElectiveAtWill + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = None + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None + +addPreferredStockClass : Party -> ContractId CT.CapTable -> Script (ContractId CT.CapTable) +addPreferredStockClass issuer capTableCid = do + result <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass ((defaultStockClass "SC_PREF") with + class_type = OcfStockClassTypePreferred + default_id_prefix = "PS-" + seniority = 2.0 + conversion_rights = [stockClassConversionRightTo "SC_COMMON"]) + ] + edits = [] + deletes = [] + pure result.updatedCapTableCid + -- Create a stock issuance to convert (follows real workflow) createStockIssuance issuer capTableCid securityId quantity = submit issuer do @@ -25,7 +63,7 @@ createStockIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_class_id = "SC_COMMON" + stock_class_id = "SC_PREF" share_price = OcfMonetary with amount = 1.0, currency = "USD" quantity = quantity cost_basis = None @@ -44,6 +82,7 @@ createStockIssuance issuer capTableCid securityId quantity = testStockConversion_PreferredToCommon = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addPreferredStockClass issuer capTableCid let securityId = "SSEC-PREF-001" result <- createStockIssuance issuer capTableCid securityId 100000.0 _ <- submit issuer do @@ -64,18 +103,26 @@ testStockConversion_PreferredToCommon = script do testStockConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addPreferredStockClass issuer capTableCid let securityId = "SSEC-PREF-002" result <- createStockIssuance issuer capTableCid securityId 50000.0 + let conversionDate = DT.time (DA.date 2025 Nov 01) 0 0 0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_CONVERSION_2_BALANCE" "SSEC-PREF-002-REMAIN" "SH-1" "SC_PREF") with + date = conversionDate + quantity = 20000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion StockConversionOcfData with - id = "TX_STOCK_CONVERSION_2" - date = DT.time (DA.date 2025 Nov 01) 0 0 0 - security_id = securityId - quantity_converted = 30000.0 - resulting_security_ids = ["SSEC-COMMON-002"] - comments = ["Elective partial conversion by investor"] - balance_security_id = Some "SSEC-PREF-002-REMAIN"] + creates = + [ CT.OcfCreateStockConversion StockConversionOcfData with + id = "TX_STOCK_CONVERSION_2" + date = conversionDate + security_id = securityId + quantity_converted = 30000.0 + resulting_security_ids = ["SSEC-COMMON-002"] + comments = ["Elective partial conversion by investor"] + balance_security_id = Some balanceIssuance.security_id + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () @@ -84,6 +131,7 @@ testStockConversion_PartialConversion = script do testStockConversion_MultipleResulting = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addPreferredStockClass issuer capTableCid let securityId = "SSEC-PREF-003" result <- createStockIssuance issuer capTableCid securityId 200000.0 _ <- submit issuer do diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index cfa5f86a..4779e0af 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -42,7 +42,7 @@ testCreateAndDeleteStockIssuance = script do deletes = [CT.OcfDeleteStockIssuance "TX_STOCK_ISSUANCE_1"] pure () --- Test creating a stock issuance with all optional fields populated +-- Test creating a stock issuance with optional fields that do not require a stock plan populated testStockIssuance_OptionalsSome = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -50,7 +50,7 @@ testStockIssuance_OptionalsSome = script do (cap_table', classId) <- setupStockClass issuer cap_table (cap_table'', stakeholderId) <- setupStakeholderWithId issuer cap_table' "SH-3" "Test Holder" - -- Create Stock Issuance with all optional fields populated + -- Keep stock_plan_id empty here; plan-backed stock issuance is covered by stock-plan tests. let issuance = (defaultStockIssuance "TX_STOCK_ISS_SOME" "SEC-3" stakeholderId classId) with custom_id = "CERT-3" date = DT.time (DA.date 2024 Feb 01) 0 0 0 @@ -58,7 +58,7 @@ testStockIssuance_OptionalsSome = script do stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 16) 0 0 0) consideration_text = Some "Cash consideration" security_law_exemptions = [OcfSecurityExemption with description = "Rule 506(b)", jurisdiction = "US-CA"] - stock_plan_id = Some "PLAN_1" + stock_plan_id = None share_numbers_issued = [OcfShareNumberRange with starting_share_number = 1.0, ending_share_number = 10.0] share_price = OcfMonetary with amount = 2.5, currency = "USD" quantity = 10.0 diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml new file mode 100644 index 00000000..59d27a90 --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -0,0 +1,2757 @@ +module OpenCapTable.TestStockPlanReservedShares where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise (EquityCompensationExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationRetraction (EquityCompensationRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConsolidation (StockConsolidationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConversion (StockConversionOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfConversionMechanism(..), OcfStockClassConversionRight(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..)) +import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +stockPlanPoolAdjustment : Text -> Text -> Decimal -> StockPlanPoolAdjustmentOcfData +stockPlanPoolAdjustment adjustmentId stockPlanId sharesReserved = StockPlanPoolAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_plan_id = stockPlanId + shares_reserved = sharesReserved + board_approval_date = None + stockholder_approval_date = None + comments = [] + +stockPlanReturnToPool : Text -> Text -> Text -> Decimal -> StockPlanReturnToPoolOcfData +stockPlanReturnToPool returnId securityId stockPlanId quantity = StockPlanReturnToPoolOcfData with + id = returnId + date = defaultTestDate + security_id = securityId + stock_plan_id = stockPlanId + quantity = quantity + reason_text = "Returned to pool" + comments = [] + +equityCompensationCancellation : Text -> Text -> Decimal -> EquityCompensationCancellationOcfData +equityCompensationCancellation cancellationId securityId quantity = EquityCompensationCancellationOcfData with + id = cancellationId + date = defaultTestDate + security_id = securityId + quantity = quantity + balance_security_id = None + reason_text = "Cancelled equity compensation" + comments = [] + +planEquityCompensationExercise : Text -> Text -> Decimal -> EquityCompensationExerciseOcfData +planEquityCompensationExercise exerciseId securityId quantity = EquityCompensationExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = [] + consideration_text = None + comments = [] + +planStockTransfer : Text -> Text -> Text -> Decimal -> StockTransferOcfData +planStockTransfer transferId securityId resultingSecurityId quantity = StockTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + +planStockConversion : Text -> Text -> Text -> Time -> Decimal -> StockConversionOcfData +planStockConversion conversionId securityId resultingSecurityId conversionDate quantity = StockConversionOcfData with + id = conversionId + date = conversionDate + security_id = securityId + quantity_converted = quantity + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + comments = [] + +stockClassConversionRightTo : Text -> OcfStockClassConversionRight +stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = None + conversion_trigger = ocfTriggerElectiveAtWill + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = None + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None + +planStockConsolidation : Text -> [Text] -> Text -> Time -> StockConsolidationOcfData +planStockConsolidation consolidationId securityIds resultingSecurityId consolidationDate = StockConsolidationOcfData with + id = consolidationId + date = consolidationDate + security_ids = securityIds + resulting_security_id = resultingSecurityId + reason_text = Some "Consolidated plan-backed stock" + comments = [] + +planStockRetraction : Text -> Text -> Time -> StockRetractionOcfData +planStockRetraction retractionId securityId retractionDate = StockRetractionOcfData with + id = retractionId + date = retractionDate + security_id = securityId + reason_text = "Retracted plan-backed stock issuance" + comments = [] + +planStockReissuance : Text -> Text -> Text -> StockReissuanceOcfData +planStockReissuance reissuanceId securityId resultingSecurityId = StockReissuanceOcfData with + id = reissuanceId + date = defaultTestDate + security_id = securityId + resulting_security_ids = [resultingSecurityId] + split_transaction_id = None + reason_text = Some "Replacement certificate" + comments = [] + +planEquityCompensationTransfer : Text -> Text -> Text -> Decimal -> EquityCompensationTransferOcfData +planEquityCompensationTransfer transferId securityId resultingSecurityId quantity = EquityCompensationTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + +createPlanAndStockIssuance : + Party -> + ContractId CT.CapTable -> + Text -> + Decimal -> + Decimal -> + Script (ContractId CT.CapTable, Text, Text) +createPlanAndStockIssuance issuer capTableCid stockPlanId reservedShares issuedShares = do + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer capTableCid + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = reservedShares + let issuance = (defaultStockIssuance ("TX_" <> stockPlanId) ("SEC-" <> stockPlanId) stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = issuedShares + + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + pure (result.updatedCapTableCid, stockClassId, stakeholderId) + +setupPlanBackedConversion : + Party -> + ContractId CT.CapTable -> + Text -> + Decimal -> + Script (ContractId CT.CapTable, Text, Text, Text, Text) +setupPlanBackedConversion issuer capTableCid suffix reservedShares = do + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_" <> suffix + let destinationStockClassId = "SC_PLAN_CONVERSION_DESTINATION_" <> suffix + let stockPlanId = "PLAN_CONVERSION_" <> suffix + let sourceSecurityId = "SEC_PLAN_CONVERSION_SOURCE_" <> suffix + let resultSecurityId = "SEC_PLAN_CONVERSION_RESULT_" <> suffix + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer capTableCid (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource ("SH_PLAN_CONVERSION_" <> suffix) ("Plan Conversion Holder " <> suffix) + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = reservedShares + stock_class_ids = [sourceStockClassId, destinationStockClassId] + let sourceIssuance = (defaultStockIssuance ("TX_PLAN_CONVERSION_SOURCE_" <> suffix) sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = (defaultStockIssuance ("TX_PLAN_CONVERSION_RESULT_" <> suffix) resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 100.0 + converted <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion (planStockConversion ("TX_PLAN_CONVERSION_" <> suffix) sourceSecurityId resultSecurityId conversionDate 100.0) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure (converted.updatedCapTableCid, stockPlanId, sourceSecurityId, resultSecurityId, stakeholderId) + +testStockPlanReservedShares_StockIssuanceCannotExceedInitialPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_LIMIT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_OVER_POOL" "SEC-STOCK-OVER-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCompensationCannotExceedInitialPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_LIMIT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_OVER_POOL" "SEC-EC-OVER-POOL" stakeholderId stockClassId stockPlanId 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockIssuanceClassMustBelongToPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithPlanClass, planClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, otherClassId) <- setupStockClassWithId issuer capTableWithPlanClass "SC_PLAN_MEMBERSHIP_OTHER" + let stockPlanId = "PLAN_STOCK_CLASS_MEMBERSHIP" + let plan = (defaultStockPlan stockPlanId planClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_CLASS_OUTSIDE_PLAN" "SEC-STOCK-CLASS-OUTSIDE-PLAN" stakeholderId otherClassId) with + stock_plan_id = Some stockPlanId + quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCompensationClassMustBelongToPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithPlanClass, planClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, otherClassId) <- setupStockClassWithId issuer capTableWithPlanClass "SC_EQUITY_PLAN_MEMBERSHIP_OTHER" + let stockPlanId = "PLAN_EQUITY_CLASS_MEMBERSHIP" + let plan = (defaultStockPlan stockPlanId planClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_CLASS_OUTSIDE_PLAN" + "SEC-EQUITY-CLASS-OUTSIDE-PLAN" + stakeholderId + otherClassId + stockPlanId + 50.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ExercisedEquityCannotBeCancelledAndReturnedAgain = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_SOURCE_HISTORY" + let securityId = "SEC-EQUITY-SOURCE-HISTORY" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_SOURCE_HISTORY" + securityId + stakeholderId + stockClassId + stockPlanId + 100.0 + initialResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd initialResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise (planEquityCompensationExercise "TX_EQUITY_SOURCE_EXERCISE" securityId 100.0) + , CT.OcfCreateEquityCompensationCancellation (equityCompensationCancellation "TX_EQUITY_SOURCE_CANCEL" securityId 100.0) + , CT.OcfCreateStockPlanReturnToPool (stockPlanReturnToPool "TX_EQUITY_SOURCE_RETURN" securityId stockPlanId 100.0) + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ExercisedEquityCannotBeRetracted = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_RETRACTION_HISTORY" + let securityId = "SEC-EQUITY-RETRACTION-HISTORY" + let resultSecurityId = "SEC-EQUITY-RETRACTION-STOCK" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_RETRACTION_HISTORY" + securityId + stakeholderId + stockClassId + stockPlanId + 100.0 + initialResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let exercise = (planEquityCompensationExercise "TX_EQUITY_RETRACTION_EXERCISE" securityId 40.0) with + resulting_security_ids = [resultSecurityId] + let resultIssuance = (defaultStockIssuance "TX_EQUITY_RETRACTION_STOCK" resultSecurityId stakeholderId stockClassId) with quantity = 40.0 + exerciseResult <- submit issuer do + exerciseCmd initialResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + + let retraction = EquityCompensationRetractionOcfData with + id = "TX_EQUITY_RETRACTION_AFTER_EXERCISE" + date = defaultTestDate + security_id = securityId + reason_text = "Cannot retract exercised equity compensation" + comments = [] + submitMustFail issuer do + exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRetraction retraction] + edits = [] + deletes = [] + +testStockPlanReservedShares_NegativeEquityCompensationCannotOffsetOverallocatedPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_NEGATIVE_OFFSET" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let stockIssuance = (defaultStockIssuance "TX_STOCK_NEGATIVE_OFFSET" "SEC-STOCK-NEGATIVE-OFFSET" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let positiveEquityCompensation = stockPlanEquityCompensationIssuance "TX_EC_POSITIVE_OFFSET" "SEC-EC-POSITIVE-OFFSET" stakeholderId stockClassId stockPlanId 50.0 + let negativeEquityCompensation = stockPlanEquityCompensationIssuance "TX_EC_NEGATIVE_OFFSET" "SEC-EC-NEGATIVE-OFFSET" stakeholderId stockClassId stockPlanId (-50.0) + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance stockIssuance + , CT.OcfCreateEquityCompensationIssuance positiveEquityCompensation + , CT.OcfCreateEquityCompensationIssuance negativeEquityCompensation + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockSplitCountsAgainstPlanPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_SPLIT_POOL" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_SPLIT_POOL" "SEC-STOCK-SPLIT-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let split = stockClassSplit "TX_SPLIT_POOL" stockClassId 2.0 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockClassSplit split + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCompensationStockSplitCountsAgainstPlanPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_SPLIT_POOL" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_SPLIT_POOL" "SEC-EC-SPLIT-POOL" stakeholderId stockClassId stockPlanId 100.0 + let split = stockClassSplit "TX_EC_SPLIT_POOL" stockClassId 2.0 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockClassSplit split + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ClasslessEquityCompensationUsesSinglePlanClassForSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_CLASSLESS_SPLIT_POOL" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = + (stockPlanEquityCompensationIssuance "TX_EC_CLASSLESS_SPLIT_POOL" "SEC-EC-CLASSLESS-SPLIT-POOL" stakeholderId stockClassId stockPlanId 100.0) with + stock_class_id = None + let split = stockClassSplit "TX_EC_CLASSLESS_SPLIT_POOL" stockClassId 2.0 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockClassSplit split + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ClasslessEquityCompensationAllowsMultiClassPlanWithoutSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_EC_CLASSLESS_SECOND" + let stockPlanId = "PLAN_EC_CLASSLESS_MULTI_CLASS" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = + (stockPlanEquityCompensationIssuance "TX_EC_CLASSLESS_MULTI_CLASS" "SEC-EC-CLASSLESS-MULTI-CLASS" stakeholderId firstStockClassId stockPlanId 100.0) with + stock_class_id = None + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessEquityCompensationRejectsAmbiguousMultiClassSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_EC_CLASSLESS_SPLIT_SECOND" + let stockPlanId = "PLAN_EC_CLASSLESS_AMBIGUOUS_SPLIT" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 200.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = + (stockPlanEquityCompensationIssuance "TX_EC_CLASSLESS_AMBIGUOUS_SPLIT" "SEC-EC-CLASSLESS-AMBIGUOUS-SPLIT" stakeholderId firstStockClassId stockPlanId 100.0) with + stock_class_id = None + let split = stockClassSplit "TX_EC_CLASSLESS_AMBIGUOUS_SPLIT" firstStockClassId 2.0 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockClassSplit split + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnBeforeSplitAdjustsWithPlanPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_RETURN_BEFORE_SPLIT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = (defaultStockIssuance "TX_RETURN_SPLIT_ORIGINAL" "SEC-RETURN-SPLIT-ORIGINAL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let cancellation = stockCancellation "TX_RETURN_SPLIT_CANCEL" firstIssuance.security_id 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_SPLIT_POOL" firstIssuance.security_id stockPlanId 100.0 + let split = (stockClassSplit "TX_RETURN_SPLIT" stockClassId 2.0 1.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + let replacementIssuance = (defaultStockIssuance "TX_RETURN_SPLIT_REPLACEMENT" "SEC-RETURN-SPLIT-REPLACEMENT" stakeholderId stockClassId) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + stock_plan_id = Some stockPlanId + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance firstIssuance + , CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateStockClassSplit split + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_StockIssuanceBeforePlanEffectiveFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_FUTURE_EFFECTIVE" + let plan = (defaultStockPlan stockPlanId stockClassId) with + initial_shares_reserved = 100.0 + board_approval_date = Some (DT.time (DA.date 2024 Jan 02) 0 0 0) + let issuance = (defaultStockIssuance "TX_BEFORE_PLAN_EFFECTIVE" "SEC-BEFORE-PLAN-EFFECTIVE" stakeholderId stockClassId) with + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + stock_plan_id = Some stockPlanId + quantity = 10.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnBeforeTargetPlanEffectiveFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_RETURN_EFFECTIVE_SOURCE" + let targetPlanId = "PLAN_RETURN_EFFECTIVE_TARGET" + let sourceSecurityId = "SEC-RETURN-EFFECTIVE-SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 50.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with + initial_shares_reserved = 50.0 + board_approval_date = Some (DT.time (DA.date 2024 Jan 03) 0 0 0) + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_RETURN_EFFECTIVE_SOURCE" + sourceSecurityId + stakeholderId + stockClassId + sourcePlanId + 50.0) with date = DT.time (DA.date 2024 Jan 01) 0 0 0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_RETURN_EFFECTIVE_CANCEL" sourceSecurityId 50.0) with date = returnDate + let returnToPool = (stockPlanReturnToPool "TX_RETURN_EFFECTIVE_POOL" sourceSecurityId targetPlanId 50.0) with date = returnDate + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_AdjustmentAllowsAdditionalIssuanceSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_ADJUSTED" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = stockPlanPoolAdjustment "TX_POOL_UP" stockPlanId 200.0 + let issuance = (defaultStockIssuance "TX_STOCK_ADJUSTED_POOL" "SEC-STOCK-ADJUSTED-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_FutureAdjustmentCannotAuthorizeBackdatedIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let earlierDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let laterDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let stockPlanId = "PLAN_BACKDATED" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = (stockPlanPoolAdjustment "TX_POOL_BACKDATED_UP" stockPlanId 200.0) with date = laterDate + let issuance = (defaultStockIssuance "TX_STOCK_BACKDATED_POOL" "SEC-STOCK-BACKDATED-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + date = earlierDate + quantity = 150.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_SameDateLowerAdjustmentWins = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_SAME_DATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let higherAdjustment = stockPlanPoolAdjustment "TX_POOL_HIGH" stockPlanId 200.0 + let lowerAdjustment = stockPlanPoolAdjustment "TX_POOL_LOW" stockPlanId 120.0 + let issuance = (defaultStockIssuance "TX_STOCK_SAME_DATE_POOL" "SEC-STOCK-SAME-DATE-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment higherAdjustment + , CT.OcfCreateStockPlanPoolAdjustment lowerAdjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_RETURN_REUSE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = stockPlanEquityCompensationIssuance "TX_EC_ORIGINAL_POOL" "SEC-EC-ORIGINAL-POOL" stakeholderId stockClassId stockPlanId 100.0 + let cancellation = equityCompensationCancellation "TX_EC_CANCEL_RETURN_TO_POOL" firstIssuance.security_id 40.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_TO_POOL" firstIssuance.security_id stockPlanId 40.0 + let secondIssuance = stockPlanEquityCompensationIssuance "TX_EC_REUSED_POOL" "SEC-EC-REUSED-POOL" stakeholderId stockClassId stockPlanId 40.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance firstIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_EquityCompensationReturnRequiresReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_RETURN_REQUIRES_REDUCTION" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_NO_REDUCTION" "SEC-EC-RETURN-NO-REDUCTION" stakeholderId stockClassId stockPlanId 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_NO_REDUCTION" issuance.security_id stockPlanId 40.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockReturnRequiresReturnedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_RETURN_LIVE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_RETURN_LIVE" "SEC-STOCK-RETURN-LIVE" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_LIVE_STOCK" issuance.security_id stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockCancellationReturnAllowsReuse = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_RETURN_REUSE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = (defaultStockIssuance "TX_STOCK_RETURN_REUSE_FIRST" "SEC-STOCK-RETURN-REUSE-FIRST" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let cancellation = stockCancellation "TX_CANCEL_STOCK_RETURN_REUSE" firstIssuance.security_id 40.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_STOCK_REUSE" firstIssuance.security_id stockPlanId 40.0 + let secondIssuance = (defaultStockIssuance "TX_STOCK_RETURN_REUSE_SECOND" "SEC-STOCK-RETURN-REUSE-SECOND" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 40.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance firstIssuance + , CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateStockIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_BackdatedReturnCannotUseFutureCancellation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuanceDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let cancellationDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let stockPlanId = "PLAN_BACKDATED_RETURN" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_BACKDATED_RETURN" "SEC-STOCK-BACKDATED-RETURN" stakeholderId stockClassId) with + date = issuanceDate + stock_plan_id = Some stockPlanId + quantity = 100.0 + let cancellation = (stockCancellation "TX_CANCEL_AFTER_RETURN" issuance.security_id 40.0) with date = cancellationDate + let returnToPool = (stockPlanReturnToPool "TX_BACKDATED_RETURN" issuance.security_id stockPlanId 40.0) with date = returnDate + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockRetractionRemovesPlanAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_RETRACTION_REALLOCATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = (defaultStockIssuance "TX_STOCK_RETRACTED_POOL" "SEC-STOCK-RETRACTED-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let retraction = StockRetractionOcfData with + id = "TX_RETRACT_STOCK_POOL" + date = defaultTestDate + security_id = firstIssuance.security_id + reason_text = "Retracted stock issuance" + comments = [] + let secondIssuance = (defaultStockIssuance "TX_STOCK_AFTER_RETRACTION" "SEC-STOCK-AFTER-RETRACTION" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance firstIssuance + , CT.OcfCreateStockRetraction retraction + , CT.OcfCreateStockIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_EquityCompensationRetractionRemovesPlanAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_RETRACTION_REALLOCATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = stockPlanEquityCompensationIssuance "TX_EC_RETRACTED_POOL" "SEC-EC-RETRACTED-POOL" stakeholderId stockClassId stockPlanId 100.0 + let retraction = EquityCompensationRetractionOcfData with + id = "TX_RETRACT_EC_POOL" + date = defaultTestDate + security_id = firstIssuance.security_id + reason_text = "Retracted equity compensation issuance" + comments = [] + let secondIssuance = stockPlanEquityCompensationIssuance "TX_EC_AFTER_RETRACTION" "SEC-EC-AFTER-RETRACTION" stakeholderId stockClassId stockPlanId 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance firstIssuance + , CT.OcfCreateEquityCompensationRetraction retraction + , CT.OcfCreateEquityCompensationIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_StockRetractionDateChecksHistoricalAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_RETRACTION_HISTORY" + let securityId = "SEC-STOCK-RETRACTION-HISTORY" + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let replacementDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (defaultStockIssuance "TX_STOCK_RETRACTION_HISTORY_SOURCE" securityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let replacementIssuance = (defaultStockIssuance "TX_STOCK_RETRACTION_HISTORY_REPLACEMENT" "SEC-STOCK-RETRACTION-HISTORY-REPLACEMENT" stakeholderId stockClassId) with + date = replacementDate + stock_plan_id = Some stockPlanId + quantity = 100.0 + let retraction = StockRetractionOcfData with + id = "TX_STOCK_RETRACTION_HISTORY_RETRACT" + date = retractionDate + security_id = securityId + reason_text = "Retracted after return" + comments = [] + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + , CT.OcfCreateStockCancellation ((stockCancellation "TX_STOCK_RETRACTION_HISTORY_CANCEL" securityId 100.0) with date = returnDate) + , CT.OcfCreateStockPlanReturnToPool ((stockPlanReturnToPool "TX_STOCK_RETRACTION_HISTORY_RETURN" securityId stockPlanId 100.0) with date = returnDate) + , CT.OcfCreateStockRetraction retraction + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityRetractionDateChecksHistoricalAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_RETRACTION_DATE_HISTORY" + let securityId = "SEC-EQUITY-RETRACTION-DATE-HISTORY" + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let replacementDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_RETRACTION_DATE_HISTORY_SOURCE" + securityId + stakeholderId + stockClassId + stockPlanId + 100.0 + let replacementIssuance = (stockPlanEquityCompensationIssuance + "TX_EQUITY_RETRACTION_DATE_HISTORY_REPLACEMENT" + "SEC-EQUITY-RETRACTION-DATE-HISTORY-REPLACEMENT" + stakeholderId + stockClassId + stockPlanId + 100.0) with date = replacementDate + let retraction = EquityCompensationRetractionOcfData with + id = "TX_EQUITY_RETRACTION_DATE_HISTORY_RETRACT" + date = retractionDate + security_id = securityId + reason_text = "Retracted after return" + comments = [] + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation + ((equityCompensationCancellation "TX_EQUITY_RETRACTION_DATE_HISTORY_CANCEL" securityId 100.0) with date = returnDate) + , CT.OcfCreateStockPlanReturnToPool + ((stockPlanReturnToPool "TX_EQUITY_RETRACTION_DATE_HISTORY_RETURN" securityId stockPlanId 100.0) with date = returnDate) + , CT.OcfCreateEquityCompensationRetraction retraction + , CT.OcfCreateEquityCompensationIssuance replacementIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_RetractedEquityCompensationCannotBeReturnedToPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_RETRACTION_RETURN" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_RETRACTION_RETURN_GRANTED" "SEC-EC-RETRACTION-RETURN" stakeholderId stockClassId stockPlanId 100.0 + let retraction = EquityCompensationRetractionOcfData with + id = "TX_RETRACT_EC_RETURN" + date = defaultTestDate + security_id = issuance.security_id + reason_text = "Retracted equity compensation issuance" + comments = [] + let returnToPool = stockPlanReturnToPool "TX_RETURN_RETRACTED_EC" issuance.security_id stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateEquityCompensationRetraction retraction + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnToPoolCannotExceedSecurityAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_RETURN_OVER" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_OVER" "SEC-EC-RETURN-OVER" stakeholderId stockClassId stockPlanId 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_OVER" issuance.security_id stockPlanId 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnToPoolCanRolloverToDifferentPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_RETURN_SOURCE" + let targetPlanId = "PLAN_RETURN_TARGET" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 50.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 50.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_ROLLOVER_SOURCE" "SEC-EC-RETURN-ROLLOVER-SOURCE" stakeholderId stockClassId sourcePlanId 50.0 + let cancellation = equityCompensationCancellation "TX_EC_RETURN_ROLLOVER_CANCEL" sourceIssuance.security_id 50.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_ROLLOVER" sourceIssuance.security_id targetPlanId 50.0 + let targetIssuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_ROLLOVER_TARGET" "SEC-EC-RETURN-ROLLOVER-TARGET" stakeholderId stockClassId targetPlanId 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ReturnToPoolRejectsTargetPlanOutsideSecurityClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_RETURN_CLASS_SOURCE" + let targetStockClassId = "SC_RETURN_CLASS_TARGET" + let sourcePlanId = "PLAN_RETURN_CLASS_SOURCE" + let targetPlanId = "PLAN_RETURN_CLASS_TARGET" + (capTableWithSourceClass, _) <- setupStockClassWith issuer cap_table (defaultStockClass sourceStockClassId) + (capTableWithTargetClass, _) <- setupStockClassWith issuer capTableWithSourceClass (defaultStockClass targetStockClassId) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithTargetClass "SH_RETURN_CLASS_MISMATCH" "Return Class Holder" + let sourcePlan = (defaultStockPlan sourcePlanId sourceStockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId targetStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_RETURN_CLASS_SOURCE" "SEC_RETURN_CLASS_SOURCE" stakeholderId sourceStockClassId sourcePlanId 100.0 + let cancellation = equityCompensationCancellation "TX_RETURN_CLASS_CANCEL" sourceIssuance.security_id 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_CLASS_POOL" sourceIssuance.security_id targetPlanId 100.0 + let targetIssuance = stockPlanEquityCompensationIssuance "TX_RETURN_CLASS_TARGET" "SEC_RETURN_CLASS_TARGET" stakeholderId targetStockClassId targetPlanId 200.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_CrossPlanReturnCanPrecedeTargetAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_RETURN_HISTORY_SOURCE" + let targetPlanId = "PLAN_RETURN_HISTORY_TARGET" + let sourceSecurityId = "SEC-RETURN-HISTORY-SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 50.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 50.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_RETURN_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId sourcePlanId 50.0) with + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_RETURN_HISTORY_CANCEL" sourceSecurityId 50.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnToPool = (stockPlanReturnToPool "TX_RETURN_HISTORY_POOL" sourceSecurityId targetPlanId 50.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + let targetIssuance = (stockPlanEquityCompensationIssuance "TX_RETURN_HISTORY_TARGET" "SEC-RETURN-HISTORY-TARGET" stakeholderId stockClassId targetPlanId 100.0) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + returned <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + + allocated <- submit issuer do + exerciseCmd returned.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance targetIssuance] + edits = [] + deletes = [] + + let excessIssuance = (stockPlanEquityCompensationIssuance + "TX_RETURN_HISTORY_EXCESS" + "SEC-RETURN-HISTORY-EXCESS" + stakeholderId + stockClassId + targetPlanId + 1.0) with date = DT.time (DA.date 2024 Jan 04) 0 0 0 + submitMustFail issuer do + exerciseCmd allocated.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance excessIssuance] + edits = [] + deletes = [] + +testStockPlanReservedShares_AdjustmentCannotDropBelowAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithIssuance, _, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_ADJ_DROP" 200.0 150.0 + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlanPoolAdjustment (stockPlanPoolAdjustment "TX_POOL_DOWN" "PLAN_ADJ_DROP" 100.0)] + edits = [] + deletes = [] + +testStockPlanReservedShares_AdjustmentCannotReferenceMissingPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlanPoolAdjustment (stockPlanPoolAdjustment "TX_POOL_ORPHAN" "PLAN_POOL_ORPHAN" 100.0)] + edits = [] + deletes = [] + +testStockPlanReservedShares_DeletePlanCannotOrphanPoolAdjustment = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, _) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_DELETE_POOL_ADJUSTMENT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = stockPlanPoolAdjustment "TX_DELETE_POOL_ADJUSTMENT" stockPlanId 200.0 + + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlan stockPlanId] + +testStockPlanReservedShares_DeleteAdjustmentCannotInvalidateAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_DELETE_ADJ" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = stockPlanPoolAdjustment "TX_POOL_DELETE" stockPlanId 200.0 + let issuance = (defaultStockIssuance "TX_STOCK_DELETE_ADJ" "SEC-STOCK-DELETE-ADJ" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlanPoolAdjustment adjustment.id] + +testStockPlanReservedShares_DeletePlanCannotOrphanStockIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithIssuance, _, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_DELETE_ORPHAN" 200.0 150.0 + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlan "PLAN_DELETE_ORPHAN"] + +testStockPlanReservedShares_StockPlanEditCannotDropBelowAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithIssuance, stockClassId, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_EDIT_DROP" 200.0 150.0 + let editedPlan = (defaultStockPlan "PLAN_EDIT_DROP" stockClassId) with initial_shares_reserved = 100.0 + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + +testStockPlanReservedShares_StockTransferSuccessorDoesNotDoubleAllocate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_TRANSFER_SUCCESSOR" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-STOCK-TRANSFER-SUCCESSOR" + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_SUCCESSOR_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (planStockTransfer "TX_STOCK_TRANSFER_SUCCESSOR" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_InheritedStockSuccessorCanReturnCancelledShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_INHERITED_STOCK_RETURN" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-INHERITED-STOCK-RETURN" + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_INHERITED_STOCK_RETURN_RESULT" resultSecurityId stakeholderId stockClassId) with + date = transferDate + stock_plan_id = None + quantity = 100.0 + transferred <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer ((planStockTransfer "TX_INHERITED_STOCK_RETURN_TRANSFER" sourceSecurityId resultSecurityId 100.0) with date = transferDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation ((stockCancellation "TX_INHERITED_STOCK_RETURN_CANCEL" resultSecurityId 100.0) with date = returnDate) + , CT.OcfCreateStockPlanReturnToPool ((stockPlanReturnToPool "TX_INHERITED_STOCK_RETURN_POOL" resultSecurityId stockPlanId 100.0) with date = returnDate) + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_StockTransferQuantityIncreaseConsumesPlanHeadroom = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_TRANSFER_INCREASE" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 150.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-STOCK-TRANSFER-INCREASE" + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_INCREASE_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = None + quantity = 200.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (planStockTransfer "TX_STOCK_TRANSFER_INCREASE" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockTransferExactContinuityRejectsEditedSuccessorQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_TRANSFER_SUCCESSOR_EDIT" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 300.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-STOCK-TRANSFER-SUCCESSOR-EDIT" + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_SUCCESSOR_EDIT_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = None + quantity = 100.0 + + transferred <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (planStockTransfer "TX_STOCK_TRANSFER_SUCCESSOR_EDIT" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + + -- The plan still has 200 shares of headroom, so this must fail because the + -- edited successor no longer equals the transfer's recorded 100-share output. + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (resultIssuance with quantity = 200.0)] + deletes = [] + +testStockPlanReservedShares_ConvertedAllocationUsesDestinationClassSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE" + let destinationStockClassId = "SC_PLAN_CONVERSION_DESTINATION" + let stockPlanId = "PLAN_CONVERSION_DESTINATION_SPLIT" + let sourceSecurityId = "SEC_PLAN_CONVERSION_SOURCE" + let resultSecurityId = "SEC_PLAN_CONVERSION_RESULT" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let splitDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_PLAN_CONVERSION" "Plan Conversion Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 150.0 + stock_class_ids = [sourceStockClassId, destinationStockClassId] + let sourceIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_SOURCE" sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_RESULT" resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 100.0 + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion (planStockConversion "TX_PLAN_CONVERSION" sourceSecurityId resultSecurityId conversionDate 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_PLAN_CONVERSION_DESTINATION_SPLIT" destinationStockClassId 2.0 1.0) with date = splitDate) + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ConvertedClassOutsidePlanTracksSplitDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_PLAN_CONVERSION_EXTERNAL_SOURCE" + let destinationStockClassId = "SC_PLAN_CONVERSION_EXTERNAL_DESTINATION" + let stockPlanId = "PLAN_CONVERSION_EXTERNAL_DESTINATION_SPLIT" + let sourceSecurityId = "SEC_PLAN_CONVERSION_EXTERNAL_SOURCE" + let resultSecurityId = "SEC_PLAN_CONVERSION_EXTERNAL_RESULT" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let splitDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let adjustmentDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_PLAN_CONVERSION_EXTERNAL" "External Conversion Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 150.0 + stock_class_ids = [sourceStockClassId] + let sourceIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_EXTERNAL_SOURCE" sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_EXTERNAL_RESULT" resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 100.0 + let adjustment = (stockPlanPoolAdjustment "TX_PLAN_CONVERSION_EXTERNAL_ADJUSTMENT" stockPlanId 200.0) with date = adjustmentDate + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion (planStockConversion "TX_PLAN_CONVERSION_EXTERNAL" sourceSecurityId resultSecurityId conversionDate 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_PLAN_CONVERSION_EXTERNAL_SPLIT" destinationStockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockPlanPoolAdjustment adjustment + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ConvertedAllocationFitsAdjustedPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_ALLOWED" + let destinationStockClassId = "SC_PLAN_CONVERSION_DESTINATION_ALLOWED" + let stockPlanId = "PLAN_CONVERSION_DESTINATION_SPLIT_ALLOWED" + let sourceSecurityId = "SEC_PLAN_CONVERSION_SOURCE_ALLOWED" + let resultSecurityId = "SEC_PLAN_CONVERSION_RESULT_ALLOWED" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let splitDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_PLAN_CONVERSION_ALLOWED" "Allowed Plan Conversion Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 200.0 + stock_class_ids = [sourceStockClassId, destinationStockClassId] + let sourceIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_SOURCE_ALLOWED" sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_RESULT_ALLOWED" resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion (planStockConversion "TX_PLAN_CONVERSION_ALLOWED" sourceSecurityId resultSecurityId conversionDate 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_PLAN_CONVERSION_DESTINATION_SPLIT_ALLOWED" destinationStockClassId 2.0 1.0) with date = splitDate) + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_SourceRetractionDoesNotEraseConvertedAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (convertedCapTable, stockPlanId, sourceSecurityId, _, stakeholderId) <- + setupPlanBackedConversion issuer cap_table "SOURCE_RETRACTION" 100.0 + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_SOURCE_RETRACTION" + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let excessIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_SOURCE_RETRACTION_EXCESS" "SEC_PLAN_CONVERSION_SOURCE_RETRACTION_EXCESS" stakeholderId sourceStockClassId) with + date = DT.time (DA.date 2024 Jan 04) 0 0 0 + stock_plan_id = Some stockPlanId + quantity = 1.0 + + submitMustFail issuer do + exerciseCmd convertedCapTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockRetraction (planStockRetraction "TX_PLAN_CONVERSION_SOURCE_RETRACTION" sourceSecurityId retractionDate) + , CT.OcfCreateStockIssuance excessIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_PartialConversionBalancePreservesAllocationAfterSourceRetraction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_PLAN_PARTIAL_CONVERSION_SOURCE" + let destinationStockClassId = "SC_PLAN_PARTIAL_CONVERSION_DESTINATION" + let stockPlanId = "PLAN_PARTIAL_CONVERSION_BALANCE" + let sourceSecurityId = "SEC_PLAN_PARTIAL_CONVERSION_SOURCE" + let resultSecurityId = "SEC_PLAN_PARTIAL_CONVERSION_RESULT" + let balanceSecurityId = "SEC_PLAN_PARTIAL_CONVERSION_BALANCE" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let excessDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_PLAN_PARTIAL_CONVERSION" "Partial Conversion Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [sourceStockClassId, destinationStockClassId] + let sourceIssuance = (defaultStockIssuance "TX_PLAN_PARTIAL_CONVERSION_SOURCE" sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let conversion = (planStockConversion "TX_PLAN_PARTIAL_CONVERSION" sourceSecurityId resultSecurityId conversionDate 40.0) with + balance_security_id = Some balanceSecurityId + let resultIssuance = (defaultStockIssuance "TX_PLAN_PARTIAL_CONVERSION_RESULT" resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 40.0 + let balanceIssuance = (defaultStockIssuance "TX_PLAN_PARTIAL_CONVERSION_BALANCE" balanceSecurityId stakeholderId sourceStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 60.0 + converted <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + + let excessIssuance = (defaultStockIssuance "TX_PLAN_PARTIAL_CONVERSION_EXCESS" "SEC_PLAN_PARTIAL_CONVERSION_EXCESS" stakeholderId sourceStockClassId) with + date = excessDate + stock_plan_id = Some stockPlanId + quantity = 60.0 + submitMustFail issuer do + exerciseCmd converted.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockRetraction (planStockRetraction "TX_PLAN_PARTIAL_CONVERSION_SOURCE_RETRACTION" sourceSecurityId retractionDate) + , CT.OcfCreateStockIssuance excessIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ResultRetractionReleasesConvertedAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (convertedCapTable, stockPlanId, _, resultSecurityId, stakeholderId) <- + setupPlanBackedConversion issuer cap_table "RESULT_RETRACTION" 100.0 + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_RESULT_RETRACTION" + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let replacementIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_RESULT_RETRACTION_REPLACEMENT" "SEC_PLAN_CONVERSION_RESULT_RETRACTION_REPLACEMENT" stakeholderId sourceStockClassId) with + date = DT.time (DA.date 2024 Jan 04) 0 0 0 + stock_plan_id = Some stockPlanId + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd convertedCapTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockRetraction (planStockRetraction "TX_PLAN_CONVERSION_RESULT_SECURITY_RETRACTION" resultSecurityId retractionDate) + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ConsolidationCannotInflatePlanAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_CONSOLIDATION_INFLATION" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 150.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC_PLAN_CONSOLIDATION_INFLATED_RESULT" + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_PLAN_CONSOLIDATION_INFLATED_RESULT" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = None + quantity = 200.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation (planStockConsolidation "TX_PLAN_CONSOLIDATION_INFLATION" [sourceSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_NonPlanStockSourceDoesNotHidePlanSuccessor = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_NON_PLAN_STOCK_SOURCE" + let sourceSecurityId = "SEC-NON-PLAN-STOCK-SOURCE" + let resultSecurityId = "SEC-NON-PLAN-STOCK-RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (defaultStockIssuance "TX_NON_PLAN_STOCK_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0 + let resultIssuance = (defaultStockIssuance "TX_NON_PLAN_STOCK_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let excessIssuance = (defaultStockIssuance "TX_NON_PLAN_STOCK_EXCESS" "SEC-NON-PLAN-STOCK-EXCESS" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + , CT.OcfCreateStockTransfer (planStockTransfer "TX_NON_PLAN_STOCK_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance excessIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockTransferDoesNotCreateReturnableShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_TRANSFER_NO_RETURN" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-STOCK-TRANSFER-NO-RETURN-RESULT" + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_NO_RETURN_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let returnToPool = stockPlanReturnToPool "TX_STOCK_TRANSFER_INVALID_RETURN" sourceSecurityId stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (planStockTransfer "TX_STOCK_TRANSFER_NO_RETURN" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_BalanceSuccessorOnlyReturnsCancelledShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_BALANCE_RETURN" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let balanceSecurityId = "SEC-STOCK-BALANCE-RETURN-RESULT" + let balanceIssuance = (defaultStockIssuance "TX_STOCK_BALANCE_RETURN_RESULT" balanceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 60.0 + let cancellation = (stockCancellation "TX_STOCK_BALANCE_RETURN_CANCEL" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let returnToPool = stockPlanReturnToPool "TX_STOCK_BALANCE_EXCESS_RETURN" sourceSecurityId stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance balanceIssuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationRequiresBalanceIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_MISSING_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_MISSING_BALANCE_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_MISSING_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_MISSING_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_MISSING_BALANCE" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationCancellation cancellation] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationRejectsOverstatedBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_OVERSTATED_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_OVERSTATED_BALANCE_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_OVERSTATED_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_OVERSTATED_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_OVERSTATED_BALANCE" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_OVERSTATED_BALANCE_RESULT" balanceSecurityId stakeholderId stockClassId stockPlanId 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationRejectsLaterBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_LATER_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_LATER_BALANCE_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_LATER_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_LATER_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let balanceDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_LATER_BALANCE" sourceSecurityId 40.0) with + date = cancellationDate + balance_security_id = Some balanceSecurityId + let balanceIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_LATER_BALANCE_RESULT" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0) with date = balanceDate + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationRejectsBalanceClassMismatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithSource, sourceStockClassId) <- setupStockClassWith issuer cap_table (defaultStockClass "SC_EQUITY_CANCEL_BALANCE_SOURCE") + (capTableWithWrong, wrongStockClassId) <- setupStockClassWith issuer capTableWithSource (defaultStockClass "SC_EQUITY_CANCEL_BALANCE_WRONG") + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithWrong "SH_EQUITY_CANCEL_BALANCE_CLASS" "Equity Cancellation Balance Class Holder" + let stockPlanId = "PLAN_EQUITY_CANCEL_BALANCE_CLASS" + let sourceSecurityId = "SEC_EQUITY_CANCEL_BALANCE_CLASS_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_BALANCE_CLASS" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [sourceStockClassId, wrongStockClassId] + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_BALANCE_CLASS_SOURCE" sourceSecurityId stakeholderId sourceStockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_BALANCE_CLASS" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_BALANCE_CLASS_RESULT" balanceSecurityId stakeholderId wrongStockClassId stockPlanId 60.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationExactBalanceReplacesSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_EXACT_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_EXACT_BALANCE_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_EXACT_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_EXACT_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let exerciseDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_EXACT_BALANCE" sourceSecurityId 40.0) with + date = cancellationDate + balance_security_id = Some balanceSecurityId + let balanceIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_EXACT_BALANCE_RESULT" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0) with + date = cancellationDate + stock_plan_id = None + cancelled <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd cancelled.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((planEquityCompensationExercise "TX_EQUITY_CANCEL_EXACT_BALANCE_SOURCE_EXERCISE" sourceSecurityId 1.0) with date = exerciseDate)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd cancelled.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((planEquityCompensationExercise "TX_EQUITY_CANCEL_EXACT_BALANCE_OVER_EXERCISE" balanceSecurityId 61.0) with date = exerciseDate)] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd cancelled.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((planEquityCompensationExercise "TX_EQUITY_CANCEL_EXACT_BALANCE_RESULT_EXERCISE" balanceSecurityId 60.0) with date = exerciseDate)] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_EquityCancellationRejectsSourceAsBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_SELF_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_SELF_BALANCE_SOURCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_SELF_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_SELF_BALANCE" sourceSecurityId 40.0) with + balance_security_id = Some sourceSecurityId + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationCancellation cancellation] + edits = [] + deletes = [] + +testStockPlanReservedShares_ClasslessEquityCancellationBalanceUsesPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CLASSLESS_EQUITY_CANCEL_BALANCE" + let sourceSecurityId = "SEC_CLASSLESS_EQUITY_CANCEL_SOURCE" + let balanceSecurityId = "SEC_CLASSLESS_EQUITY_CANCEL_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_CLASSLESS_EQUITY_CANCEL_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0) with + stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_CLASSLESS_EQUITY_CANCEL" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_CLASSLESS_EQUITY_CANCEL_BALANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_EquityTransferSuccessorDoesNotDoubleAllocate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_SUCCESSOR" + let sourceSecurityId = "SEC-EQUITY-TRANSFER-SOURCE" + let resultSecurityId = "SEC-EQUITY-TRANSFER-SUCCESSOR" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_SUCCESSOR" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessEquityTransferResultUsesPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CLASSLESS_EQUITY_TRANSFER" + let sourceSecurityId = "SEC_CLASSLESS_EQUITY_TRANSFER_SOURCE" + let resultSecurityId = "SEC_CLASSLESS_EQUITY_TRANSFER_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_CLASSLESS_EQUITY_TRANSFER_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0) with + stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = stockPlanEquityCompensationIssuance "TX_CLASSLESS_EQUITY_TRANSFER_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_CLASSLESS_EQUITY_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_EquityTransferReducesSourceQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_SOURCE_REDUCTION" + let sourceSecurityId = "SEC-EQUITY-TRANSFER-REDUCED-SOURCE" + let resultSecurityId = "SEC-EQUITY-TRANSFER-REDUCED-RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_REDUCED_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_REDUCED_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + transferred <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_SOURCE_REDUCTION" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (planEquityCompensationExercise "TX_EQUITY_TRANSFER_REDUCED_SOURCE_EXERCISE" sourceSecurityId 100.0)] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferRejectsOverstatedResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_OVERSTATED_RESULT" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_RESULT_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 300.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_RESULT_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 200.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_OVERSTATED_RESULT" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferRejectsLaterResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_LATER_RESULT" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_LATER_RESULT_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_LATER_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_RESULT_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let transfer = (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_LATER_RESULT" sourceSecurityId resultSecurityId 100.0) with date = transferDate + let resultIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0) with date = resultDate + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferRejectsOverstatedBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_OVERSTATED_BALANCE" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_BALANCE_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_BALANCE_RESULT" + let balanceSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 300.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transfer = (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_OVERSTATED_BALANCE" sourceSecurityId resultSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_BALANCE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 40.0 + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_BALANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferRejectsLaterBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_LATER_BALANCE" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_LATER_BALANCE_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_LATER_BALANCE_RESULT" + let balanceSecurityId = "SEC_EQUITY_TRANSFER_LATER_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let balanceDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let transfer = ((planEquityCompensationTransfer "TX_EQUITY_TRANSFER_LATER_BALANCE" sourceSecurityId resultSecurityId 40.0) with + date = transferDate + balance_security_id = Some balanceSecurityId) + let resultIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 40.0) with date = transferDate + let balanceIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE_ISSUANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0) with date = balanceDate + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferAcceptsExactBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_EXACT_BALANCE" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_EXACT_BALANCE_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_EXACT_BALANCE_RESULT" + let balanceSecurityId = "SEC_EQUITY_TRANSFER_EXACT_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transfer = (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_EXACT_BALANCE" sourceSecurityId resultSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 40.0 + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE_ISSUANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_EquityTransferRejectsResultStockClassMismatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_EQUITY_TRANSFER_CLASS_SOURCE" + let resultStockClassId = "SC_EQUITY_TRANSFER_CLASS_RESULT" + let stockPlanId = "PLAN_EQUITY_TRANSFER_CLASS_MISMATCH" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_CLASS_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_CLASS_RESULT" + (capTableWithSourceClass, _) <- setupStockClassWith issuer cap_table (defaultStockClass sourceStockClassId) + (capTableWithResultClass, _) <- setupStockClassWith issuer capTableWithSourceClass (defaultStockClass resultStockClassId) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithResultClass "SH_EQUITY_TRANSFER_CLASS_MISMATCH" "Equity Transfer Class Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 200.0 + stock_class_ids = [sourceStockClassId, resultStockClassId] + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_CLASS_SOURCE" sourceSecurityId stakeholderId sourceStockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_CLASS_RESULT" resultSecurityId stakeholderId resultStockClassId stockPlanId 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_CLASS_MISMATCH" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferSuccessorCanExercise = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_SUCCESSOR_EXERCISE" + let sourceSecurityId = "SEC-EQUITY-TRANSFER-EXERCISE-SOURCE" + let resultSecurityId = "SEC-EQUITY-TRANSFER-EXERCISE-RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXERCISE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXERCISE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + transferred <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_SUCCESSOR_EXERCISE" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (planEquityCompensationExercise "TX_EQUITY_TRANSFER_SUCCESSOR_EXERCISED" resultSecurityId 100.0)] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_NonPlanEquitySourceDoesNotHidePlanSuccessor = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_NON_PLAN_EQUITY_SOURCE" + let sourceSecurityId = "SEC-NON-PLAN-EQUITY-SOURCE" + let resultSecurityId = "SEC-NON-PLAN-EQUITY-RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_NON_PLAN_EQUITY_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0) with + stock_plan_id = None + let resultIssuance = stockPlanEquityCompensationIssuance "TX_NON_PLAN_EQUITY_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + let excessIssuance = stockPlanEquityCompensationIssuance "TX_NON_PLAN_EQUITY_EXCESS" "SEC-NON-PLAN-EQUITY-EXCESS" stakeholderId stockClassId stockPlanId 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_NON_PLAN_EQUITY_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance excessIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReissuancePreservesSourceAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_REISSUANCE_LINEAGE" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-REISSUANCE-LINEAGE-RESULT" + let resultIssuance = (defaultStockIssuance "TX_REISSUANCE_LINEAGE_RESULT" resultSecurityId stakeholderId stockClassId) with quantity = 100.0 + let excessIssuance = (defaultStockIssuance "TX_REISSUANCE_LINEAGE_EXCESS" "SEC-REISSUANCE-LINEAGE-EXCESS" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 1.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (planStockReissuance "TX_REISSUANCE_LINEAGE" sourceSecurityId resultSecurityId) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance excessIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReissuanceQuantityIncreaseConsumesPlanHeadroom = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_REISSUANCE_INCREASE" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 150.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-REISSUANCE-INCREASE" + let resultIssuance = (defaultStockIssuance "TX_REISSUANCE_INCREASE_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = None + quantity = 200.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (planStockReissuance "TX_REISSUANCE_INCREASE" sourceSecurityId resultSecurityId) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquitySplitDoesNotCreateReturnableShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_SPLIT_RETURN" + let securityId = "SEC-EQUITY-SPLIT-RETURN" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EQUITY_SPLIT_RETURN" securityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + let splitDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit ((stockClassSplit "TX_EQUITY_SPLIT_RETURN_SPLIT" stockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockPlanReturnToPool ((stockPlanReturnToPool "TX_EQUITY_SPLIT_INVALID_RETURN" securityId stockPlanId 100.0) with date = returnDate) + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ClasslessEquityCancellationReturnUsesPlanClassSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CLASSLESS_EQUITY_RETURN_SPLIT" + let securityId = "SEC-CLASSLESS-EQUITY-RETURN-SPLIT" + let splitDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_EQUITY_RETURN_SPLIT" + securityId + stakeholderId + stockClassId + stockPlanId + 100.0) with stock_class_id = None + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateEquityCompensationCancellation + ((equityCompensationCancellation "TX_CLASSLESS_EQUITY_RETURN_CANCEL" securityId 50.0) with date = defaultTestDate) + , CT.OcfCreateStockClassSplit + ((stockClassSplit "TX_CLASSLESS_EQUITY_RETURN_SPLIT_EVENT" stockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockPlanReturnToPool + ((stockPlanReturnToPool "TX_CLASSLESS_EQUITY_RETURN_POOL" securityId stockPlanId 100.0) with date = returnDate) + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_CumulativeReturnsAreSplitAdjusted = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_CUMULATIVE_SPLIT_RETURNS" + (capTableIssued, stockClassId, _) <- createPlanAndStockIssuance issuer cap_table stockPlanId 200.0 100.0 + let securityId = "SEC-" <> stockPlanId + let splitDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let secondReturnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let firstReturn = stockPlanReturnToPool "TX_CUMULATIVE_SPLIT_RETURN_FIRST" securityId stockPlanId 50.0 + let secondReturn = (stockPlanReturnToPool "TX_CUMULATIVE_SPLIT_RETURN_SECOND" securityId stockPlanId 50.0) with date = secondReturnDate + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation (stockCancellation "TX_CUMULATIVE_SPLIT_CANCEL" securityId 50.0) + , CT.OcfCreateStockPlanReturnToPool firstReturn + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_CUMULATIVE_SPLIT" stockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockPlanReturnToPool secondReturn + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_PlanAddClassRequiresSourceDisambiguation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_SUCCESSOR_PLAN_EDIT" "Classless Successor Plan Edit Holder" + let stockPlanId = "PLAN_CLASSLESS_SUCCESSOR_PLAN_EDIT" + let sourceSecurityId = "SEC_CLASSLESS_SUCCESSOR_PLAN_EDIT_SOURCE" + let resultSecurityId = "SEC_CLASSLESS_SUCCESSOR_PLAN_EDIT_RESULT" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_SUCCESSOR_PLAN_EDIT_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_SUCCESSOR_PLAN_EDIT_RESULT" + resultSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_plan_id = None + transferred <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_SUCCESSOR_PLAN_EDIT_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + + let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + + let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some firstStockClassId + _ <- submit issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = + [ CT.OcfEditStockPlan editedPlan + , CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance + ] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessSuccessorRejectsClassOutsidePlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithPlanClass, planStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_SUCCESSOR_IN_PLAN" + (capTableWithClasses, outsideStockClassId) <- setupStockClassWithId issuer capTableWithPlanClass "SC_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN" "Classless Successor Outside Plan Holder" + let stockPlanId = "PLAN_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN" + let sourceSecurityId = "SEC_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_SOURCE" + let resultSecurityId = "SEC_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_RESULT" + let plan = (defaultStockPlan stockPlanId planStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_SOURCE" + sourceSecurityId + stakeholderId + planStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_RESULT" + resultSecurityId + stakeholderId + outsideStockClassId + stockPlanId + 100.0) with stock_plan_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_MultiClassClasslessSourceRequiresClasslessSuccessor = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_MULTICLASS_CLASSLESS_SUCCESSOR_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_MULTICLASS_CLASSLESS_SUCCESSOR_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_MULTICLASS_CLASSLESS_SUCCESSOR" "Multi-Class Classless Successor Holder" + let stockPlanId = "PLAN_MULTICLASS_CLASSLESS_SUCCESSOR" + let sourceSecurityId = "SEC_MULTICLASS_CLASSLESS_SUCCESSOR_SOURCE" + let classedResultSecurityId = "SEC_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSED_RESULT" + let classlessResultSecurityId = "SEC_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSLESS_RESULT" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_MULTICLASS_CLASSLESS_SUCCESSOR_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let classedResultIssuance = (stockPlanEquityCompensationIssuance + "TX_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSED_RESULT" + classedResultSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_plan_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSED_TRANSFER" sourceSecurityId classedResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance classedResultIssuance + ] + edits = [] + deletes = [] + + let classlessResultIssuance = (stockPlanEquityCompensationIssuance + "TX_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSLESS_RESULT" + classlessResultSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with + stock_class_id = None + stock_plan_id = None + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSLESS_TRANSFER" sourceSecurityId classlessResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance classlessResultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_AmbiguousClasslessReturnStaysInSourcePlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_AMBIGUOUS_CLASSLESS_RETURN_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_AMBIGUOUS_CLASSLESS_RETURN_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_AMBIGUOUS_CLASSLESS_RETURN" "Ambiguous Classless Return Holder" + let stockPlanId = "PLAN_AMBIGUOUS_CLASSLESS_RETURN" + let securityId = "SEC_AMBIGUOUS_CLASSLESS_RETURN" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = (stockPlanEquityCompensationIssuance + "TX_AMBIGUOUS_CLASSLESS_RETURN_SOURCE" + securityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateEquityCompensationCancellation + (equityCompensationCancellation "TX_AMBIGUOUS_CLASSLESS_RETURN_CANCEL" securityId 100.0) + , CT.OcfCreateStockPlanReturnToPool + (stockPlanReturnToPool "TX_AMBIGUOUS_CLASSLESS_RETURN_POOL" securityId stockPlanId 100.0) + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_CLASSLESS_ROLLOVER_SOURCE" + let targetPlanId = "PLAN_CLASSLESS_ROLLOVER_TARGET" + let sourceSecurityId = "SEC_CLASSLESS_ROLLOVER_SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ROLLOVER_SOURCE" + sourceSecurityId + stakeholderId + stockClassId + sourcePlanId + 100.0) with stock_class_id = None + let targetIssuance = stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ROLLOVER_TARGET" + "SEC_CLASSLESS_ROLLOVER_TARGET" + stakeholderId + stockClassId + targetPlanId + 200.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = equityCompensationCancellation "TX_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0 + let returnToPool = stockPlanReturnToPool "TX_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] + edits = [] + deletes = [] + + let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some stockClassId + disambiguated <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance] + deletes = [] + + _ <- submit issuer do + exerciseCmd disambiguated.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation + cancellation + , CT.OcfCreateStockPlanReturnToPool + returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ConsolidationAllowsSamePlanSources = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CONSOLIDATION_SAME_PLAN" + let firstSourceSecurityId = "SEC_CONSOLIDATION_SAME_PLAN_FIRST" + let secondSourceSecurityId = "SEC_CONSOLIDATION_SAME_PLAN_SECOND" + let resultSecurityId = "SEC_CONSOLIDATION_SAME_PLAN_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_SAME_PLAN_FIRST" firstSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 40.0 + let secondSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_SAME_PLAN_SECOND" secondSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 60.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance firstSourceIssuance + , CT.OcfCreateStockIssuance secondSourceIssuance + ] + edits = [] + deletes = [] + + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_CONSOLIDATION_SAME_PLAN_RESULT" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = None + quantity = 100.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation + (planStockConsolidation "TX_CONSOLIDATION_SAME_PLAN" [firstSourceSecurityId, secondSourceSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ConsolidationRejectsDifferentResultPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_CONSOLIDATION_RESULT_SOURCE" + let resultPlanId = "PLAN_CONSOLIDATION_RESULT_TARGET" + let firstSourceSecurityId = "SEC_CONSOLIDATION_RESULT_FIRST" + let secondSourceSecurityId = "SEC_CONSOLIDATION_RESULT_SECOND" + let resultSecurityId = "SEC_CONSOLIDATION_RESULT_DIFFERENT_PLAN" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let resultPlan = (defaultStockPlan resultPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_RESULT_FIRST" firstSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some sourcePlanId + quantity = 40.0 + let secondSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_RESULT_SECOND" secondSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some sourcePlanId + quantity = 60.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan resultPlan + , CT.OcfCreateStockIssuance firstSourceIssuance + , CT.OcfCreateStockIssuance secondSourceIssuance + ] + edits = [] + deletes = [] + + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_CONSOLIDATION_RESULT_DIFFERENT_PLAN" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = Some resultPlanId + quantity = 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation + (planStockConsolidation "TX_CONSOLIDATION_RESULT_DIFFERENT_PLAN" [firstSourceSecurityId, secondSourceSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ConsolidationRejectsMixedPlanSources = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CONSOLIDATION_MIXED_SOURCES" + let planSourceSecurityId = "SEC_CONSOLIDATION_MIXED_PLAN_SOURCE" + let nonPlanSourceSecurityId = "SEC_CONSOLIDATION_MIXED_NON_PLAN_SOURCE" + let resultSecurityId = "SEC_CONSOLIDATION_MIXED_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let planSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_MIXED_PLAN_SOURCE" planSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 50.0 + let nonPlanSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_MIXED_NON_PLAN_SOURCE" nonPlanSourceSecurityId stakeholderId stockClassId) with quantity = 50.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance planSourceIssuance + , CT.OcfCreateStockIssuance nonPlanSourceIssuance + ] + edits = [] + deletes = [] + + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_CONSOLIDATION_MIXED_RESULT" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = None + quantity = 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation + (planStockConsolidation "TX_CONSOLIDATION_MIXED_SOURCES" [planSourceSecurityId, nonPlanSourceSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquitySuccessorPlanMustMatchInheritedPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_EQUITY_SUCCESSOR_SOURCE" + let targetPlanId = "PLAN_EQUITY_SUCCESSOR_TARGET" + let planSourceSecurityId = "SEC_EQUITY_SUCCESSOR_PLAN_SOURCE" + let nonPlanSourceSecurityId = "SEC_EQUITY_SUCCESSOR_NON_PLAN_SOURCE" + let mismatchedResultSecurityId = "SEC_EQUITY_SUCCESSOR_MISMATCHED_RESULT" + let allowedResultSecurityId = "SEC_EQUITY_SUCCESSOR_ALLOWED_RESULT" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let planSourceIssuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_SUCCESSOR_PLAN_SOURCE" + planSourceSecurityId + stakeholderId + stockClassId + sourcePlanId + 100.0 + let nonPlanSourceIssuance = (stockPlanEquityCompensationIssuance + "TX_EQUITY_SUCCESSOR_NON_PLAN_SOURCE" + nonPlanSourceSecurityId + stakeholderId + stockClassId + targetPlanId + 100.0) with stock_plan_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance planSourceIssuance + , CT.OcfCreateEquityCompensationIssuance nonPlanSourceIssuance + ] + edits = [] + deletes = [] + + let mismatchedResultIssuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_SUCCESSOR_MISMATCHED_RESULT" + mismatchedResultSecurityId + stakeholderId + stockClassId + targetPlanId + 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_EQUITY_SUCCESSOR_MISMATCHED_TRANSFER" planSourceSecurityId mismatchedResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance mismatchedResultIssuance + ] + edits = [] + deletes = [] + + let allowedResultIssuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_SUCCESSOR_ALLOWED_RESULT" + allowedResultSecurityId + stakeholderId + stockClassId + targetPlanId + 100.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_EQUITY_SUCCESSOR_ALLOWED_TRANSFER" nonPlanSourceSecurityId allowedResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance allowedResultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_StockSuccessorPlanMustMatchInheritedPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_STOCK_SUCCESSOR_SOURCE" + let targetPlanId = "PLAN_STOCK_SUCCESSOR_TARGET" + let planSourceSecurityId = "SEC_STOCK_SUCCESSOR_PLAN_SOURCE" + let nonPlanSourceSecurityId = "SEC_STOCK_SUCCESSOR_NON_PLAN_SOURCE" + let mismatchedResultSecurityId = "SEC_STOCK_SUCCESSOR_MISMATCHED_RESULT" + let allowedResultSecurityId = "SEC_STOCK_SUCCESSOR_ALLOWED_RESULT" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let planSourceIssuance = (defaultStockIssuance "TX_STOCK_SUCCESSOR_PLAN_SOURCE" planSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some sourcePlanId + quantity = 100.0 + let nonPlanSourceIssuance = (defaultStockIssuance "TX_STOCK_SUCCESSOR_NON_PLAN_SOURCE" nonPlanSourceSecurityId stakeholderId stockClassId) with quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateStockIssuance planSourceIssuance + , CT.OcfCreateStockIssuance nonPlanSourceIssuance + ] + edits = [] + deletes = [] + + let mismatchedResultIssuance = (defaultStockIssuance "TX_STOCK_SUCCESSOR_MISMATCHED_RESULT" mismatchedResultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some targetPlanId + quantity = 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer + (planStockTransfer "TX_STOCK_SUCCESSOR_MISMATCHED_TRANSFER" planSourceSecurityId mismatchedResultSecurityId 100.0) + , CT.OcfCreateStockIssuance mismatchedResultIssuance + ] + edits = [] + deletes = [] + + let allowedResultIssuance = (defaultStockIssuance "TX_STOCK_SUCCESSOR_ALLOWED_RESULT" allowedResultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some targetPlanId + quantity = 100.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer + (planStockTransfer "TX_STOCK_SUCCESSOR_ALLOWED_TRANSFER" nonPlanSourceSecurityId allowedResultSecurityId 100.0) + , CT.OcfCreateStockIssuance allowedResultIssuance + ] + edits = [] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml index 3dc9004d..ef2f9690 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml @@ -2,8 +2,10 @@ module OpenCapTable.TestStockPlanReturnToPool where import Fairmint.OpenCapTable.OCF.StockPlan import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool +import Fairmint.OpenCapTable.OCF.StockCancellation import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup +import OpenCapTable.TestHelpers (defaultStockIssuance, setupForStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -13,30 +15,47 @@ import Daml.Script -- Test: Create and archive return to pool (termination scenario) testCreateAndArchiveStockPlanReturnToPool = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_OPT_001" result <- submit issuer do - exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlan StockPlanOcfData with - id = "PLAN_2021" - plan_name = "2021 Equity Incentive Plan" - initial_shares_reserved = 1_000_000.0 - board_approval_date = Some (DT.time (DA.date 2021 Jan 01) 0 0 0) - stockholder_approval_date = None - default_cancellation_behavior = Some OcfPlanCancelReturnToPool - stock_class_ids = ["SC_COMMON"] - comments = []] + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan StockPlanOcfData with + id = "PLAN_2021" + plan_name = "2021 Equity Incentive Plan" + initial_shares_reserved = 1_000_000.0 + board_approval_date = Some (DT.time (DA.date 2021 Jan 01) 0 0 0) + stockholder_approval_date = None + default_cancellation_behavior = Some OcfPlanCancelReturnToPool + stock_class_ids = [stockClassId] + comments = [] + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_OPT_001" securityId stakeholderId stockClassId) with + stock_plan_id = Some "PLAN_2021" + quantity = 25000.0) + ] edits = [] deletes = [] -- Employee termination - unvested options return to pool _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with - id = "TX_RETURN_TO_POOL_1" - date = DT.time (DA.date 2024 Jun 30) 0 0 0 - security_id = "SEC_OPT_001" - stock_plan_id = "PLAN_2021" - quantity = 25000.0 - reason_text = "Employee termination - unvested options cancelled and returned to pool" - comments = ["Employee B voluntary resignation effective June 30, 2024"]] + creates = + [ CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX_CANCEL_FOR_RETURN_1" + date = DT.time (DA.date 2024 Jun 30) 0 0 0 + security_id = securityId + quantity = 25000.0 + balance_security_id = None + reason_text = "Employee termination - unvested options cancelled" + comments = ["Employee B voluntary resignation effective June 30, 2024"] + , CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with + id = "TX_RETURN_TO_POOL_1" + date = DT.time (DA.date 2024 Jun 30) 0 0 0 + security_id = securityId + stock_plan_id = "PLAN_2021" + quantity = 25000.0 + reason_text = "Employee termination - unvested options cancelled and returned to pool" + comments = ["Employee B voluntary resignation effective June 30, 2024"] + ] edits = [] deletes = [] pure () @@ -45,30 +64,47 @@ testCreateAndArchiveStockPlanReturnToPool = script do -- Test: Create and archive with Archive choice (expiration scenario) testCreateAndArchiveStockPlanReturnToPoolExpiration = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_OPT_002" result <- submit issuer do - exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlan StockPlanOcfData with - id = "PLAN_2015" - plan_name = "2015 Stock Option Plan" - initial_shares_reserved = 500_000.0 - board_approval_date = Some (DT.time (DA.date 2015 Mar 15) 0 0 0) - stockholder_approval_date = Some (DT.time (DA.date 2015 Apr 01) 0 0 0) - default_cancellation_behavior = Some OcfPlanCancelReturnToPool - stock_class_ids = ["SC_COMMON"] - comments = []] + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan StockPlanOcfData with + id = "PLAN_2015" + plan_name = "2015 Stock Option Plan" + initial_shares_reserved = 500_000.0 + board_approval_date = Some (DT.time (DA.date 2015 Mar 15) 0 0 0) + stockholder_approval_date = Some (DT.time (DA.date 2015 Apr 01) 0 0 0) + default_cancellation_behavior = Some OcfPlanCancelReturnToPool + stock_class_ids = [stockClassId] + comments = [] + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_OPT_002" securityId stakeholderId stockClassId) with + stock_plan_id = Some "PLAN_2015" + quantity = 10000.0) + ] edits = [] deletes = [] -- Expired unexercised options return to pool _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with - id = "TX_RETURN_TO_POOL_2" - date = DT.time (DA.date 2025 Mar 15) 0 0 0 - security_id = "SEC_OPT_002" - stock_plan_id = "PLAN_2015" - quantity = 10000.0 - reason_text = "Options expired unexercised after 10-year term" - comments = []] + creates = + [ CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX_CANCEL_FOR_RETURN_2" + date = DT.time (DA.date 2025 Mar 15) 0 0 0 + security_id = securityId + quantity = 10000.0 + balance_security_id = None + reason_text = "Options expired unexercised after 10-year term" + comments = [] + , CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with + id = "TX_RETURN_TO_POOL_2" + date = DT.time (DA.date 2025 Mar 15) 0 0 0 + security_id = securityId + stock_plan_id = "PLAN_2015" + quantity = 10000.0 + reason_text = "Options expired unexercised after 10-year term" + comments = [] + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockRepurchase.daml b/Test/daml/OpenCapTable/TestStockRepurchase.daml index fc8aa75b..0546623d 100644 --- a/Test/daml/OpenCapTable/TestStockRepurchase.daml +++ b/Test/daml/OpenCapTable/TestStockRepurchase.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.OCF.StockRepurchase import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -67,17 +67,24 @@ testStockRepurchase_PartialRepurchase = script do capTableCid <- addPrerequisites issuer cap_table let securityId = "SSEC-002" result <- createStockIssuance issuer capTableCid securityId 100000.0 + let repurchaseDate = DT.time (DA.date 2023 Jun 15) 0 0 0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_REPURCHASE_2_BALANCE" "SSEC-002-REMAIN" "SH-1" "SC_COMMON") with + date = repurchaseDate + quantity = 50000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockRepurchase StockRepurchaseOcfData with - id = "TX_STOCK_REPURCHASE_2" - date = DT.time (DA.date 2023 Jun 15) 0 0 0 - security_id = securityId - price = OcfMonetary with amount = 0.01, currency = "USD" - quantity = 50000.0 - comments = ["Partial repurchase of unvested shares upon voluntary termination."] - balance_security_id = Some "SSEC-002-REMAIN" - consideration_text = Some "Repurchase at original issue price per Stock Restriction Agreement Section 3."] + creates = + [ CT.OcfCreateStockRepurchase StockRepurchaseOcfData with + id = "TX_STOCK_REPURCHASE_2" + date = repurchaseDate + security_id = securityId + price = OcfMonetary with amount = 0.01, currency = "USD" + quantity = 50000.0 + comments = ["Partial repurchase of unvested shares upon voluntary termination."] + balance_security_id = Some balanceIssuance.security_id + consideration_text = Some "Repurchase at original issue price per Stock Restriction Agreement Section 3." + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockTransfer.daml b/Test/daml/OpenCapTable/TestStockTransfer.daml index 5f65170d..d8474d1e 100644 --- a/Test/daml/OpenCapTable/TestStockTransfer.daml +++ b/Test/daml/OpenCapTable/TestStockTransfer.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.OCF.StockTransfer import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -68,17 +68,24 @@ testStockTransfer_FounderTransferWithConsideration = script do capTableCid <- addPrerequisites issuer cap_table let securityId = "SSEC-FOUNDER-001" result <- createStockIssuance issuer capTableCid securityId 1000.0 + let transferDate = DT.time (DA.date 2024 Apr 18) 0 0 0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_2_BALANCE" "SSEC-FOUNDER-REMAIN" "SH-1" "SC_COMMON") with + date = transferDate + quantity = 925.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockTransfer StockTransferOcfData with - id = "TX_STOCK_TRANSFER_2" - date = DT.time (DA.date 2024 Apr 18) 0 0 0 - quantity = 75.0 - security_id = securityId - resulting_security_ids = ["SSEC-TRANSFEREE-001"] - comments = ["Stock transfer from Founder A to Founder B per Stock Transfer Agreement. Transferee assumes all obligations including vesting schedule and repurchase option."] - balance_security_id = Some "SSEC-FOUNDER-REMAIN" - consideration_text = Some "Transfer of 75 shares at $0.0001 per share for aggregate consideration of $0.01."] + creates = + [ CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX_STOCK_TRANSFER_2" + date = transferDate + quantity = 75.0 + security_id = securityId + resulting_security_ids = ["SSEC-TRANSFEREE-001"] + comments = ["Stock transfer from Founder A to Founder B per Stock Transfer Agreement. Transferee assumes all obligations including vesting schedule and repurchase option."] + balance_security_id = Some balanceIssuance.security_id + consideration_text = Some "Transfer of 75 shares at $0.0001 per share for aggregate consideration of $0.01." + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestVestingAcceleration.daml b/Test/daml/OpenCapTable/TestVestingAcceleration.daml index 75248cc7..177c424b 100644 --- a/Test/daml/OpenCapTable/TestVestingAcceleration.daml +++ b/Test/daml/OpenCapTable/TestVestingAcceleration.daml @@ -26,7 +26,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = quantity diff --git a/Test/daml/OpenCapTable/TestVestingEvent.daml b/Test/daml/OpenCapTable/TestVestingEvent.daml index 77acc903..f5cbcd49 100644 --- a/Test/daml/OpenCapTable/TestVestingEvent.daml +++ b/Test/daml/OpenCapTable/TestVestingEvent.daml @@ -26,7 +26,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = quantity diff --git a/Test/daml/OpenCapTable/TestVestingStart.daml b/Test/daml/OpenCapTable/TestVestingStart.daml index 20f23760..f4678b13 100644 --- a/Test/daml/OpenCapTable/TestVestingStart.daml +++ b/Test/daml/OpenCapTable/TestVestingStart.daml @@ -26,7 +26,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = quantity @@ -95,7 +95,7 @@ testVestingStart_RSUGrant = script do stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeRSU quantity = 25000.0 diff --git a/Test/daml/OpenCapTable/TestWarrantExercise.daml b/Test/daml/OpenCapTable/TestWarrantExercise.daml index 226709c5..809017b8 100644 --- a/Test/daml/OpenCapTable/TestWarrantExercise.daml +++ b/Test/daml/OpenCapTable/TestWarrantExercise.daml @@ -2,17 +2,34 @@ module OpenCapTable.TestWarrantExercise where import Fairmint.OpenCapTable.OCF.WarrantIssuance import Fairmint.OpenCapTable.OCF.WarrantExercise +import Fairmint.OpenCapTable.Types.Conversion import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) -import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addPrerequisites) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +warrantExerciseTrigger : Decimal -> OcfConversionTrigger +warrantExerciseTrigger convertsToQuantity = + OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = convertsToQuantity + converts_to_future_round = None + converts_to_stock_class_id = Some "SC_COMMON" + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "ELECTIVE_AT_WILL" + type_ = OcfTriggerTypeTypeElectiveAtWill + -- Create a warrant issuance to exercise (follows real workflow) createWarrantIssuance issuer capTableCid securityId qty = submit issuer do @@ -30,7 +47,7 @@ createWarrantIssuance issuer capTableCid securityId qty = quantity = Some qty quantity_source = Some OcfQuantityUnspecified exercise_price = Some (OcfMonetary with amount = 0.01, currency = "USD") - exercise_triggers = [ocfTriggerElectiveAtWill] + exercise_triggers = [warrantExerciseTrigger qty] purchase_price = OcfMonetary with amount = 0.01, currency = "USD" vestings = [] vesting_terms_id = None @@ -43,7 +60,7 @@ createWarrantIssuance issuer capTableCid securityId qty = -- Warrants exercised into common stock testWarrantExercise_BasicExercise = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "WSEC-001" result <- createWarrantIssuance issuer capTableCid securityId 10000.0 _ <- submit issuer do @@ -64,7 +81,7 @@ testWarrantExercise_BasicExercise = script do -- Test 2: Warrant exercise with comments (partial exercise scenario) testWarrantExercise_PartialWithComments = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "WSEC-002" result <- createWarrantIssuance issuer capTableCid securityId 50000.0 _ <- submit issuer do @@ -85,7 +102,7 @@ testWarrantExercise_PartialWithComments = script do -- Test 3: Simple exercise with minimal fields testWarrantExercise_MinimalFields = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "WSEC-003" result <- createWarrantIssuance issuer capTableCid securityId 5000.0 _ <- submit issuer do diff --git a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar index 9821b4b1..736a92a6 100644 --- a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:524b71370b42d579bd3b204c2ca05023da73c1bd4396efe9aa1aabf438576143 -size 2471277 +oid sha256:5f74b5cbcd0221d65802c2a76d87f90a533425f7f5d81005f76c90d89f17060f +size 3024844 diff --git a/dars/dars.lock b/dars/dars.lock index 4718d2f3..5424fa18 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -12,10 +12,10 @@ ] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { - "sha256": "524b71370b42d579bd3b204c2ca05023da73c1bd4396efe9aa1aabf438576143", - "size": 2471277, + "sha256": "5f74b5cbcd0221d65802c2a76d87f90a533425f7f5d81005f76c90d89f17060f", + "size": 3024844, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-14T01:30:05.159Z", + "uploadedAt": "2026-07-14T12:14:38.847Z", "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index f1fc96d6..c12dc515 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -12,12 +12,16 @@ module Fairmint.OpenCapTable.CapTable where import DA.Map (Map) import qualified DA.Map as Map -import DA.List (sortOn) +import DA.List (sortOn, unique) import DA.Action (foldlA, when) import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import qualified Fairmint.OpenCapTable.OCF.ConvertibleIssuance as ConvertibleIssuance (OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) -- OCF Types @@ -116,6 +120,4743 @@ toMaps ct = CapTableMaps with {{map_field}} = ct.{{map_field}} {{/each}} +securityIdsInFinalMaps : CapTableMaps -> Update [Text] +securityIdsInFinalMaps maps = do + convertibleSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.convertible_issuances) + equityCompensationSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.equity_compensation_issuances) + stockSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.stock_issuances) + warrantSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.warrant_issuances) + pure (convertibleSecurityIds ++ equityCompensationSecurityIds ++ stockSecurityIds ++ warrantSecurityIds) + +validateFinalSecurityIdUniqueness : CapTableMaps -> Update () +validateFinalSecurityIdUniqueness maps = do + securityIds <- securityIdsInFinalMaps maps + assertMsg "Security IDs must be globally unique" (unique securityIds) + +validateFinalOcfObjectIdUniqueness : CapTableMaps -> Update () +validateFinalOcfObjectIdUniqueness maps = do +{{#each types_existing}} + {{map_field}}ObjectIds <- foldlA + (\objectIds objectCid -> do + object <- fetch objectCid + pure (object.{{data_param}}.id :: objectIds)) + [] + (Map.values maps.{{map_field}}) + assertMsg + "{{name}} OCF object IDs must be unique within their object type" + (unique {{map_field}}ObjectIds) +{{/each}} +{{#each types_appended}} + {{map_field}}ObjectIds <- foldlA + (\objectIds objectCid -> do + object <- fetch objectCid + pure (object.{{data_param}}.id :: objectIds)) + [] + (Map.values (fromOptional Map.empty maps.{{map_field}})) + assertMsg + "{{name}} OCF object IDs must be unique within their object type" + (unique {{map_field}}ObjectIds) +{{/each}} + +data AuthorizedSharesCeiling + = AuthorizedSharesFinite Decimal + | AuthorizedSharesUnlimited + | AuthorizedSharesNotApplicable + deriving (Eq, Show) + +data StockClassShareEvent + = StockClassShareDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal + | StockClassShareMultiplier with + event_date: Time + event_priority: Int + ratio: OcfRatio + | StockClassShareRetraction with + event_date: Time + event_priority: Int + | StockClassShareTerminalDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal + deriving (Eq, Show) + +data StockClassShareLot = StockClassShareLot with + stock_class_id: Text + security_id: Optional Text + event_date: Time + quantity: Decimal + deriving (Eq, Show) + +data StockConversionResultTerm = StockConversionResultTerm with + stock_class_id: Text + quantity: Decimal + deriving (Eq, Show) + +data StockClassShareEventIndex = StockClassShareEventIndex with + stock_security_events_by_security_id: Map Text [StockClassShareEvent] + stock_class_split_events_by_stock_class_id: Map Text [StockClassShareEvent] + stock_plan_retraction_events_by_security_id: Optional (Map Text [StockClassShareEvent]) + stock_successor_sort_keys_by_security_id: Optional (Map Text (Time, Int, Int)) + deriving (Eq, Show) + +type StockSuccessorEdge = (Text, Time, [Text]) +type StockSuccessorEdgeIndex = Map Text [(Time, [Text])] + +data SecurityQuantityEvent + = SecurityQuantityDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal + | SecurityQuantityTerminalDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal + | SecurityQuantityMultiplier with + event_date: Time + event_priority: Int + ratio: OcfRatio + | SecurityQuantityRetraction with + event_date: Time + event_priority: Int + deriving (Eq, Show) + +data StockPlanSuccessorIndex = StockPlanSuccessorIndex with + stock_successors_by_security_id: Map Text (Time, Optional Text) + equity_compensation_successors_by_security_id: Map Text (Time, Optional Text) + deriving (Eq, Show) + +authorizedSharesCeiling : OcfInitialSharesAuthorized -> AuthorizedSharesCeiling +authorizedSharesCeiling auth = case auth of + OcfInitialSharesNumeric n -> AuthorizedSharesFinite n + OcfInitialSharesEnum OcfAuthorizedSharesUnlimited -> AuthorizedSharesUnlimited + OcfInitialSharesEnum OcfAuthorizedSharesNotApplicable -> AuthorizedSharesNotApplicable + +authorizedSharesLimit : OcfInitialSharesAuthorized -> Optional Decimal +authorizedSharesLimit auth = case authorizedSharesCeiling auth of + AuthorizedSharesFinite n -> Some n + _ -> None + +issuerAuthorizedSharesLimit : Optional OcfInitialSharesAuthorized -> Optional Decimal +issuerAuthorizedSharesLimit initialSharesAuthorized = + authorizedSharesLimit =<< initialSharesAuthorized + +stockClassAuthorizedAdjustmentTakesPrecedence : + StockClassAuthorizedSharesAdjustmentOcfData -> + StockClassAuthorizedSharesAdjustmentOcfData -> + Bool +stockClassAuthorizedAdjustmentTakesPrecedence candidate current = + candidate.date > current.date || + (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) + +issuerAuthorizedAdjustmentTakesPrecedence : + IssuerAuthorizedSharesAdjustmentOcfData -> + IssuerAuthorizedSharesAdjustmentOcfData -> + Bool +issuerAuthorizedAdjustmentTakesPrecedence candidate current = + candidate.date > current.date || + (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) + +stockPlanPoolAdjustmentTakesPrecedence : + StockPlanPoolAdjustmentOcfData -> + StockPlanPoolAdjustmentOcfData -> + Bool +stockPlanPoolAdjustmentTakesPrecedence candidate current = + candidate.date > current.date || + (candidate.date == current.date && candidate.shares_reserved < current.shares_reserved) + +stockClassConversionRatioAdjustmentTakesPrecedence : + StockClassConversionRatioAdjustmentOcfData -> + StockClassConversionRatioAdjustmentOcfData -> + Bool +stockClassConversionRatioAdjustmentTakesPrecedence candidate current = + candidate.date > current.date || + (candidate.date == current.date && stockClassConversionRatioAdjustmentIncreasesShares candidate current) + +stockClassConversionRatioAdjustmentIncreasesShares : + StockClassConversionRatioAdjustmentOcfData -> + StockClassConversionRatioAdjustmentOcfData -> + Bool +stockClassConversionRatioAdjustmentIncreasesShares candidate current = + let candidateRatio = candidate.new_ratio_conversion_mechanism.ratio in + let currentRatio = current.new_ratio_conversion_mechanism.ratio in + candidateRatio.numerator * currentRatio.denominator > currentRatio.numerator * candidateRatio.denominator + +addDateById : Text -> Time -> Map Text [Time] -> Map Text [Time] +addDateById key date index = + Map.insert key (date :: fromOptional [] (Map.lookup key index)) index + +addIssuerAuthorizedAdjustmentById : + Text -> + IssuerAuthorizedSharesAdjustmentOcfData -> + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] +addIssuerAuthorizedAdjustmentById key adjustment index = + Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index + +addStockClassAuthorizedAdjustmentById : + Text -> + StockClassAuthorizedSharesAdjustmentOcfData -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] +addStockClassAuthorizedAdjustmentById key adjustment index = + Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index + +addStockPlanPoolAdjustmentById : + Text -> + StockPlanPoolAdjustmentOcfData -> + Map Text [StockPlanPoolAdjustmentOcfData] -> + Map Text [StockPlanPoolAdjustmentOcfData] +addStockPlanPoolAdjustmentById key adjustment index = + Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index + +addStockClassShareLotByStockClass : + StockClassShareLot -> + Map Text [StockClassShareLot] -> + Map Text [StockClassShareLot] +addStockClassShareLotByStockClass lot index = + Map.insert lot.stock_class_id (lot :: fromOptional [] (Map.lookup lot.stock_class_id index)) index + +allResultingStockIssuancesExist : CapTableMaps -> [Text] -> Bool +allResultingStockIssuancesExist maps securityIds = + not (null securityIds) && all (\securityId -> isSome (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds + +stockBalanceSecurityIssuanceExists : CapTableMaps -> Optional Text -> Bool +stockBalanceSecurityIssuanceExists maps balanceSecurityId = case balanceSecurityId of + Some securityId -> isSome (Map.lookup securityId maps.stock_issuances_by_security_id) + None -> True + +equityCompensationBalanceSecurityIssuanceExists : CapTableMaps -> Optional Text -> Bool +equityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId = case balanceSecurityId of + Some securityId -> isSome (Map.lookup securityId maps.equity_compensation_issuances_by_security_id) + None -> True + +noEquityCompensationBalanceSecurityIssuanceExists : CapTableMaps -> Optional Text -> Bool +noEquityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId = case balanceSecurityId of + Some securityId -> isNone (Map.lookup securityId maps.equity_compensation_issuances_by_security_id) + None -> True + +allResultingEquityCompensationIssuancesExist : CapTableMaps -> [Text] -> Bool +allResultingEquityCompensationIssuancesExist maps securityIds = + not (null securityIds) && all (\securityId -> isSome (Map.lookup securityId maps.equity_compensation_issuances_by_security_id)) securityIds + +noResultingEquityCompensationIssuancesExist : CapTableMaps -> [Text] -> Bool +noResultingEquityCompensationIssuancesExist maps securityIds = + all (\securityId -> isNone (Map.lookup securityId maps.equity_compensation_issuances_by_security_id)) securityIds + +assertNoPartialResultingEquityCompensationIssuanceCoverage : Text -> CapTableMaps -> [Text] -> Update () +assertNoPartialResultingEquityCompensationIssuanceCoverage label maps securityIds = + let hasPartialCoverage = + not (null securityIds) + && not (allResultingEquityCompensationIssuancesExist maps securityIds) + && not (noResultingEquityCompensationIssuancesExist maps securityIds) + in assertMsg (label <> " has only some resulting equity compensation issuances recorded") (not hasPartialCoverage) + +allEquityCompensationSuccessorIssuancesExist : CapTableMaps -> [Text] -> Optional Text -> Bool +allEquityCompensationSuccessorIssuancesExist maps resultingSecurityIds balanceSecurityId = + allResultingEquityCompensationIssuancesExist maps resultingSecurityIds + && equityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId + +noEquityCompensationSuccessorIssuancesExist : CapTableMaps -> [Text] -> Optional Text -> Bool +noEquityCompensationSuccessorIssuancesExist maps resultingSecurityIds balanceSecurityId = + noResultingEquityCompensationIssuancesExist maps resultingSecurityIds + && noEquityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId + +assertNoPartialEquityCompensationSuccessorCoverage : + Text -> + CapTableMaps -> + [Text] -> + Optional Text -> + Update () +assertNoPartialEquityCompensationSuccessorCoverage label maps resultingSecurityIds balanceSecurityId = do + assertNoPartialResultingEquityCompensationIssuanceCoverage label maps resultingSecurityIds + assertMsg + (label <> " has only some successor equity compensation issuances recorded") + ( allEquityCompensationSuccessorIssuancesExist maps resultingSecurityIds balanceSecurityId + || noEquityCompensationSuccessorIssuancesExist maps resultingSecurityIds balanceSecurityId + ) + +noResultingStockIssuancesExist : CapTableMaps -> [Text] -> Bool +noResultingStockIssuancesExist maps securityIds = + all (\securityId -> isNone (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds + +assertNoPartialResultingStockIssuanceCoverage : Text -> CapTableMaps -> [Text] -> Update () +assertNoPartialResultingStockIssuanceCoverage label maps securityIds = + let hasPartialCoverage = + not (null securityIds) + && not (allResultingStockIssuancesExist maps securityIds) + && not (noResultingStockIssuancesExist maps securityIds) + in assertMsg (label <> " has only some resulting stock issuances recorded") (not hasPartialCoverage) + +assertAllResultingStockIssuancesExist : Text -> CapTableMaps -> [Text] -> Update () +assertAllResultingStockIssuancesExist label maps securityIds = + assertMsg (label <> " is missing a resulting stock issuance") (allResultingStockIssuancesExist maps securityIds) + +stockIssuanceShareLot : StockIssuanceOcfData -> StockClassShareLot +stockIssuanceShareLot issuanceData = + StockClassShareLot with + stock_class_id = issuanceData.stock_class_id + security_id = Some issuanceData.security_id + event_date = issuanceData.date + quantity = issuanceData.quantity + +stockClassDataIndex : CapTableMaps -> Update (Map Text StockClassOcfData) +stockClassDataIndex maps = + foldlA + (\index stockClassCid -> do + stockClass <- fetch stockClassCid + pure (Map.insert stockClass.stock_class_data.id stockClass.stock_class_data index)) + Map.empty + (Map.values maps.stock_classes) + +stockClassDataList : CapTableMaps -> Update [StockClassOcfData] +stockClassDataList maps = + foldlA + (\stockClasses stockClassCid -> do + stockClass <- fetch stockClassCid + pure (stockClass.stock_class_data :: stockClasses)) + [] + (Map.values maps.stock_classes) + +stockClassConversionRatioAdjustmentAsOf : + CapTableMaps -> + Text -> + Time -> + Update (Optional StockClassConversionRatioAdjustmentOcfData) +stockClassConversionRatioAdjustmentAsOf maps stockClassId asOf = + foldlA + (\latest adjustmentCid -> do + adjustment <- fetch adjustmentCid + let candidate = adjustment.adjustment_data + if candidate.stock_class_id == stockClassId && candidate.date <= asOf + then case latest of + Some current -> + if stockClassConversionRatioAdjustmentTakesPrecedence candidate current + then pure (Some candidate) + else pure latest + None -> pure (Some candidate) + else pure latest) + None + (Map.values maps.stock_class_conversion_ratio_adjustments) + +stockClassConversionOutputQuantity : + CapTableMaps -> + StockClassOcfData -> + OcfStockClassConversionRight -> + StockConversionOcfData -> + Update (Optional Decimal) +stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData = + case conversionRight.conversion_mechanism of + OcfConversionMechanismRatioConversion -> do + adjustmentOpt <- stockClassConversionRatioAdjustmentAsOf maps sourceStockClass.id conversionData.date + let ratioOpt = case adjustmentOpt of + Some adjustment -> Some adjustment.new_ratio_conversion_mechanism.ratio + None -> conversionRight.ratio + case ratioOpt of + Some ratio -> pure (Some (conversionData.quantity_converted * ratio.numerator / ratio.denominator)) + None -> pure None + _ -> pure None + +stockClassConversionRightDateEligible : Time -> OcfStockClassConversionRight -> Bool +stockClassConversionRightDateEligible conversionDate conversionRight = + let trigger = conversionRight.conversion_trigger + beforeExpiration = case conversionRight.expires_at of + Some expiresAt -> conversionDate <= expiresAt + None -> True + triggerDateEligible = case trigger.type_ of + OcfTriggerTypeTypeAutomaticOnDate -> trigger.trigger_date == Some conversionDate + OcfTriggerTypeTypeElectiveInRange -> case (trigger.start_date, trigger.end_date) of + (Some startDate, Some endDate) -> startDate <= conversionDate && conversionDate <= endDate + _ -> False + _ -> True + in beforeExpiration && triggerDateEligible + +stockConversionResultTerms : + CapTableMaps -> + Map Text StockClassOcfData -> + StockIssuanceOcfData -> + StockConversionOcfData -> + Update [StockConversionResultTerm] +stockConversionResultTerms maps stockClassIndex sourceIssuanceData conversionData = + case Map.lookup sourceIssuanceData.stock_class_id stockClassIndex of + Some sourceStockClass -> do + terms <- foldlA + (\resultTerms conversionRight -> + if stockClassConversionRightDateEligible conversionData.date conversionRight + then do + quantityOpt <- stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData + case quantityOpt of + Some quantity -> do + assertMsg + ("Stock conversion output references missing stock class: " <> conversionRight.converts_to_stock_class_id) + (isSome (Map.lookup conversionRight.converts_to_stock_class_id stockClassIndex)) + pure ((StockConversionResultTerm with + stock_class_id = conversionRight.converts_to_stock_class_id + quantity = quantity) :: resultTerms) + None -> pure resultTerms + else pure resultTerms) + [] + sourceStockClass.conversion_rights + assertMsg + ("Stock conversion has no computable date-eligible conversion right: " <> conversionData.id) + (not (null terms)) + pure terms + None -> do + assertMsg ("Stock conversion source stock class not found: " <> sourceIssuanceData.stock_class_id) False + pure [] + +stockConversionOutputShareLot : + CapTableMaps -> + Map Text StockClassOcfData -> + StockIssuanceOcfData -> + StockConversionOcfData -> + Update (Optional StockClassShareLot) +stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversionData = do + resultTerms <- stockConversionResultTerms maps stockClassIndex sourceIssuanceData conversionData + case resultTerms of + [resultTerm] -> pure (Some (StockClassShareLot with + stock_class_id = resultTerm.stock_class_id + security_id = None + event_date = conversionData.date + quantity = resultTerm.quantity)) + [] -> pure None + _ -> do + assertMsg + ("Stock conversion output cannot be reconstructed without explicit results because multiple conversion rights are date-eligible: " <> conversionData.id) + False + pure None + +warrantExerciseTrigger : WarrantIssuanceOcfData -> Text -> Optional OcfConversionTrigger +warrantExerciseTrigger issuanceData triggerId = + foldl + (\matched trigger -> + case matched of + Some _ -> matched + None -> if trigger.trigger_id == triggerId then Some trigger else None) + None + issuanceData.exercise_triggers + +convertibleConversionTrigger : ConvertibleIssuanceOcfData -> Text -> Optional ConvertibleIssuance.OcfConvertibleConversionTrigger +convertibleConversionTrigger issuanceData triggerId = + foldl + (\matched trigger -> + case matched of + Some _ -> matched + None -> if trigger.trigger_id == triggerId then Some trigger else None) + None + issuanceData.conversion_triggers + +convertibleConversionOutputQuantity : + ConvertibleIssuanceOcfData -> + ConvertibleConversionOcfData -> + OcfConvertibleConversionRight -> + Update (Optional Decimal) +convertibleConversionOutputQuantity issuanceData conversionData conversionRight = do + let issuedUnits = issuanceData.investment_amount.amount + assertMsg + ("Convertible conversion source has no positive unit base: " <> issuanceData.security_id) + (issuedUnits > 0.0) + case (conversionData.balance_security_id, conversionData.quantity_converted) of + (Some _, None) -> + assertMsg + ("Partial convertible conversion requires quantity_converted: " <> conversionData.id) + False + _ -> pure () + let convertedUnits = fromOptional issuedUnits conversionData.quantity_converted + assertMsg + ("Convertible conversion quantity exceeds source units for " <> conversionData.id <> ": converted " <> show convertedUnits <> " exceeds " <> show issuedUnits) + (convertedUnits > 0.0 && convertedUnits <= issuedUnits) + let outputQuantityOpt = case conversionRight.conversion_mechanism of + OcfConvMechFixedAmount fixedAmount -> + Some (fixedAmount.converts_to_quantity * convertedUnits / issuedUnits) + OcfConvMechRatio ratio -> + Some (convertedUnits * ratio.numerator / ratio.denominator) + _ -> None + case outputQuantityOpt of + Some outputQuantity -> do + assertNonNegativeQuantity + ("Convertible conversion output quantity for " <> conversionData.id) + outputQuantity + pure (Some outputQuantity) + None -> pure None + +convertibleConversionExpectedResult : + ConvertibleIssuanceOcfData -> + ConvertibleConversionOcfData -> + Update (Optional Text, Optional Decimal) +convertibleConversionExpectedResult issuanceData conversionData = + case convertibleConversionTrigger issuanceData conversionData.trigger_id of + Some trigger -> do + quantityOpt <- convertibleConversionOutputQuantity issuanceData conversionData trigger.conversion_right + pure (trigger.conversion_right.converts_to_stock_class_id, quantityOpt) + None -> do + assertMsg + ("Convertible conversion trigger not found: " <> conversionData.trigger_id) + False + pure (None, None) + +convertibleConversionOutputShareLot : + Map Text StockClassOcfData -> + ConvertibleIssuanceOcfData -> + ConvertibleConversionOcfData -> + Update (Optional StockClassShareLot) +convertibleConversionOutputShareLot stockClassIndex issuanceData conversionData = do + (stockClassIdOpt, quantityOpt) <- convertibleConversionExpectedResult issuanceData conversionData + case (stockClassIdOpt, quantityOpt) of + (Some stockClassId, Some quantity) -> do + assertMsg + ("Convertible conversion output references missing stock class: " <> stockClassId) + (isSome (Map.lookup stockClassId stockClassIndex)) + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = conversionData.date + quantity = quantity)) + (None, _) -> do + assertMsg + ("Convertible conversion output cannot be reconstructed because trigger has no destination stock class: " <> conversionData.trigger_id) + False + pure None + (_, None) -> do + assertMsg + ("Convertible conversion output cannot be reconstructed for its conversion mechanism; resulting stock issuances are required: " <> conversionData.id) + False + pure None + +securityQuantityReductionEvent : Time -> Decimal -> Optional Text -> SecurityQuantityEvent +securityQuantityReductionEvent eventDate quantity balanceSecurityId = + case balanceSecurityId of + Some _ -> + SecurityQuantityTerminalDelta with + event_date = eventDate + event_priority = 2 + quantity_delta = 0.0 - quantity + None -> + SecurityQuantityDelta with + event_date = eventDate + event_priority = 1 + quantity_delta = 0.0 - quantity + +securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int, Int) +securityQuantityEventSortKey event = case event of + SecurityQuantityDelta with event_date; event_priority -> (event_date, event_priority, 0) + SecurityQuantityTerminalDelta with event_date; event_priority -> (event_date, event_priority, 1) + SecurityQuantityMultiplier with event_date; event_priority -> (event_date, event_priority, 2) + SecurityQuantityRetraction with event_date; event_priority -> (event_date, event_priority, 3) + +securityQuantityEventDate : SecurityQuantityEvent -> Time +securityQuantityEventDate event = case event of + SecurityQuantityDelta with event_date -> event_date + SecurityQuantityTerminalDelta with event_date -> event_date + SecurityQuantityMultiplier with event_date -> event_date + SecurityQuantityRetraction with event_date -> event_date + +assertSecurityQuantityEventsOnOrAfter : Text -> Time -> [SecurityQuantityEvent] -> Update () +assertSecurityQuantityEventsOnOrAfter securityId earliest events = + mapA_ + (\event -> + assertMsg + ("Security quantity event cannot precede issuance for " <> securityId <> ": event date " <> show (securityQuantityEventDate event) <> " is before " <> show earliest) + (securityQuantityEventDate event >= earliest)) + events + +validateSecurityQuantityBalanceEvent : Text -> Optional Decimal -> SecurityQuantityEvent -> Update (Optional Decimal) +validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentOpt of + Some current -> case event of + SecurityQuantityDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure (Some next) + SecurityQuantityTerminalDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Security terminal reduction requires a positive balance for " <> securityId <> ": current balance is " <> show current) + (current > 0.0) + assertMsg + ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure (Some 0.0) + SecurityQuantityMultiplier with ratio -> + pure (Some (current * ratio.numerator / ratio.denominator)) + SecurityQuantityRetraction _ _ -> do + assertMsg + ("Security retraction requires a positive balance for " <> securityId <> ": current balance is " <> show current) + (current > 0.0) + pure (Some 0.0) + None -> case event of + SecurityQuantityDelta {} -> pure None + SecurityQuantityTerminalDelta {} -> pure (Some 0.0) + SecurityQuantityMultiplier {} -> pure None + SecurityQuantityRetraction _ _ -> pure (Some 0.0) + +validateSecurityQuantityBalance : Text -> Optional Decimal -> [SecurityQuantityEvent] -> Update () +validateSecurityQuantityBalance securityId initialQuantity events = do + _ <- foldlA + (validateSecurityQuantityBalanceEvent securityId) + initialQuantity + (sortOn securityQuantityEventSortKey events) + pure () + +securityQuantityBeforeEvent : + Text -> + Optional Decimal -> + [SecurityQuantityEvent] -> + (Time, Int, Int) -> + Update (Optional Decimal) +securityQuantityBeforeEvent securityId initialQuantity events targetSortKey = + foldlA + (validateSecurityQuantityBalanceEvent securityId) + initialQuantity + (filter + (\event -> securityQuantityEventSortKey event < targetSortKey) + (sortOn securityQuantityEventSortKey events)) + +warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [SecurityQuantityEvent] +warrantSecurityQuantityEvents maps issuanceData = do + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == issuanceData.security_id + then pure (securityQuantityReductionEvent transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id :: events) + else pure events) + [] + (Map.values maps.warrant_transfers) + + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + then pure (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id :: events) + else pure events) + [] + (Map.values maps.warrant_cancellations) + + exerciseEvents <- foldlA + (\events exerciseCid -> do + exercise <- fetch exerciseCid + if exercise.exercise_data.security_id == issuanceData.security_id + then case exercise.exercise_data.quantity of + Some quantity -> + pure ((SecurityQuantityDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - quantity) + :: events) + None -> + pure ((SecurityQuantityRetraction with + event_date = exercise.exercise_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.warrant_exercises) + + retractionEvents <- foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.warrant_retractions) + + pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ retractionEvents) + +validateWarrantSecurityBalance : CapTableMaps -> WarrantIssuanceOcfData -> Update () +validateWarrantSecurityBalance maps issuanceData = do + events <- warrantSecurityQuantityEvents maps issuanceData + case issuanceData.quantity of + Some quantity -> + validateSecurityQuantityBalance + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = quantity) + :: events) + None -> do + assertSecurityQuantityEventsOnOrAfter issuanceData.security_id issuanceData.date events + validateSecurityQuantityBalance issuanceData.security_id None events + +validateWarrantSecurityBalances : CapTableMaps -> Update () +validateWarrantSecurityBalances maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateWarrantSecurityBalance maps issuance.issuance_data) + (Map.values maps.warrant_issuances) + +warrantSecurityQuantityBeforeTerminal : + CapTableMaps -> + WarrantIssuanceOcfData -> + Time -> + Update (Optional Decimal) +warrantSecurityQuantityBeforeTerminal maps issuanceData eventDate = do + events <- warrantSecurityQuantityEvents maps issuanceData + case issuanceData.quantity of + Some quantity -> + securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = quantity) + :: events + ) + (eventDate, 2, 1) + None -> + securityQuantityBeforeEvent issuanceData.security_id None events (eventDate, 2, 1) + +warrantExerciseQuantity : CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> OcfWarrantConversionRight -> Update (Optional Decimal) +warrantExerciseQuantity maps issuanceData exerciseData conversionRight = + case conversionRight.conversion_mechanism of + OcfWarrantMechanismFixedAmount with converts_to_quantity -> do + exercisedQuantityOpt <- case exerciseData.quantity of + Some exercisedQuantity -> pure (Some exercisedQuantity) + None -> warrantSecurityQuantityBeforeTerminal maps issuanceData exerciseData.date + case (issuanceData.quantity, exercisedQuantityOpt) of + (Some issuedQuantity, Some exercisedQuantity) -> + if issuedQuantity > 0.0 + then pure (Some (converts_to_quantity * exercisedQuantity / issuedQuantity)) + else pure None + _ -> pure None + _ -> pure None + +warrantExerciseOutputShareLot : + CapTableMaps -> + WarrantIssuanceOcfData -> + WarrantExerciseOcfData -> + Update (Optional StockClassShareLot) +warrantExerciseOutputShareLot maps issuanceData exerciseData = + case warrantExerciseTrigger issuanceData exerciseData.trigger_id of + Some trigger -> + case trigger.conversion_right of + OcfRightWarrant conversionRight -> + case conversionRight.converts_to_stock_class_id of + Some stockClassId -> + case conversionRight.conversion_mechanism of + OcfWarrantMechanismFixedAmount _ -> do + quantityOpt <- warrantExerciseQuantity maps issuanceData exerciseData conversionRight + case quantityOpt of + Some quantity -> + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = quantity)) + None -> do + assertMsg ("Warrant exercise output cannot be reconstructed because its fixed conversion quantity is not computable: " <> exerciseData.id) False + pure None + _ -> do + assertMsg ("Warrant exercise output cannot be reconstructed for a non-fixed conversion mechanism; a resulting stock issuance is required: " <> exerciseData.id) False + pure None + None -> do + assertMsg ("Warrant exercise output cannot be reconstructed because trigger has no destination stock class: " <> exerciseData.trigger_id) False + pure None + _ -> do + assertMsg ("Warrant exercise output cannot be reconstructed because trigger is not a warrant conversion right: " <> exerciseData.trigger_id) False + pure None + None -> do + assertMsg ("Warrant exercise output cannot be reconstructed because trigger does not exist on warrant issuance: " <> exerciseData.trigger_id) False + pure None + +equityCompensationOutputStockClassId : + CapTableMaps -> + Text -> + EquityCompensationIssuanceOcfData -> + Update (Optional Text) +equityCompensationOutputStockClassId maps label issuanceData = + case issuanceData.compensation_type of + OcfCompensationTypeCSAR -> pure None + _ -> case issuanceData.stock_class_id of + Some stockClassId -> pure (Some stockClassId) + None -> case issuanceData.stock_plan_id of + Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + case stockPlan.plan_data.stock_class_ids of + [stockClassId] -> pure (Some stockClassId) + [] -> do + assertMsg (label <> " cannot infer an output stock class because stock plan " <> stockPlanId <> " has no stock class") False + pure None + _ -> do + assertMsg (label <> " cannot infer an output stock class because stock plan " <> stockPlanId <> " has multiple stock classes") False + pure None + None -> do + assertMsg (label <> " cannot infer an output stock class because stock plan " <> stockPlanId <> " does not exist") False + pure None + None -> do + assertMsg (label <> " cannot infer an output stock class because its source issuance has neither stock_class_id nor stock_plan_id") False + pure None + +equityCompensationExerciseOutputShareLot : + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + EquityCompensationExerciseOcfData -> + Update (Optional StockClassShareLot) +equityCompensationExerciseOutputShareLot maps issuanceData exerciseData = do + stockClassIdOpt <- equityCompensationOutputStockClassId + maps + ("Equity compensation exercise " <> exerciseData.id) + issuanceData + case stockClassIdOpt of + Some stockClassId -> do + assertNonNegativeQuantity ("Equity compensation exercise output quantity for " <> exerciseData.id) exerciseData.quantity + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = exerciseData.quantity)) + None -> pure None + +equityCompensationReleaseOutputShareLot : + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + EquityCompensationReleaseOcfData -> + Update (Optional StockClassShareLot) +equityCompensationReleaseOutputShareLot maps issuanceData releaseData = do + stockClassIdOpt <- equityCompensationOutputStockClassId + maps + ("Equity compensation release " <> releaseData.id) + issuanceData + pure $ case stockClassIdOpt of + Some stockClassId -> Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = releaseData.settlement_date + quantity = releaseData.quantity) + None -> None + +validateStockResultIssuances : + CapTableMaps -> + Text -> + Time -> + Optional Text -> + Optional Decimal -> + [Text] -> + Update () +validateStockResultIssuances maps label expectedDate expectedStockClassIdOpt expectedQuantityOpt securityIds = do + assertMsg (label <> " has no resulting stock issuance") (not (null securityIds)) + actualQuantity <- foldlA + (\total securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " result date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == expectedDate) + case expectedStockClassIdOpt of + Some expectedStockClassId -> + assertMsg + (label <> " result stock class mismatch for " <> securityId <> ": expected " <> expectedStockClassId <> ", found " <> issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id == expectedStockClassId) + None -> pure () + assertNonNegativeQuantity (label <> " result quantity for " <> securityId) issuance.issuance_data.quantity + pure (total + issuance.issuance_data.quantity) + None -> do + assertMsg (label <> " result stock issuance not found: " <> securityId) False + pure total) + 0.0 + securityIds + case expectedQuantityOpt of + Some expectedQuantity -> + assertMsg + (label <> " resulting quantity mismatch: expected " <> show expectedQuantity <> ", found " <> show actualQuantity) + (actualQuantity == expectedQuantity) + None -> pure () + +validateStockConversionResultIssuances : + CapTableMaps -> + StockConversionOcfData -> + [StockConversionResultTerm] -> + Update () +validateStockConversionResultIssuances maps conversionData expectedTerms = do + (actualStockClassIdOpt, actualQuantity) <- foldlA + (\(stockClassIdOpt, total) securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Stock conversion result date mismatch for " <> securityId <> ": expected " <> show conversionData.date <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == conversionData.date) + case stockClassIdOpt of + Some stockClassId -> + assertMsg + ("Stock conversion results must use one stock class for " <> conversionData.id) + (issuance.issuance_data.stock_class_id == stockClassId) + None -> pure () + assertNonNegativeQuantity ("Stock conversion result quantity for " <> securityId) issuance.issuance_data.quantity + pure (Some issuance.issuance_data.stock_class_id, total + issuance.issuance_data.quantity) + None -> do + assertMsg ("Stock conversion result security not found: " <> securityId) False + pure (stockClassIdOpt, total)) + (None, 0.0) + conversionData.resulting_security_ids + case actualStockClassIdOpt of + Some actualStockClassId -> + assertMsg + ("Stock conversion results do not match any date-eligible conversion right for " <> conversionData.id <> ": found class " <> actualStockClassId <> " and quantity " <> show actualQuantity) + (any + (\expectedTerm -> + expectedTerm.stock_class_id == actualStockClassId && + expectedTerm.quantity == actualQuantity) + expectedTerms) + None -> + assertMsg ("Stock conversion has no resulting security: " <> conversionData.id) False + +validateWarrantExerciseResultIssuances : CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> Update () +validateWarrantExerciseResultIssuances maps issuanceData exerciseData = + case warrantExerciseTrigger issuanceData exerciseData.trigger_id of + Some trigger -> case trigger.conversion_right of + OcfRightWarrant conversionRight -> do + expectedQuantityOpt <- warrantExerciseQuantity maps issuanceData exerciseData conversionRight + validateStockResultIssuances + maps + ("Warrant exercise " <> exerciseData.id) + exerciseData.date + conversionRight.converts_to_stock_class_id + expectedQuantityOpt + exerciseData.resulting_security_ids + _ -> assertMsg ("Warrant exercise trigger is not a warrant conversion right: " <> exerciseData.trigger_id) False + None -> assertMsg ("Warrant exercise trigger not found: " <> exerciseData.trigger_id) False + +validateConvertibleConversionResultIssuances : CapTableMaps -> ConvertibleIssuanceOcfData -> ConvertibleConversionOcfData -> Update () +validateConvertibleConversionResultIssuances maps issuanceData conversionData = do + (expectedStockClassIdOpt, expectedQuantityOpt) <- convertibleConversionExpectedResult issuanceData conversionData + validateStockResultIssuances + maps + ("Convertible conversion " <> conversionData.id) + conversionData.date + expectedStockClassIdOpt + expectedQuantityOpt + conversionData.resulting_security_ids + +validateEquityCompensationExerciseResultIssuances : CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationExerciseOcfData -> Update () +validateEquityCompensationExerciseResultIssuances maps issuanceData exerciseData = do + let label = "Equity compensation exercise " <> exerciseData.id + case issuanceData.compensation_type of + OcfCompensationTypeCSAR -> + assertMsg (label <> " cannot produce stock for cash-settled stock appreciation rights") False + _ -> do + expectedStockClassId <- equityCompensationOutputStockClassId maps label issuanceData + validateStockResultIssuances + maps + label + exerciseData.date + expectedStockClassId + (Some exerciseData.quantity) + exerciseData.resulting_security_ids + +validateEquityCompensationReleaseResultIssuances : CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationReleaseOcfData -> Update () +validateEquityCompensationReleaseResultIssuances maps issuanceData releaseData = do + let label = "Equity compensation release " <> releaseData.id + case issuanceData.compensation_type of + OcfCompensationTypeCSAR -> + assertMsg (label <> " cannot produce stock for cash-settled stock appreciation rights") False + _ -> do + expectedStockClassId <- equityCompensationOutputStockClassId maps label issuanceData + validateStockResultIssuances + maps + label + releaseData.settlement_date + expectedStockClassId + (Some releaseData.quantity) + releaseData.resulting_security_ids + +addOptionalStockClassShareLot : + Optional StockClassShareLot -> + Map Text [StockClassShareLot] -> + Map Text [StockClassShareLot] +addOptionalStockClassShareLot lotOpt index = + case lotOpt of + Some lot -> addStockClassShareLotByStockClass lot index + None -> index + +stockClassShareLotsByStockClass : + CapTableMaps -> + Map Text StockClassOcfData -> + Update (Map Text [StockClassShareLot]) +stockClassShareLotsByStockClass maps stockClassIndex = do + stockIssuanceLots <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure (addStockClassShareLotByStockClass (stockIssuanceShareLot issuance.issuance_data) index)) + Map.empty + (Map.values maps.stock_issuances) + + stockTransferLots <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + let label = "Stock transfer " <> transfer.transfer_data.id + assertNoPartialResultingStockIssuanceCoverage label maps transfer.transfer_data.resulting_security_ids + if allResultingStockIssuancesExist maps transfer.transfer_data.resulting_security_ids + then pure index + else case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + pure (addStockClassShareLotByStockClass + (StockClassShareLot with + stock_class_id = sourceIssuance.issuance_data.stock_class_id + security_id = None + event_date = transfer.transfer_data.date + quantity = transfer.transfer_data.quantity) + index) + None -> do + assertMsg (label <> " references missing source stock issuance: " <> transfer.transfer_data.security_id) False + pure index) + stockIssuanceLots + (Map.values maps.stock_transfers) + + stockConversionLots <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + assertNoPartialResultingStockIssuanceCoverage ("Stock conversion " <> conversion.conversion_data.id) maps conversion.conversion_data.resulting_security_ids + if allResultingStockIssuancesExist maps conversion.conversion_data.resulting_security_ids + then case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + expectedTerms <- stockConversionResultTerms maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + validateStockConversionResultIssuances maps conversion.conversion_data expectedTerms + pure index + None -> do + assertMsg ("Stock conversion references missing source stock issuance: " <> conversion.conversion_data.security_id) False + pure index + else case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + lotOpt <- stockConversionOutputShareLot maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> do + assertMsg ("Stock conversion references missing source stock issuance: " <> conversion.conversion_data.security_id) False + pure index) + stockTransferLots + (Map.values maps.stock_conversions) + + convertibleConversionLots <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + let label = "Convertible conversion " <> conversion.conversion_data.id + assertNoPartialResultingStockIssuanceCoverage label maps conversion.conversion_data.resulting_security_ids + case Map.lookup conversion.conversion_data.security_id maps.convertible_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + if allResultingStockIssuancesExist maps conversion.conversion_data.resulting_security_ids + then do + validateConvertibleConversionResultIssuances maps sourceIssuance.issuance_data conversion.conversion_data + pure index + else do + lotOpt <- convertibleConversionOutputShareLot stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> do + assertMsg (label <> " references missing source convertible issuance: " <> conversion.conversion_data.security_id) False + pure index) + stockConversionLots + (Map.values maps.convertible_conversions) + + warrantExerciseLots <- foldlA + (\index exerciseCid -> do + exercise <- fetch exerciseCid + assertNoPartialResultingStockIssuanceCoverage ("Warrant exercise " <> exercise.exercise_data.id) maps exercise.exercise_data.resulting_security_ids + if allResultingStockIssuancesExist maps exercise.exercise_data.resulting_security_ids + then case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + validateWarrantExerciseResultIssuances maps issuance.issuance_data exercise.exercise_data + pure index + None -> do + assertMsg ("Warrant exercise references missing warrant issuance: " <> exercise.exercise_data.security_id) False + pure index + else case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + lotOpt <- warrantExerciseOutputShareLot maps issuance.issuance_data exercise.exercise_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> do + assertMsg ("Warrant exercise references missing warrant issuance: " <> exercise.exercise_data.security_id) False + pure index) + convertibleConversionLots + (Map.values maps.warrant_exercises) + + equityExerciseLots <- foldlA + (\index exerciseCid -> do + exercise <- fetch exerciseCid + assertNoPartialResultingStockIssuanceCoverage ("Equity compensation exercise " <> exercise.exercise_data.id) maps exercise.exercise_data.resulting_security_ids + if allResultingStockIssuancesExist maps exercise.exercise_data.resulting_security_ids + then case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + validateEquityCompensationExerciseResultIssuances maps issuance.issuance_data exercise.exercise_data + pure index + None -> do + assertMsg ("Equity compensation exercise references missing issuance: " <> exercise.exercise_data.security_id) False + pure index + else case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + lotOpt <- equityCompensationExerciseOutputShareLot maps issuance.issuance_data exercise.exercise_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> do + assertMsg ("Equity compensation exercise references missing issuance: " <> exercise.exercise_data.security_id) False + pure index) + warrantExerciseLots + (Map.values maps.equity_compensation_exercises) + + foldlA + (\index releaseCid -> do + release <- fetch releaseCid + assertNoPartialResultingStockIssuanceCoverage ("Equity compensation release " <> release.release_data.id) maps release.release_data.resulting_security_ids + if allResultingStockIssuancesExist maps release.release_data.resulting_security_ids + then case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + validateEquityCompensationReleaseResultIssuances maps issuance.issuance_data release.release_data + pure index + None -> do + assertMsg ("Equity compensation release references missing issuance: " <> release.release_data.security_id) False + pure index + else case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + lotOpt <- equityCompensationReleaseOutputShareLot maps issuance.issuance_data release.release_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> do + assertMsg ("Equity compensation release references missing issuance: " <> release.release_data.security_id) False + pure index) + equityExerciseLots + (Map.values maps.equity_compensation_releases) + +stockClassAuthorizedAdjustmentIndex : CapTableMaps -> Update (Map Text StockClassAuthorizedSharesAdjustmentOcfData) +stockClassAuthorizedAdjustmentIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + let stockClassId = adjustment.adjustment_data.stock_class_id + let nextIndex = case Map.lookup stockClassId index of + Some current -> + if stockClassAuthorizedAdjustmentTakesPrecedence adjustment.adjustment_data current + then Map.insert stockClassId adjustment.adjustment_data index + else index + None -> Map.insert stockClassId adjustment.adjustment_data index + pure nextIndex) + Map.empty + (Map.values maps.stock_class_authorized_shares_adjustments) + +stockClassAuthorizedAdjustmentHistoryIndex : CapTableMaps -> Update (Map Text [StockClassAuthorizedSharesAdjustmentOcfData]) +stockClassAuthorizedAdjustmentHistoryIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addStockClassAuthorizedAdjustmentById adjustment.adjustment_data.stock_class_id adjustment.adjustment_data index)) + Map.empty + (Map.values maps.stock_class_authorized_shares_adjustments) + +stockClassAuthorizedAdjustmentAsOf : + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Text -> + Time -> + Optional StockClassAuthorizedSharesAdjustmentOcfData +stockClassAuthorizedAdjustmentAsOf adjustmentHistory stockClassId asOf = + foldl + (\latest candidate -> + if candidate.date <= asOf + then case latest of + Some current -> + if stockClassAuthorizedAdjustmentTakesPrecedence candidate current + then Some candidate + else latest + None -> Some candidate + else latest) + None + (fromOptional [] (Map.lookup stockClassId adjustmentHistory)) + +currentStockClassAuthorizedCeiling : + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + StockClassOcfData -> + AuthorizedSharesCeiling +currentStockClassAuthorizedCeiling adjustmentIndex stockClassData = + case Map.lookup stockClassData.id adjustmentIndex of + Some adjustment -> AuthorizedSharesFinite adjustment.new_shares_authorized + None -> authorizedSharesCeiling stockClassData.initial_shares_authorized + +currentStockClassAuthorizedLimit : + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + StockClassOcfData -> + Optional Decimal +currentStockClassAuthorizedLimit adjustmentIndex stockClassData = + case currentStockClassAuthorizedCeiling adjustmentIndex stockClassData of + AuthorizedSharesFinite n -> Some n + _ -> None + +stockClassAuthorizedCeilingAt : + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + StockClassOcfData -> + Time -> + AuthorizedSharesCeiling +stockClassAuthorizedCeilingAt adjustmentHistory stockClassData asOf = + case stockClassAuthorizedAdjustmentAsOf adjustmentHistory stockClassData.id asOf of + Some adjustment -> AuthorizedSharesFinite adjustment.new_shares_authorized + None -> authorizedSharesCeiling stockClassData.initial_shares_authorized + +stockClassAuthorizedLimitAt : + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + StockClassOcfData -> + Time -> + Optional Decimal +stockClassAuthorizedLimitAt adjustmentHistory stockClassData asOf = + case stockClassAuthorizedCeilingAt adjustmentHistory stockClassData asOf of + AuthorizedSharesFinite n -> Some n + _ -> None + +latestIssuerAuthorizedAdjustment : CapTableMaps -> Text -> Update (Optional IssuerAuthorizedSharesAdjustmentOcfData) +latestIssuerAuthorizedAdjustment maps issuerId = + foldlA + (\latest adjustmentCid -> do + adjustment <- fetch adjustmentCid + if adjustment.adjustment_data.issuer_id == issuerId + then pure $ case latest of + Some current -> + if issuerAuthorizedAdjustmentTakesPrecedence adjustment.adjustment_data current + then Some adjustment.adjustment_data + else latest + None -> Some adjustment.adjustment_data + else pure latest) + None + (Map.values maps.issuer_authorized_shares_adjustments) + +issuerAuthorizedAdjustmentHistoryIndex : CapTableMaps -> Update (Map Text [IssuerAuthorizedSharesAdjustmentOcfData]) +issuerAuthorizedAdjustmentHistoryIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addIssuerAuthorizedAdjustmentById adjustment.adjustment_data.issuer_id adjustment.adjustment_data index)) + Map.empty + (Map.values maps.issuer_authorized_shares_adjustments) + +issuerAuthorizedAdjustmentAsOf : + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> + Text -> + Time -> + Optional IssuerAuthorizedSharesAdjustmentOcfData +issuerAuthorizedAdjustmentAsOf adjustmentHistory issuerId asOf = + foldl + (\latest candidate -> + if candidate.date <= asOf + then case latest of + Some current -> + if issuerAuthorizedAdjustmentTakesPrecedence candidate current + then Some candidate + else latest + None -> Some candidate + else latest) + None + (fromOptional [] (Map.lookup issuerId adjustmentHistory)) + +currentIssuerAuthorizedLimit : CapTableMaps -> ContractId Issuer -> Update (Optional Decimal) +currentIssuerAuthorizedLimit maps issuerCid = do + issuerContract <- fetch issuerCid + latestAdjustment <- latestIssuerAuthorizedAdjustment maps issuerContract.issuer_data.id + pure $ case latestAdjustment of + Some adjustment -> Some adjustment.new_shares_authorized + None -> issuerAuthorizedSharesLimit issuerContract.issuer_data.initial_shares_authorized + +stockPlanPoolAdjustmentIndex : CapTableMaps -> Update (Map Text StockPlanPoolAdjustmentOcfData) +stockPlanPoolAdjustmentIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + let stockPlanId = adjustment.adjustment_data.stock_plan_id + let nextIndex = case Map.lookup stockPlanId index of + Some current -> + if stockPlanPoolAdjustmentTakesPrecedence adjustment.adjustment_data current + then Map.insert stockPlanId adjustment.adjustment_data index + else index + None -> Map.insert stockPlanId adjustment.adjustment_data index + pure nextIndex) + Map.empty + (Map.values maps.stock_plan_pool_adjustments) + +stockPlanPoolAdjustmentHistoryIndex : CapTableMaps -> Update (Map Text [StockPlanPoolAdjustmentOcfData]) +stockPlanPoolAdjustmentHistoryIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addStockPlanPoolAdjustmentById adjustment.adjustment_data.stock_plan_id adjustment.adjustment_data index)) + Map.empty + (Map.values maps.stock_plan_pool_adjustments) + +stockPlanPoolAdjustmentAsOf : + Map Text [StockPlanPoolAdjustmentOcfData] -> + Text -> + Time -> + Optional StockPlanPoolAdjustmentOcfData +stockPlanPoolAdjustmentAsOf adjustmentHistory stockPlanId asOf = + foldl + (\latest candidate -> + if candidate.date <= asOf + then case latest of + Some current -> + if stockPlanPoolAdjustmentTakesPrecedence candidate current + then Some candidate + else latest + None -> Some candidate + else latest) + None + (fromOptional [] (Map.lookup stockPlanId adjustmentHistory)) + +currentStockPlanReservedShares : + Map Text StockPlanPoolAdjustmentOcfData -> + StockPlanOcfData -> + Decimal +currentStockPlanReservedShares adjustmentIndex stockPlanData = + case Map.lookup stockPlanData.id adjustmentIndex of + Some adjustment -> adjustment.shares_reserved + None -> stockPlanData.initial_shares_reserved + +stockPlanReservedSharesAt : + Map Text [StockPlanPoolAdjustmentOcfData] -> + StockPlanOcfData -> + Time -> + Decimal +stockPlanReservedSharesAt adjustmentHistory stockPlanData asOf = + case stockPlanPoolAdjustmentAsOf adjustmentHistory stockPlanData.id asOf of + Some adjustment -> adjustment.shares_reserved + None -> stockPlanData.initial_shares_reserved + +issuerAuthorizedLimitAt : + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> + IssuerOcfData -> + Time -> + Optional Decimal +issuerAuthorizedLimitAt adjustmentHistory issuerData asOf = + case issuerAuthorizedAdjustmentAsOf adjustmentHistory issuerData.id asOf of + Some adjustment -> Some adjustment.new_shares_authorized + None -> issuerAuthorizedSharesLimit issuerData.initial_shares_authorized + +stockClassShareEventSortKey : StockClassShareEvent -> (Time, Int, Int) +stockClassShareEventSortKey event = case event of + StockClassShareDelta with event_date; event_priority -> (event_date, event_priority, 0) + StockClassShareTerminalDelta with event_date; event_priority -> (event_date, event_priority, 1) + StockClassShareMultiplier with event_date; event_priority -> (event_date, event_priority, 2) + StockClassShareRetraction with event_date; event_priority -> (event_date, event_priority, 3) + +stockClassShareEventOnOrAfter : Time -> StockClassShareEvent -> Bool +stockClassShareEventOnOrAfter earliest event = case event of + StockClassShareDelta with event_date -> event_date >= earliest + StockClassShareTerminalDelta with event_date -> event_date >= earliest + StockClassShareMultiplier with event_date -> event_date >= earliest + StockClassShareRetraction with event_date -> event_date >= earliest + +stockClassShareEventOnOrBefore : Time -> StockClassShareEvent -> Bool +stockClassShareEventOnOrBefore latest event = case event of + StockClassShareDelta with event_date -> event_date <= latest + StockClassShareTerminalDelta with event_date -> event_date <= latest + StockClassShareMultiplier with event_date -> event_date <= latest + StockClassShareRetraction with event_date -> event_date <= latest + +applyStockClassShareEvent : Decimal -> StockClassShareEvent -> Optional Decimal +applyStockClassShareEvent current event = case event of + StockClassShareDelta with quantity_delta -> + let next = current + quantity_delta + in if next < 0.0 then None else Some next + StockClassShareTerminalDelta with quantity_delta -> + let next = current + quantity_delta + in if next < 0.0 then None else Some 0.0 + StockClassShareMultiplier with ratio -> Some (current * ratio.numerator / ratio.denominator) + StockClassShareRetraction _ _ -> Some 0.0 + +applyStockClassShareEventOptional : Optional Decimal -> StockClassShareEvent -> Optional Decimal +applyStockClassShareEventOptional currentOpt event = + case currentOpt of + Some current -> applyStockClassShareEvent current event + None -> None + +stockSecurityReductionEvents : Time -> Decimal -> Optional Text -> [StockClassShareEvent] +stockSecurityReductionEvents eventDate quantity balanceSecurityId = case balanceSecurityId of + Some _ -> + [ StockClassShareTerminalDelta with + event_date = eventDate + event_priority = 2 + quantity_delta = 0.0 - quantity + ] + None -> + [ StockClassShareDelta with + event_date = eventDate + event_priority = 1 + quantity_delta = 0.0 - quantity + ] + +addStockClassShareEvent : Text -> StockClassShareEvent -> Map Text [StockClassShareEvent] -> Map Text [StockClassShareEvent] +addStockClassShareEvent key event index = + Map.insert key (event :: fromOptional [] (Map.lookup key index)) index + +addStockClassShareEvents : Text -> [StockClassShareEvent] -> Map Text [StockClassShareEvent] -> Map Text [StockClassShareEvent] +addStockClassShareEvents key events index = + foldl (\updatedIndex event -> addStockClassShareEvent key event updatedIndex) index events + +addStockClassShareEventForKeys : [Text] -> StockClassShareEvent -> Map Text [StockClassShareEvent] -> Map Text [StockClassShareEvent] +addStockClassShareEventForKeys keys event index = + foldl (\updatedIndex key -> addStockClassShareEvent key event updatedIndex) index keys + +stockSuccessorEdgeIndex : [StockSuccessorEdge] -> StockSuccessorEdgeIndex +stockSuccessorEdgeIndex = + foldl + (\index (sourceSecurityId, eventDate, successorSecurityIds) -> + Map.insert + sourceSecurityId + ((eventDate, successorSecurityIds) :: fromOptional [] (Map.lookup sourceSecurityId index)) + index) + Map.empty + +propagateStockSuccessorSortKeysFrom : + StockSuccessorEdgeIndex -> + [(Text, (Time, Int, Int))] -> + Map Text (Time, Int, Int) -> + Map Text (Time, Int, Int) -> + Map Text (Time, Int, Int) +propagateStockSuccessorSortKeysFrom edgeIndex pending expanded sortKeys = + case pending of + [] -> sortKeys + (sourceSecurityId, sourceSortKey) :: remaining -> + if Map.lookup sourceSecurityId expanded == Some sourceSortKey + then propagateStockSuccessorSortKeysFrom edgeIndex remaining expanded sortKeys + else + let (sourceDate, _, _) = sourceSortKey + successorSecurityIds = concatMap + (\(eventDate, edgeSuccessorIds) -> + if eventDate == sourceDate then edgeSuccessorIds else []) + (fromOptional [] (Map.lookup sourceSecurityId edgeIndex)) + nextSortKeys = foldl + (\index successorSecurityId -> Map.insert successorSecurityId sourceSortKey index) + sortKeys + successorSecurityIds + nextPending = map (\successorSecurityId -> (successorSecurityId, sourceSortKey)) successorSecurityIds ++ remaining + in propagateStockSuccessorSortKeysFrom + edgeIndex + nextPending + (Map.insert sourceSecurityId sourceSortKey expanded) + nextSortKeys + +propagateStockSuccessorSortKeys : + [StockSuccessorEdge] -> + Map Text (Time, Int, Int) -> + Map Text (Time, Int, Int) +propagateStockSuccessorSortKeys edges initialSortKeys = + propagateStockSuccessorSortKeysFrom + (stockSuccessorEdgeIndex edges) + (Map.toList initialSortKeys) + Map.empty + initialSortKeys + +stockClassShareEventIndex : CapTableMaps -> Update StockClassShareEventIndex +stockClassShareEventIndex maps = do + cancellationEventsBySecurityId <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + let events = stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id + if stockBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_security_id + then pure (addStockClassShareEvents cancellation.cancellation_data.security_id events index) + else pure index) + Map.empty + (Map.values maps.stock_cancellations) + + repurchaseEventsBySecurityId <- foldlA + (\index repurchaseCid -> do + repurchase <- fetch repurchaseCid + let events = stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id + if stockBalanceSecurityIssuanceExists maps repurchase.repurchase_data.balance_security_id + then pure (addStockClassShareEvents repurchase.repurchase_data.security_id events index) + else pure index) + cancellationEventsBySecurityId + (Map.values maps.stock_repurchases) + + conversionEventsBySecurityId <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + let events = stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id + if stockBalanceSecurityIssuanceExists maps conversion.conversion_data.balance_security_id + then pure (addStockClassShareEvents conversion.conversion_data.security_id events index) + else pure index) + repurchaseEventsBySecurityId + (Map.values maps.stock_conversions) + + transferEventsBySecurityId <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + assertNoPartialResultingStockIssuanceCoverage ("Stock transfer " <> transfer.transfer_data.id) maps transfer.transfer_data.resulting_security_ids + let events = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id + pure (addStockClassShareEvents transfer.transfer_data.security_id events index)) + conversionEventsBySecurityId + (Map.values maps.stock_transfers) + + reissuanceEventsBySecurityId <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + assertNoPartialResultingStockIssuanceCoverage ("Stock reissuance " <> reissuance.reissuance_data.id) maps reissuance.reissuance_data.resulting_security_ids + if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids + then pure (addStockClassShareEvent + reissuance.reissuance_data.security_id + (StockClassShareRetraction with + event_date = reissuance.reissuance_data.date + event_priority = 3) + index) + else pure index) + transferEventsBySecurityId + (Map.values maps.stock_reissuances) + + consolidationEventsBySecurityId <- foldlA + (\index consolidationCid -> do + consolidation <- fetch consolidationCid + if isSome (Map.lookup consolidation.consolidation_data.resulting_security_id maps.stock_issuances_by_security_id) + then pure (addStockClassShareEventForKeys + consolidation.consolidation_data.security_ids + (StockClassShareRetraction with + event_date = consolidation.consolidation_data.date + event_priority = 3) + index) + else pure index) + reissuanceEventsBySecurityId + (Map.values maps.stock_consolidations) + + (retractionEventsBySecurityId, planRetractionEventsBySecurityId) <- foldlA + (\(shareIndex, planIndex) retractionCid -> do + retraction <- fetch retractionCid + let event = StockClassShareRetraction with + event_date = retraction.retraction_data.date + event_priority = 3 + pure + ( addStockClassShareEvent retraction.retraction_data.security_id event shareIndex + , addStockClassShareEvent retraction.retraction_data.security_id event planIndex + )) + (consolidationEventsBySecurityId, Map.empty) + (Map.values maps.stock_retractions) + + splitEventsByStockClassId <- foldlA + (\index splitCid -> do + split <- fetch splitCid + pure (addStockClassShareEvent + split.split_data.stock_class_id + (StockClassShareMultiplier with + event_date = split.split_data.date + event_priority = 2 + ratio = split.split_data.split_ratio) + index)) + Map.empty + (Map.values maps.stock_class_splits) + + reissuanceSuccessorSortKeys <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + pure (foldl + (\updatedIndex securityId -> Map.insert securityId (reissuance.reissuance_data.date, 3, 0) updatedIndex) + index + reissuance.reissuance_data.resulting_security_ids)) + Map.empty + (Map.values maps.stock_reissuances) + + successorSortKeys <- foldlA + (\index consolidationCid -> do + consolidation <- fetch consolidationCid + pure (Map.insert + consolidation.consolidation_data.resulting_security_id + (consolidation.consolidation_data.date, 3, 0) + index)) + reissuanceSuccessorSortKeys + (Map.values maps.stock_consolidations) + + stockTransferSuccessorEdges <- foldlA + (\edges transferCid -> do + transfer <- fetch transferCid + let successorSecurityIds = case transfer.transfer_data.balance_security_id of + Some balanceSecurityId -> balanceSecurityId :: transfer.transfer_data.resulting_security_ids + None -> transfer.transfer_data.resulting_security_ids + pure ((transfer.transfer_data.security_id, transfer.transfer_data.date, successorSecurityIds) :: edges)) + [] + (Map.values maps.stock_transfers) + + stockCancellationSuccessorEdges <- foldlA + (\edges cancellationCid -> do + cancellation <- fetch cancellationCid + case cancellation.cancellation_data.balance_security_id of + Some balanceSecurityId -> + pure ((cancellation.cancellation_data.security_id, cancellation.cancellation_data.date, [balanceSecurityId]) :: edges) + None -> pure edges) + stockTransferSuccessorEdges + (Map.values maps.stock_cancellations) + + stockRepurchaseSuccessorEdges <- foldlA + (\edges repurchaseCid -> do + repurchase <- fetch repurchaseCid + case repurchase.repurchase_data.balance_security_id of + Some balanceSecurityId -> + pure ((repurchase.repurchase_data.security_id, repurchase.repurchase_data.date, [balanceSecurityId]) :: edges) + None -> pure edges) + stockCancellationSuccessorEdges + (Map.values maps.stock_repurchases) + + stockSuccessorEdges <- foldlA + (\edges conversionCid -> do + conversion <- fetch conversionCid + let successorSecurityIds = case conversion.conversion_data.balance_security_id of + Some balanceSecurityId -> balanceSecurityId :: conversion.conversion_data.resulting_security_ids + None -> conversion.conversion_data.resulting_security_ids + pure ((conversion.conversion_data.security_id, conversion.conversion_data.date, successorSecurityIds) :: edges)) + stockRepurchaseSuccessorEdges + (Map.values maps.stock_conversions) + + let propagatedSuccessorSortKeys = propagateStockSuccessorSortKeys stockSuccessorEdges successorSortKeys + + pure StockClassShareEventIndex with + stock_security_events_by_security_id = retractionEventsBySecurityId + stock_plan_retraction_events_by_security_id = Some planRetractionEventsBySecurityId + stock_class_split_events_by_stock_class_id = splitEventsByStockClassId + stock_successor_sort_keys_by_security_id = Some propagatedSuccessorSortKeys + +stockClassShareLotSplitCutoffSortKey : StockClassShareEventIndex -> StockClassShareLot -> (Time, Int, Int) +stockClassShareLotSplitCutoffSortKey eventIndex lot = + let defaultSortKey = (lot.event_date, 0, 0) + in case lot.security_id of + Some securityId -> case Map.lookup securityId (fromOptional Map.empty eventIndex.stock_successor_sort_keys_by_security_id) of + Some successorSortKey -> + let (successorDate, _, _) = successorSortKey + in if successorDate == lot.event_date then successorSortKey else defaultSortKey + None -> defaultSortKey + None -> defaultSortKey + +stockClassShareLotEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] +stockClassShareLotEvents eventIndex lot = + let splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex lot + securityEvents = case lot.security_id of + Some securityId -> fromOptional [] (Map.lookup securityId eventIndex.stock_security_events_by_security_id) + None -> [] + splitEvents = + filter + (\event -> stockClassShareEventSortKey event >= splitCutoffSortKey) + (fromOptional [] (Map.lookup lot.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) + in (StockClassShareDelta with + event_date = lot.event_date + event_priority = 0 + quantity_delta = lot.quantity) + :: (securityEvents ++ splitEvents) + +stockClassShareLotPlanAllocationEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] +stockClassShareLotPlanAllocationEvents eventIndex lot = + let splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex lot + retractionEvents = case lot.security_id of + Some securityId -> + filter + (\event -> case event of + StockClassShareRetraction _ _ -> True + _ -> False) + (fromOptional [] (Map.lookup securityId (fromOptional Map.empty eventIndex.stock_plan_retraction_events_by_security_id))) + None -> [] + splitEvents = + filter + (\event -> stockClassShareEventSortKey event >= splitCutoffSortKey) + (fromOptional [] (Map.lookup lot.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) + in (StockClassShareDelta with + event_date = lot.event_date + event_priority = 0 + quantity_delta = lot.quantity) + :: (retractionEvents ++ splitEvents) + +stockClassShareLotPlanAllocatedQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal +stockClassShareLotPlanAllocatedQuantity eventIndex lot = + foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey (stockClassShareLotPlanAllocationEvents eventIndex lot)) + +stockClassShareLotPlanAllocatedQuantityAt : StockClassShareEventIndex -> StockClassShareLot -> Time -> Optional Decimal +stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf = + if lot.event_date <= asOf + then + foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn + stockClassShareEventSortKey + (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotPlanAllocationEvents eventIndex lot))) + else Some 0.0 + +stockClassShareLotCurrentQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal +stockClassShareLotCurrentQuantity eventIndex lot = + foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey (stockClassShareLotEvents eventIndex lot)) + +stockClassShareLotQuantityAt : StockClassShareEventIndex -> StockClassShareLot -> Time -> Optional Decimal +stockClassShareLotQuantityAt eventIndex lot asOf = + if lot.event_date <= asOf + then + foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn + stockClassShareEventSortKey + (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotEvents eventIndex lot))) + else Some 0.0 + +stockSecurityShareEvents : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] +stockSecurityShareEvents eventIndex maps issuanceData = do + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + then pure (stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id ++ events) + else pure events) + [] + (Map.values maps.stock_cancellations) + + repurchaseEvents <- foldlA + (\events repurchaseCid -> do + repurchase <- fetch repurchaseCid + if repurchase.repurchase_data.security_id == issuanceData.security_id + then pure (stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id ++ events) + else pure events) + [] + (Map.values maps.stock_repurchases) + + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == issuanceData.security_id + then pure (stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id ++ events) + else pure events) + [] + (Map.values maps.stock_transfers) + + conversionEvents <- foldlA + (\events conversionCid -> do + conversion <- fetch conversionCid + if conversion.conversion_data.security_id == issuanceData.security_id + then pure (stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id ++ events) + else pure events) + [] + (Map.values maps.stock_conversions) + + retractionEvents <- foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == issuanceData.security_id + then pure ((StockClassShareRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.stock_retractions) + + reissuanceEvents <- foldlA + (\events reissuanceCid -> do + reissuance <- fetch reissuanceCid + if reissuance.reissuance_data.security_id == issuanceData.security_id + then pure ((StockClassShareRetraction with + event_date = reissuance.reissuance_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.stock_reissuances) + + consolidationEvents <- foldlA + (\events consolidationCid -> do + consolidation <- fetch consolidationCid + if issuanceData.security_id `elem` consolidation.consolidation_data.security_ids + then pure ((StockClassShareRetraction with + event_date = consolidation.consolidation_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.stock_consolidations) + + let splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex (stockIssuanceShareLot issuanceData) + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + let splitSortKey = (split.split_data.date, 2, 2) + if split.split_data.stock_class_id == issuanceData.stock_class_id && splitSortKey >= splitCutoffSortKey + then pure ((StockClassShareMultiplier with + event_date = split.split_data.date + event_priority = 2 + ratio = split.split_data.split_ratio) + :: events) + else pure events) + [] + (Map.values maps.stock_class_splits) + + pure ((StockClassShareDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: (cancellationEvents ++ repurchaseEvents ++ transferEvents ++ conversionEvents ++ retractionEvents ++ reissuanceEvents ++ consolidationEvents ++ splitEvents)) + +validateStockSecurityBalanceEvent : Text -> Decimal -> StockClassShareEvent -> Update Decimal +validateStockSecurityBalanceEvent securityId current event = case event of + StockClassShareDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Stock security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure next + StockClassShareTerminalDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Stock security terminal reduction requires a positive balance for " <> securityId <> ": current balance is " <> show current) + (current > 0.0) + assertMsg + ("Stock security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure 0.0 + StockClassShareMultiplier with ratio -> + pure (current * ratio.numerator / ratio.denominator) + StockClassShareRetraction _ _ -> do + assertMsg + ("Stock security balance cannot be retired for " <> securityId <> " because it has no remaining balance") + (current > 0.0) + pure 0.0 + +validateStockSecurityBalance : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> Update () +validateStockSecurityBalance eventIndex maps issuanceData = do + events <- stockSecurityShareEvents eventIndex maps issuanceData + _ <- foldlA + (validateStockSecurityBalanceEvent issuanceData.security_id) + 0.0 + (sortOn stockClassShareEventSortKey events) + pure () + +validateStockSecurityBalances : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockSecurityBalances eventIndex maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateStockSecurityBalance eventIndex maps issuance.issuance_data) + (Map.values maps.stock_issuances) + +stockSecurityQuantityBeforeEvent : + StockClassShareEventIndex -> + CapTableMaps -> + StockIssuanceOcfData -> + (Time, Int, Int) -> + Update Decimal +stockSecurityQuantityBeforeEvent eventIndex maps issuanceData targetSortKey = do + events <- stockSecurityShareEvents eventIndex maps issuanceData + foldlA + (validateStockSecurityBalanceEvent issuanceData.security_id) + 0.0 + (filter + (\event -> stockClassShareEventSortKey event < targetSortKey) + (sortOn stockClassShareEventSortKey events)) + +stockSecurityQuantityBeforeTerminal : + StockClassShareEventIndex -> + CapTableMaps -> + StockIssuanceOcfData -> + Time -> + Update Decimal +stockSecurityQuantityBeforeTerminal eventIndex maps issuanceData eventDate = + stockSecurityQuantityBeforeEvent eventIndex maps issuanceData (eventDate, 2, 1) + +stockReductionSortKey : Time -> Optional Text -> (Time, Int, Int) +stockReductionSortKey eventDate balanceSecurityId = + case balanceSecurityId of + Some _ -> (eventDate, 2, 1) + None -> (eventDate, 1, 0) + +stockClassQuantityAtSortKey : + CapTableMaps -> + Text -> + Time -> + Decimal -> + (Time, Int, Int) -> + Update Decimal +stockClassQuantityAtSortKey maps stockClassId quantityDate quantity targetSortKey = + let (targetDate, _, _) = targetSortKey + quantitySortKey = if quantityDate == targetDate then targetSortKey else (quantityDate, 0, 0) + in foldlA + (\adjustedQuantity splitCid -> do + split <- fetch splitCid + let splitSortKey = (split.split_data.date, 2, 2) + ratio = split.split_data.split_ratio + if split.split_data.stock_class_id /= stockClassId + then pure adjustedQuantity + else if quantitySortKey < targetSortKey && quantitySortKey < splitSortKey && splitSortKey <= targetSortKey + then pure (adjustedQuantity * ratio.numerator / ratio.denominator) + else if targetSortKey < quantitySortKey && targetSortKey < splitSortKey && splitSortKey < quantitySortKey + then pure (adjustedQuantity * ratio.denominator / ratio.numerator) + else pure adjustedQuantity) + quantity + (Map.values maps.stock_class_splits) + +stockIssuanceQuantityAtSortKey : CapTableMaps -> StockIssuanceOcfData -> (Time, Int, Int) -> Update Decimal +stockIssuanceQuantityAtSortKey maps issuanceData = + stockClassQuantityAtSortKey maps issuanceData.stock_class_id issuanceData.date issuanceData.quantity + +equityCompensationIssuanceQuantityAtSortKey : + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + (Time, Int, Int) -> + Update Decimal +equityCompensationIssuanceQuantityAtSortKey maps issuanceData targetSortKey = + case issuanceData.stock_class_id of + Some stockClassId -> + stockClassQuantityAtSortKey maps stockClassId issuanceData.date issuanceData.quantity targetSortKey + None -> pure issuanceData.quantity + +equityCompensationSuccessorStockClassIds : + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + Update [Optional Text] +equityCompensationSuccessorStockClassIds maps issuanceData = + case issuanceData.stock_class_id of + Some stockClassId -> pure [Some stockClassId] + None -> case issuanceData.stock_plan_id of + Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure $ case stockPlan.plan_data.stock_class_ids of + [stockClassId] -> [None, Some stockClassId] + _ -> [None] + None -> pure [None] + None -> pure [None] + +equityCompensationIssuanceQuantityTotal : + CapTableMaps -> + Text -> + [Optional Text] -> + Time -> + (Time, Int, Int) -> + [Text] -> + Update Decimal +equityCompensationIssuanceQuantityTotal maps label expectedStockClassIds expectedDate targetSortKey securityIds = + foldlA + (\total securityId -> + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " successor date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == expectedDate) + assertMsg + (label <> " successor stock class mismatch for " <> securityId <> ": expected one of " <> show expectedStockClassIds <> ", found " <> show issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id `elem` expectedStockClassIds) + quantity <- equityCompensationIssuanceQuantityAtSortKey maps issuance.issuance_data targetSortKey + pure (total + quantity) + None -> do + assertMsg (label <> " successor equity compensation issuance not found: " <> securityId) False + pure total) + 0.0 + securityIds + +stockIssuanceQuantityTotal : CapTableMaps -> Text -> Text -> Time -> (Time, Int, Int) -> [Text] -> Update Decimal +stockIssuanceQuantityTotal maps label expectedStockClassId expectedDate targetSortKey securityIds = + foldlA + (\total securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " successor date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == expectedDate) + assertMsg + (label <> " successor stock class mismatch for " <> securityId <> ": expected " <> expectedStockClassId <> ", found " <> issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id == expectedStockClassId) + quantity <- stockIssuanceQuantityAtSortKey maps issuance.issuance_data targetSortKey + pure (total + quantity) + None -> do + assertMsg (label <> " successor stock issuance not found: " <> securityId) False + pure total) + 0.0 + securityIds + +validateStockBalanceSuccessor : + CapTableMaps -> + Text -> + StockIssuanceOcfData -> + Time -> + Decimal -> + Text -> + Update () +validateStockBalanceSuccessor maps label sourceIssuanceData eventDate expectedQuantity balanceSecurityId = + case Map.lookup balanceSecurityId maps.stock_issuances_by_security_id of + Some balanceIssuanceCid -> do + balanceIssuance <- fetch balanceIssuanceCid + assertMsg + (label <> " balance result date mismatch for " <> balanceSecurityId <> ": expected " <> show eventDate <> ", found " <> show balanceIssuance.issuance_data.date) + (balanceIssuance.issuance_data.date == eventDate) + assertMsg + (label <> " balance stock class mismatch for " <> balanceSecurityId <> ": expected " <> sourceIssuanceData.stock_class_id <> ", found " <> balanceIssuance.issuance_data.stock_class_id) + (balanceIssuance.issuance_data.stock_class_id == sourceIssuanceData.stock_class_id) + assertMsg + (label <> " balance quantity mismatch for " <> balanceSecurityId <> ": expected " <> show expectedQuantity <> ", found " <> show balanceIssuance.issuance_data.quantity) + (balanceIssuance.issuance_data.quantity == expectedQuantity) + None -> + assertMsg (label <> " balance stock issuance not found: " <> balanceSecurityId) False + +validateStockTerminalBalanceSuccessor : + StockClassShareEventIndex -> + CapTableMaps -> + Text -> + StockIssuanceOcfData -> + Time -> + Decimal -> + Optional Text -> + Update () +validateStockTerminalBalanceSuccessor eventIndex maps label sourceIssuanceData eventDate reductionQuantity balanceSecurityId = + case balanceSecurityId of + Some securityId -> do + currentQuantity <- stockSecurityQuantityBeforeTerminal eventIndex maps sourceIssuanceData eventDate + let expectedQuantity = currentQuantity - reductionQuantity + assertMsg + (label <> " exceeds the current source quantity for " <> sourceIssuanceData.security_id) + (expectedQuantity >= 0.0) + validateStockBalanceSuccessor maps label sourceIssuanceData eventDate expectedQuantity securityId + None -> pure () + +validateStockBalanceSuccessors : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockBalanceSuccessors eventIndex maps = do + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + case cancellation.cancellation_data.balance_security_id of + Some _ -> case Map.lookup cancellation.cancellation_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex maps ("Stock cancellation " <> cancellation.cancellation_data.id) + sourceIssuance.issuance_data cancellation.cancellation_data.date cancellation.cancellation_data.quantity + cancellation.cancellation_data.balance_security_id + None -> assertMsg ("Stock cancellation source issuance not found: " <> cancellation.cancellation_data.security_id) False + None -> pure ()) + (Map.values maps.stock_cancellations) + + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + case repurchase.repurchase_data.balance_security_id of + Some _ -> case Map.lookup repurchase.repurchase_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex maps ("Stock repurchase " <> repurchase.repurchase_data.id) + sourceIssuance.issuance_data repurchase.repurchase_data.date repurchase.repurchase_data.quantity + repurchase.repurchase_data.balance_security_id + None -> assertMsg ("Stock repurchase source issuance not found: " <> repurchase.repurchase_data.security_id) False + None -> pure ()) + (Map.values maps.stock_repurchases) + + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + case conversion.conversion_data.balance_security_id of + Some _ -> case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex maps ("Stock conversion " <> conversion.conversion_data.id) + sourceIssuance.issuance_data conversion.conversion_data.date conversion.conversion_data.quantity_converted + conversion.conversion_data.balance_security_id + None -> assertMsg ("Stock conversion source issuance not found: " <> conversion.conversion_data.security_id) False + None -> pure ()) + (Map.values maps.stock_conversions) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + case transfer.transfer_data.balance_security_id of + Some _ -> case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex maps ("Stock transfer " <> transfer.transfer_data.id) + sourceIssuance.issuance_data transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id + None -> assertMsg ("Stock transfer source issuance not found: " <> transfer.transfer_data.security_id) False + None -> pure ()) + (Map.values maps.stock_transfers) + +validateStockTransferResultIssuances : CapTableMaps -> Update () +validateStockTransferResultIssuances maps = + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Stock transfer " <> transfer.transfer_data.id + assertNoPartialResultingStockIssuanceCoverage label maps transfer.transfer_data.resulting_security_ids + if allResultingStockIssuancesExist maps transfer.transfer_data.resulting_security_ids + then case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let targetSortKey = stockReductionSortKey transfer.transfer_data.date transfer.transfer_data.balance_security_id + resultingQuantity <- stockIssuanceQuantityTotal + maps + label + sourceIssuance.issuance_data.stock_class_id + transfer.transfer_data.date + targetSortKey + transfer.transfer_data.resulting_security_ids + assertMsg + (label <> " resulting quantity mismatch: expected " <> show transfer.transfer_data.quantity <> ", found " <> show resultingQuantity) + (resultingQuantity == transfer.transfer_data.quantity) + None -> assertMsg (label <> " source stock issuance not found: " <> transfer.transfer_data.security_id) False + else pure ()) + (Map.values maps.stock_transfers) + +validateStockReissuanceResultIssuances : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockReissuanceResultIssuances eventIndex maps = + mapA_ + (\reissuanceCid -> do + reissuance <- fetch reissuanceCid + let label = "Stock reissuance " <> reissuance.reissuance_data.id + assertNoPartialResultingStockIssuanceCoverage label maps reissuance.reissuance_data.resulting_security_ids + case Map.lookup reissuance.reissuance_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> + if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids + then do + sourceIssuance <- fetch sourceIssuanceCid + let targetSortKey = (reissuance.reissuance_data.date, 3, 0) + sourceQuantity <- stockSecurityQuantityBeforeEvent + eventIndex + maps + sourceIssuance.issuance_data + targetSortKey + resultingQuantity <- stockIssuanceQuantityTotal + maps + label + sourceIssuance.issuance_data.stock_class_id + reissuance.reissuance_data.date + targetSortKey + reissuance.reissuance_data.resulting_security_ids + assertMsg + (label <> " resulting quantity mismatch: expected " <> show sourceQuantity <> ", found " <> show resultingQuantity) + (resultingQuantity == sourceQuantity) + else pure () + None -> assertMsg (label <> " source stock issuance not found: " <> reissuance.reissuance_data.security_id) False) + (Map.values maps.stock_reissuances) + +validateStockConsolidationResultIssuances : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockConsolidationResultIssuances eventIndex maps = + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + let label = "Stock consolidation " <> consolidation.consolidation_data.id + mapA_ + (\sourceSecurityId -> + assertMsg + (label <> " source stock issuance not found: " <> sourceSecurityId) + (isSome (Map.lookup sourceSecurityId maps.stock_issuances_by_security_id))) + consolidation.consolidation_data.security_ids + case Map.lookup consolidation.consolidation_data.resulting_security_id maps.stock_issuances_by_security_id of + Some resultIssuanceCid -> do + resultIssuance <- fetch resultIssuanceCid + assertMsg + (label <> " result date mismatch for " <> consolidation.consolidation_data.resulting_security_id <> ": expected " <> show consolidation.consolidation_data.date <> ", found " <> show resultIssuance.issuance_data.date) + (resultIssuance.issuance_data.date == consolidation.consolidation_data.date) + let targetSortKey = (consolidation.consolidation_data.date, 3, 3) + sourceQuantity <- foldlA + (\total sourceSecurityId -> + case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + assertMsg + (label <> " source stock class mismatch for " <> sourceSecurityId <> ": expected " <> resultIssuance.issuance_data.stock_class_id <> ", found " <> sourceIssuance.issuance_data.stock_class_id) + (sourceIssuance.issuance_data.stock_class_id == resultIssuance.issuance_data.stock_class_id) + quantity <- stockSecurityQuantityBeforeEvent + eventIndex + maps + sourceIssuance.issuance_data + targetSortKey + pure (total + quantity) + None -> do + assertMsg (label <> " source stock issuance not found: " <> sourceSecurityId) False + pure total) + 0.0 + consolidation.consolidation_data.security_ids + resultQuantity <- stockIssuanceQuantityAtSortKey maps resultIssuance.issuance_data targetSortKey + assertMsg + (label <> " resulting quantity mismatch: expected " <> show sourceQuantity <> ", found " <> show resultQuantity) + (resultQuantity == sourceQuantity) + None -> pure ()) + (Map.values maps.stock_consolidations) + +addOptionalDecimal : Optional Decimal -> Optional Decimal -> Optional Decimal +addOptionalDecimal leftOpt rightOpt = + case (leftOpt, rightOpt) of + (Some left, Some right) -> Some (left + right) + _ -> None + +stockClassIssuedQuantity : + StockClassShareEventIndex -> + Map Text [StockClassShareLot] -> + Text -> + Optional Decimal +stockClassIssuedQuantity eventIndex lotsByStockClass stockClassId = + foldl + (\totalOpt lot -> addOptionalDecimal totalOpt (stockClassShareLotCurrentQuantity eventIndex lot)) + (Some 0.0) + (fromOptional [] (Map.lookup stockClassId lotsByStockClass)) + +stockClassIssuedQuantityAt : + StockClassShareEventIndex -> + Map Text [StockClassShareLot] -> + Text -> + Time -> + Optional Decimal +stockClassIssuedQuantityAt eventIndex lotsByStockClass stockClassId asOf = + foldl + (\totalOpt lot -> addOptionalDecimal totalOpt (stockClassShareLotQuantityAt eventIndex lot asOf)) + (Some 0.0) + (fromOptional [] (Map.lookup stockClassId lotsByStockClass)) + +assertWithinStockClassAuthorizedShares : Text -> Decimal -> Optional Decimal -> Update () +assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = case limitOpt of + Some limit -> + assertMsg + ("Stock class authorized shares exceeded for " <> stockClassId <> ": issued " <> show issuedQuantity <> " exceeds authorized " <> show limit) + (issuedQuantity <= limit) + None -> pure () + +assertValidStockClassIssuedQuantity : Text -> Optional Decimal -> Update Decimal +assertValidStockClassIssuedQuantity stockClassId issuedQuantityOpt = + case issuedQuantityOpt of + Some issuedQuantity -> pure issuedQuantity + None -> do + assertMsg ("Stock class issued shares cannot be computed because a stock security was reduced below zero for " <> stockClassId) False + pure 0.0 + +assertWithinStockClassAuthorizedSharesAtDate : Text -> Time -> Decimal -> Optional Decimal -> Update () +assertWithinStockClassAuthorizedSharesAtDate stockClassId asOf issuedQuantity limitOpt = case limitOpt of + Some limit -> + assertMsg + ("Stock class authorized shares exceeded for " <> stockClassId <> " on " <> show asOf <> ": issued " <> show issuedQuantity <> " exceeds authorized " <> show limit) + (issuedQuantity <= limit) + None -> pure () + +stockClassAuthorizedValidationDateIndex : CapTableMaps -> Map Text [StockClassShareLot] -> Update (Map Text [Time]) +stockClassAuthorizedValidationDateIndex maps lotsByStockClass = do + let lotDates = + foldl + (\index lot -> addDateById lot.stock_class_id lot.event_date index) + Map.empty + (concat (Map.values lotsByStockClass)) + + splitDates <- foldlA + (\index splitCid -> do + split <- fetch splitCid + pure (addDateById split.split_data.stock_class_id split.split_data.date index)) + lotDates + (Map.values maps.stock_class_splits) + + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addDateById adjustment.adjustment_data.stock_class_id adjustment.adjustment_data.date index)) + splitDates + (Map.values maps.stock_class_authorized_shares_adjustments) + +validateAllStockClassAuthorizedShares : + StockClassShareEventIndex -> + [StockClassOcfData] -> + Map Text [StockClassShareLot] -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + Update () +validateAllStockClassAuthorizedShares eventIndex stockClasses lotsByStockClass adjustmentIndex = + mapA_ + (\stockClassData -> do + currentIssued <- assertValidStockClassIssuedQuantity stockClassData.id (stockClassIssuedQuantity eventIndex lotsByStockClass stockClassData.id) + let limitOpt = currentStockClassAuthorizedLimit adjustmentIndex stockClassData + assertWithinStockClassAuthorizedShares stockClassData.id currentIssued limitOpt) + stockClasses + +validateStockClassAuthorizedSharesAtDates : + StockClassShareEventIndex -> + [StockClassOcfData] -> + Map Text [StockClassShareLot] -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Map Text [Time] -> + Update () +validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass adjustmentHistory validationDates = + mapA_ + (\stockClassData -> do + let stockClassId = stockClassData.id + mapA_ + (\asOf -> do + issuedAtDate <- assertValidStockClassIssuedQuantity stockClassId (stockClassIssuedQuantityAt eventIndex lotsByStockClass stockClassId asOf) + let limitOpt = stockClassAuthorizedLimitAt adjustmentHistory stockClassData asOf + assertWithinStockClassAuthorizedSharesAtDate stockClassId asOf issuedAtDate limitOpt) + (fromOptional [] (Map.lookup stockClassId validationDates))) + stockClasses + +addStockClassAuthorizedContribution : Optional Decimal -> AuthorizedSharesCeiling -> Optional Decimal +addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) of + (Some total, AuthorizedSharesFinite shares) -> Some (total + shares) + (_, AuthorizedSharesNotApplicable) -> None + (_, AuthorizedSharesUnlimited) -> None + (None, _) -> None + +latestOptionalTime : Optional Time -> Optional Time -> Optional Time +latestOptionalTime leftOpt rightOpt = + case (leftOpt, rightOpt) of + (Some left, Some right) -> Some (if left >= right then left else right) + (Some left, None) -> Some left + (None, Some right) -> Some right + (None, None) -> None + +stockClassEffectiveDate : StockClassOcfData -> Optional Time +stockClassEffectiveDate stockClassData = + latestOptionalTime stockClassData.board_approval_date stockClassData.stockholder_approval_date + +stockClassEffectiveOnOrBefore : Time -> StockClassOcfData -> Bool +stockClassEffectiveOnOrBefore asOf stockClassData = + case stockClassEffectiveDate stockClassData of + Some effectiveDate -> effectiveDate <= asOf + None -> True + +currentStockClassAuthorizedTotalAt : + [StockClassOcfData] -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Time -> + Optional Decimal +currentStockClassAuthorizedTotalAt stockClasses adjustmentHistory asOf = + foldl + (\totalOpt stockClassData -> + if stockClassEffectiveOnOrBefore asOf stockClassData + then + let ceiling = stockClassAuthorizedCeilingAt adjustmentHistory stockClassData asOf + in addStockClassAuthorizedContribution totalOpt ceiling + else totalOpt) + (Some 0.0) + stockClasses + +currentStockClassAuthorizedTotal : + [StockClassOcfData] -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + Optional Decimal +currentStockClassAuthorizedTotal stockClasses adjustmentIndex = + foldl + (\totalOpt stockClassData -> + let ceiling = currentStockClassAuthorizedCeiling adjustmentIndex stockClassData + in addStockClassAuthorizedContribution totalOpt ceiling) + (Some 0.0) + stockClasses + +validateIssuerAuthorizedShares : + [StockClassOcfData] -> + CapTableMaps -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + ContractId Issuer -> + Update () +validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid = do + issuerLimitOpt <- currentIssuerAuthorizedLimit maps issuerCid + case issuerLimitOpt of + None -> pure () + Some issuerLimit -> do + let stockClassTotalOpt = currentStockClassAuthorizedTotal stockClasses adjustmentIndex + case stockClassTotalOpt of + Some stockClassTotal -> + assertMsg + ("Issuer authorized shares exceeded: stock classes authorize " <> show stockClassTotal <> " shares but issuer authorizes " <> show issuerLimit) + (stockClassTotal <= issuerLimit) + None -> + assertMsg "Issuer authorized shares exceeded: stock class authorization is unlimited but issuer authorization is finite" False + +issuerAuthorizedValidationDates : CapTableMaps -> [StockClassOcfData] -> Update [Time] +issuerAuthorizedValidationDates maps stockClasses = do + issuerAdjustmentDates <- foldlA + (\dates adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (adjustment.adjustment_data.date :: dates)) + [] + (Map.values maps.issuer_authorized_shares_adjustments) + + stockClassAdjustmentDates <- foldlA + (\dates adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (adjustment.adjustment_data.date :: dates)) + issuerAdjustmentDates + (Map.values maps.stock_class_authorized_shares_adjustments) + + pure + (foldl + (\dates stockClassData -> + case stockClassEffectiveDate stockClassData of + Some effectiveDate -> effectiveDate :: dates + None -> dates) + stockClassAdjustmentDates + stockClasses) + +validateIssuerAuthorizedSharesAtDates : + [StockClassOcfData] -> + CapTableMaps -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> + ContractId Issuer -> + Update () +validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid = do + issuerContract <- fetch issuerCid + validationDates <- issuerAuthorizedValidationDates maps stockClasses + mapA_ + (\asOf -> case issuerAuthorizedLimitAt issuerAdjustmentHistory issuerContract.issuer_data asOf of + None -> pure () + Some issuerLimit -> do + let stockClassTotalOpt = currentStockClassAuthorizedTotalAt stockClasses stockClassAdjustmentHistory asOf + case stockClassTotalOpt of + Some stockClassTotal -> + assertMsg + ("Issuer authorized shares exceeded on " <> show asOf <> ": stock classes authorize " <> show stockClassTotal <> " shares but issuer authorizes " <> show issuerLimit) + (stockClassTotal <= issuerLimit) + None -> + assertMsg ("Issuer authorized shares exceeded on " <> show asOf <> ": stock class authorization is unlimited but issuer authorization is finite") False) + (issuerContract.issuer_data.formation_date :: validationDates) + +validateStockClassShareLotReferences : + Map Text StockClassOcfData -> + Map Text [StockClassShareLot] -> + Update () +validateStockClassShareLotReferences stockClassIndex lotsByStockClass = + mapA_ + (\stockClassId -> + assertMsg + ("Stock class share lot references missing stock class: " <> stockClassId) + (isSome (Map.lookup stockClassId stockClassIndex))) + (Map.keys lotsByStockClass) + +validateStockClassAuthorizedShareAdjustmentReferences : + CapTableMaps -> + Map Text StockClassOcfData -> + Update () +validateStockClassAuthorizedShareAdjustmentReferences maps stockClassIndex = + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + assertMsg + ("Stock class authorized shares adjustment references missing stock class: " <> adjustment.adjustment_data.stock_class_id) + (isSome (Map.lookup adjustment.adjustment_data.stock_class_id stockClassIndex))) + (Map.values maps.stock_class_authorized_shares_adjustments) + +validateIssuerAuthorizedShareAdjustmentReferences : + CapTableMaps -> + ContractId Issuer -> + Update () +validateIssuerAuthorizedShareAdjustmentReferences maps issuerCid = do + issuerContract <- fetch issuerCid + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + assertMsg + ("Issuer authorized shares adjustment references another issuer: " <> adjustment.adjustment_data.id) + (adjustment.adjustment_data.issuer_id == issuerContract.issuer_data.id)) + (Map.values maps.issuer_authorized_shares_adjustments) + +addQuantityById : Text -> Decimal -> Map Text Decimal -> Map Text Decimal +addQuantityById key quantity totals = + let current = fromOptional 0.0 (Map.lookup key totals) + in Map.insert key (current + quantity) totals + +mergeQuantityMaps : Map Text Decimal -> Map Text Decimal -> Map Text Decimal +mergeQuantityMaps base delta = + foldl (\totals (key, quantity) -> addQuantityById key quantity totals) base (Map.toList delta) + +negateQuantityMap : Map Text Decimal -> Map Text Decimal +negateQuantityMap quantities = + foldl (\totals (key, quantity) -> Map.insert key (-quantity) totals) Map.empty (Map.toList quantities) + +assertNonNegativeQuantity : Text -> Decimal -> Update () +assertNonNegativeQuantity label quantity = + assertMsg (label <> " cannot be negative: " <> show quantity) (quantity >= 0.0) + +addPlanSuccessor : Time -> Text -> Text -> Map Text (Time, Optional Text) -> Map Text (Time, Optional Text) +addPlanSuccessor producedAt stockPlanId securityId index = + case Map.lookup securityId index of + Some (existingDate, existingPlanIdOpt) -> + let earliestDate = if producedAt < existingDate then producedAt else existingDate + inheritedPlanIdOpt = case existingPlanIdOpt of + Some existingPlanId -> if existingPlanId == stockPlanId then Some stockPlanId else None + None -> None + in Map.insert securityId (earliestDate, inheritedPlanIdOpt) index + None -> Map.insert securityId (producedAt, Some stockPlanId) index + +addOptionalPlanSuccessor : Time -> Text -> Optional Text -> Map Text (Time, Optional Text) -> Map Text (Time, Optional Text) +addOptionalPlanSuccessor producedAt stockPlanId securityIdOpt index = case securityIdOpt of + Some securityId -> addPlanSuccessor producedAt stockPlanId securityId index + None -> index + +addPlanSuccessors : Time -> Text -> [Text] -> Map Text (Time, Optional Text) -> Map Text (Time, Optional Text) +addPlanSuccessors producedAt stockPlanId securityIds index = + foldl (\updatedIndex securityId -> addPlanSuccessor producedAt stockPlanId securityId updatedIndex) index securityIds + +declaredPlanForSecurity : Map Text Text -> Map Text (Time, Optional Text) -> Text -> Optional Text +declaredPlanForSecurity declaredPlans successorIndex securityId = + case Map.lookup securityId declaredPlans of + Some stockPlanId -> Some stockPlanId + None -> case Map.lookup securityId successorIndex of + Some (_, stockPlanIdOpt) -> stockPlanIdOpt + None -> None + +effectivePlanForSecurity : + Map Text (Time, Optional Text) -> + Text -> + Optional Text -> + Optional Text +effectivePlanForSecurity successorIndex securityId declaredPlanIdOpt = + case declaredPlanIdOpt of + Some stockPlanId -> Some stockPlanId + None -> case Map.lookup securityId successorIndex of + Some (_, stockPlanIdOpt) -> stockPlanIdOpt + None -> None + +addInheritedPlanSuccessorsForSource : + Map Text Text -> + Map Text (Time, Optional Text) -> + Text -> + Time -> + Optional Text -> + [Text] -> + Map Text (Time, Optional Text) +addInheritedPlanSuccessorsForSource declaredPlans successorIndex sourceSecurityId producedAt balanceSecurityId resultingSecurityIds = + case declaredPlanForSecurity declaredPlans successorIndex sourceSecurityId of + Some stockPlanId -> + let withBalance = addOptionalPlanSuccessor producedAt stockPlanId balanceSecurityId successorIndex + in addPlanSuccessors producedAt stockPlanId resultingSecurityIds withBalance + None -> successorIndex + +successorCarriesPlanAllocation : Map Text (Time, Optional Text) -> Text -> Optional Text -> Bool +successorCarriesPlanAllocation successorIndex securityId declaredPlanIdOpt = + case Map.lookup securityId successorIndex of + Some (_, Some inheritedPlanId) -> case declaredPlanIdOpt of + Some declaredPlanId -> declaredPlanId == inheritedPlanId + None -> True + _ -> False + +successorCarriesPlanAllocationOnOrBefore : Map Text (Time, Optional Text) -> Text -> Optional Text -> Time -> Bool +successorCarriesPlanAllocationOnOrBefore successorIndex securityId declaredPlanIdOpt asOf = + case Map.lookup securityId successorIndex of + Some (producedAt, Some inheritedPlanId) -> + producedAt <= asOf && case declaredPlanIdOpt of + Some declaredPlanId -> declaredPlanId == inheritedPlanId + None -> True + _ -> False + +effectivePlanIndex : Map Text Text -> Map Text (Time, Optional Text) -> Map Text Text +effectivePlanIndex declaredPlans successorIndex = + foldl + (\plans (securityId, (_, stockPlanIdOpt)) -> + case (Map.lookup securityId plans, stockPlanIdOpt) of + (None, Some stockPlanId) -> Map.insert securityId stockPlanId plans + _ -> plans) + declaredPlans + (Map.toList successorIndex) + +validateRecordedSuccessorPlanId : + Text -> + Map Text (Time, Optional Text) -> + Text -> + Optional Text -> + Update () +validateRecordedSuccessorPlanId label successorIndex securityId recordedPlanIdOpt = + case (Map.lookup securityId successorIndex, recordedPlanIdOpt) of + (Some (_, inheritedPlanIdOpt), Some recordedPlanId) -> + assertMsg + (label <> " " <> securityId <> " records stock plan " <> recordedPlanId <> " but inherits " <> show inheritedPlanIdOpt) + (inheritedPlanIdOpt == Some recordedPlanId) + _ -> pure () + +validateEquityCompensationSuccessorPlanContinuity : + CapTableMaps -> + Map Text (Time, Optional Text) -> + Update () +validateEquityCompensationSuccessorPlanContinuity maps successorIndex = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateRecordedSuccessorPlanId + "Equity compensation successor" + successorIndex + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id) + (Map.values maps.equity_compensation_issuances) + +validateStockSuccessorPlanContinuity : + CapTableMaps -> + Map Text (Time, Optional Text) -> + Update () +validateStockSuccessorPlanContinuity maps successorIndex = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateRecordedSuccessorPlanId + "Stock successor" + successorIndex + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id) + (Map.values maps.stock_issuances) + +mergePlanIndexes : Map Text Text -> Map Text Text -> Map Text Text +mergePlanIndexes base additions = + foldl + (\plans (securityId, stockPlanId) -> + if isSome (Map.lookup securityId plans) then plans else Map.insert securityId stockPlanId plans) + base + (Map.toList additions) + +equityPlanSuccessorPass : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update (Map Text (Time, Optional Text)) +equityPlanSuccessorPass maps declaredPlans initialIndex = do + cancellationSuccessors <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + pure (addInheritedPlanSuccessorsForSource + declaredPlans + index + cancellation.cancellation_data.security_id + cancellation.cancellation_data.date + cancellation.cancellation_data.balance_security_id + [])) + initialIndex + (Map.values maps.equity_compensation_cancellations) + + foldlA + (\index transferCid -> do + transfer <- fetch transferCid + pure (addInheritedPlanSuccessorsForSource + declaredPlans + index + transfer.transfer_data.security_id + transfer.transfer_data.date + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids)) + cancellationSuccessors + (Map.values maps.equity_compensation_transfers) + +stabilizeEquityPlanSuccessors : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update (Map Text (Time, Optional Text)) +stabilizeEquityPlanSuccessors maps declaredPlans currentIndex = do + nextIndex <- equityPlanSuccessorPass maps declaredPlans currentIndex + if nextIndex == currentIndex + then pure currentIndex + else stabilizeEquityPlanSuccessors maps declaredPlans nextIndex + +stockPlanSuccessorPass : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update (Map Text (Time, Optional Text)) +stockPlanSuccessorPass maps sourcePlans initialIndex = do + cancellationSuccessors <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + pure (addInheritedPlanSuccessorsForSource sourcePlans index cancellation.cancellation_data.security_id cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id [])) + initialIndex + (Map.values maps.stock_cancellations) + + repurchaseSuccessors <- foldlA + (\index repurchaseCid -> do + repurchase <- fetch repurchaseCid + pure (addInheritedPlanSuccessorsForSource sourcePlans index repurchase.repurchase_data.security_id repurchase.repurchase_data.date repurchase.repurchase_data.balance_security_id [])) + cancellationSuccessors + (Map.values maps.stock_repurchases) + + transferSuccessors <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + pure (addInheritedPlanSuccessorsForSource sourcePlans index transfer.transfer_data.security_id transfer.transfer_data.date transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) + repurchaseSuccessors + (Map.values maps.stock_transfers) + + conversionSuccessors <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + pure (addInheritedPlanSuccessorsForSource sourcePlans index conversion.conversion_data.security_id conversion.conversion_data.date conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids)) + transferSuccessors + (Map.values maps.stock_conversions) + + reissuanceSuccessors <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + pure (addInheritedPlanSuccessorsForSource sourcePlans index reissuance.reissuance_data.security_id reissuance.reissuance_data.date None reissuance.reissuance_data.resulting_security_ids)) + conversionSuccessors + (Map.values maps.stock_reissuances) + + consolidationSuccessors <- foldlA + (\index consolidationCid -> do + consolidation <- fetch consolidationCid + foldlA + (\nextIndex sourceSecurityId -> + pure (addInheritedPlanSuccessorsForSource sourcePlans nextIndex sourceSecurityId consolidation.consolidation_data.date None [consolidation.consolidation_data.resulting_security_id])) + index + consolidation.consolidation_data.security_ids) + reissuanceSuccessors + (Map.values maps.stock_consolidations) + + exerciseSuccessors <- foldlA + (\index exerciseCid -> do + exercise <- fetch exerciseCid + pure (addInheritedPlanSuccessorsForSource sourcePlans index exercise.exercise_data.security_id exercise.exercise_data.date None exercise.exercise_data.resulting_security_ids)) + consolidationSuccessors + (Map.values maps.equity_compensation_exercises) + + foldlA + (\index releaseCid -> do + release <- fetch releaseCid + pure (addInheritedPlanSuccessorsForSource sourcePlans index release.release_data.security_id release.release_data.settlement_date None release.release_data.resulting_security_ids)) + exerciseSuccessors + (Map.values maps.equity_compensation_releases) + +stabilizeStockPlanSuccessors : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update (Map Text (Time, Optional Text)) +stabilizeStockPlanSuccessors maps sourcePlans currentIndex = do + nextIndex <- stockPlanSuccessorPass maps sourcePlans currentIndex + if nextIndex == currentIndex + then pure currentIndex + else stabilizeStockPlanSuccessors maps sourcePlans nextIndex + +validateStockConsolidationPlanContinuity : + CapTableMaps -> + Map Text Text -> + Map Text (Time, Optional Text) -> + Update () +validateStockConsolidationPlanContinuity maps sourcePlans successorIndex = + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + let sourcePlanIds = map + (declaredPlanForSecurity sourcePlans successorIndex) + consolidation.consolidation_data.security_ids + case sourcePlanIds of + [] -> pure () + firstPlanId :: remainingPlanIds -> do + assertMsg + ("Stock consolidation " <> consolidation.consolidation_data.id <> " cannot combine sources with different stock-plan attribution") + (all (== firstPlanId) remainingPlanIds) + assertMsg + ("Stock consolidation " <> consolidation.consolidation_data.id <> " resulting security must preserve source stock-plan attribution") + (declaredPlanForSecurity + sourcePlans + successorIndex + consolidation.consolidation_data.resulting_security_id + == firstPlanId)) + (Map.values maps.stock_consolidations) + +stockPlanSuccessorIndex : CapTableMaps -> Update StockPlanSuccessorIndex +stockPlanSuccessorIndex maps = do + equityDeclaredPlans <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> Map.insert issuance.issuance_data.security_id stockPlanId index + None -> index) + Map.empty + (Map.values maps.equity_compensation_issuances) + equitySuccessors <- stabilizeEquityPlanSuccessors maps equityDeclaredPlans Map.empty + validateEquityCompensationSuccessorPlanContinuity maps equitySuccessors + let equityEffectivePlans = effectivePlanIndex equityDeclaredPlans equitySuccessors + + stockDeclaredPlans <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> Map.insert issuance.issuance_data.security_id stockPlanId index + None -> index) + Map.empty + (Map.values maps.stock_issuances) + let stockSourcePlans = mergePlanIndexes stockDeclaredPlans equityEffectivePlans + stockSuccessors <- stabilizeStockPlanSuccessors maps stockSourcePlans Map.empty + validateStockSuccessorPlanContinuity maps stockSuccessors + validateStockConsolidationPlanContinuity maps stockSourcePlans stockSuccessors + + pure StockPlanSuccessorIndex with + stock_successors_by_security_id = stockSuccessors + equity_compensation_successors_by_security_id = equitySuccessors + +recordedInheritedStockIssuanceSummary : + Map Text (Time, Optional Text) -> + Text -> + CapTableMaps -> + [Text] -> + Update (Optional Text, Decimal) +recordedInheritedStockIssuanceSummary successorIndex stockPlanId maps securityIds = + foldlA + (\(stockClassIdOpt, total) securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + if successorCarriesPlanAllocation successorIndex securityId issuance.issuance_data.stock_plan_id + && effectivePlanForSecurity successorIndex securityId issuance.issuance_data.stock_plan_id == Some stockPlanId + then pure (Some issuance.issuance_data.stock_class_id, total + issuance.issuance_data.quantity) + else pure (stockClassIdOpt, total) + None -> pure (stockClassIdOpt, total)) + (None, 0.0) + securityIds + +recordedInheritedStockIssuanceLots : + Map Text (Time, Optional Text) -> + Text -> + CapTableMaps -> + [Text] -> + Update [StockClassShareLot] +recordedInheritedStockIssuanceLots successorIndex stockPlanId maps securityIds = + foldlA + (\lots securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + if successorCarriesPlanAllocation successorIndex securityId issuance.issuance_data.stock_plan_id + && effectivePlanForSecurity successorIndex securityId issuance.issuance_data.stock_plan_id == Some stockPlanId + then pure (stockIssuanceShareLot issuance.issuance_data :: lots) + else pure lots + None -> pure lots) + [] + securityIds + +stockConversionPlanOutputShareLots : + Map Text (Time, Optional Text) -> + Text -> + CapTableMaps -> + Map Text StockClassOcfData -> + StockIssuanceOcfData -> + StockConversionOcfData -> + Update [StockClassShareLot] +stockConversionPlanOutputShareLots successorIndex stockPlanId maps stockClassIndex sourceIssuanceData conversionData = do + resultLots <- if allResultingStockIssuancesExist maps conversionData.resulting_security_ids + then recordedInheritedStockIssuanceLots successorIndex stockPlanId maps conversionData.resulting_security_ids + else do + outputLotOpt <- stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversionData + pure $ case outputLotOpt of + Some outputLot -> [outputLot] + None -> [] + balanceLots <- case conversionData.balance_security_id of + Some balanceSecurityId -> + recordedInheritedStockIssuanceLots successorIndex stockPlanId maps [balanceSecurityId] + None -> pure [] + pure (resultLots ++ balanceLots) + +stockPlanSuccessorExcessLot : + Text -> + Text -> + Time -> + Decimal -> + Decimal -> + [(Text, StockClassShareLot)] +stockPlanSuccessorExcessLot stockPlanId stockClassId eventDate sourceQuantity successorQuantity = + if successorQuantity > sourceQuantity + then + [ (stockPlanId, StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = eventDate + quantity = successorQuantity - sourceQuantity) + ] + else [] + +stockPlanInheritedSuccessorExcessLots : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Update [(Text, StockClassShareLot)] +stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps = do + reissuanceLots <- foldlA + (\lots reissuanceCid -> do + reissuance <- fetch reissuanceCid + case Map.lookup reissuance.reissuance_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + sourceIssuance.issuance_data.security_id + sourceIssuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + sourceQuantity <- stockSecurityQuantityBeforeEvent + eventIndex + maps + sourceIssuance.issuance_data + (reissuance.reissuance_data.date, 3, 3) + (_, successorQuantity) <- recordedInheritedStockIssuanceSummary + successorIndex.stock_successors_by_security_id + stockPlanId + maps + reissuance.reissuance_data.resulting_security_ids + pure (stockPlanSuccessorExcessLot + stockPlanId + sourceIssuance.issuance_data.stock_class_id + reissuance.reissuance_data.date + sourceQuantity + successorQuantity ++ lots) + None -> pure lots + None -> pure lots) + [] + (Map.values maps.stock_reissuances) + + pure reissuanceLots + +stockPlanConversionAllocationTransitions : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Update [(Text, StockClassShareLot, [StockClassShareLot])] +stockPlanConversionAllocationTransitions eventIndex successorIndex maps = do + stockClassIndex <- stockClassDataIndex maps + foldlA + (\transitions conversionCid -> do + conversion <- fetch conversionCid + case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + sourceIssuance.issuance_data.security_id + sourceIssuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + outputLots <- stockConversionPlanOutputShareLots + successorIndex.stock_successors_by_security_id + stockPlanId + maps + stockClassIndex + sourceIssuance.issuance_data + conversion.conversion_data + sourceBasisQuantity <- case conversion.conversion_data.balance_security_id of + Some _ -> stockSecurityQuantityBeforeTerminal + eventIndex + maps + sourceIssuance.issuance_data + conversion.conversion_data.date + None -> pure conversion.conversion_data.quantity_converted + pure $ case outputLots of + _ :: _ -> + ( stockPlanId + , StockClassShareLot with + stock_class_id = sourceIssuance.issuance_data.stock_class_id + security_id = Some sourceIssuance.issuance_data.security_id + event_date = conversion.conversion_data.date + quantity = sourceBasisQuantity + , outputLots + ) :: transitions + [] -> transitions + None -> pure transitions + None -> pure transitions) + [] + (Map.values maps.stock_conversions) + +stockPlanConversionAllocationAdjustmentIndex : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Update (Map Text Decimal) +stockPlanConversionAllocationAdjustmentIndex eventIndex successorIndex maps = do + transitions <- stockPlanConversionAllocationTransitions eventIndex successorIndex maps + foldlA + (\totals (stockPlanId, sourceLot, outputLots) -> + let outputQuantityOpt = foldl + (\totalOpt outputLot -> addOptionalDecimal totalOpt (stockClassShareLotPlanAllocatedQuantity eventIndex outputLot)) + (Some 0.0) + outputLots + in + case + ( stockClassShareLotPlanAllocatedQuantity eventIndex sourceLot + , outputQuantityOpt + ) of + (Some sourceQuantity, Some outputQuantity) -> + pure (addQuantityById stockPlanId (outputQuantity - sourceQuantity) totals) + _ -> do + assertMsg ("Stock conversion plan-allocation transition has an invalid quantity for " <> stockPlanId) False + pure totals) + Map.empty + transitions + +stockPlanConversionAllocationAdjustmentIndexAt : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Time -> + Update (Map Text Decimal) +stockPlanConversionAllocationAdjustmentIndexAt eventIndex successorIndex maps asOf = do + transitions <- stockPlanConversionAllocationTransitions eventIndex successorIndex maps + foldlA + (\totals (stockPlanId, sourceLot, outputLots) -> + let outputQuantityOpt = foldl + (\totalOpt outputLot -> addOptionalDecimal totalOpt (stockClassShareLotPlanAllocatedQuantityAt eventIndex outputLot asOf)) + (Some 0.0) + outputLots + in + case + ( stockClassShareLotPlanAllocatedQuantityAt eventIndex sourceLot asOf + , outputQuantityOpt + ) of + (Some sourceQuantity, Some outputQuantity) -> + pure (addQuantityById stockPlanId (outputQuantity - sourceQuantity) totals) + _ -> do + assertMsg ("Stock conversion plan-allocation transition has an invalid quantity for " <> stockPlanId <> " on " <> show asOf) False + pure totals) + Map.empty + transitions + +stockPlanSuccessorExcessAllocationIndex : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Update (Map Text Decimal) +stockPlanSuccessorExcessAllocationIndex eventIndex successorIndex maps = do + excessLots <- stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps + foldlA + (\totals (stockPlanId, lot) -> + case stockClassShareLotPlanAllocatedQuantity eventIndex lot of + Some quantity -> do + assertNonNegativeQuantity ("Inherited stock-plan successor excess for " <> stockPlanId) quantity + pure (addQuantityById stockPlanId quantity totals) + None -> do + assertMsg ("Inherited stock-plan successor excess has an invalid quantity for " <> stockPlanId) False + pure totals) + Map.empty + excessLots + +stockPlanSuccessorExcessAllocationIndexAt : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Time -> + Update (Map Text Decimal) +stockPlanSuccessorExcessAllocationIndexAt eventIndex successorIndex maps asOf = do + excessLots <- stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps + foldlA + (\totals (stockPlanId, lot) -> + case stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf of + Some quantity -> do + assertNonNegativeQuantity ("Inherited stock-plan successor excess for " <> stockPlanId <> " on " <> show asOf) quantity + pure (addQuantityById stockPlanId quantity totals) + None -> do + assertMsg ("Inherited stock-plan successor excess has an invalid quantity for " <> stockPlanId <> " on " <> show asOf) False + pure totals) + Map.empty + excessLots + +stockIssuancePlanAllocationIndex : StockClassShareEventIndex -> StockPlanSuccessorIndex -> CapTableMaps -> Update (Map Text Decimal) +stockIssuancePlanAllocationIndex eventIndex successorIndex maps = + foldlA + (\totals issuanceCid -> do + issuance <- fetch issuanceCid + if successorCarriesPlanAllocation + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id + then pure totals + else case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + let lot = stockIssuanceShareLot issuance.issuance_data + case stockClassShareLotPlanAllocatedQuantity eventIndex lot of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Stock issuance plan allocation has an invalid quantity for " <> issuance.issuance_data.id) False + pure totals + None -> pure totals) + Map.empty + (Map.values maps.stock_issuances) + +equityCompensationPlanStockClassIds : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [Text] +equityCompensationPlanStockClassIds maps issuanceData = + case issuanceData.stock_class_id of + Some stockClassId -> pure [stockClassId] + None -> case issuanceData.stock_plan_id of + Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure stockPlan.plan_data.stock_class_ids + None -> pure [] + None -> pure [] + +equityCompensationPlanSplitEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [StockClassShareEvent] +equityCompensationPlanSplitEvents maps issuanceData = do + stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id `elem` stockClassIds && split.split_data.date >= issuanceData.date + then pure ((StockClassShareMultiplier with + event_date = split.split_data.date + event_priority = 2 + ratio = split.split_data.split_ratio) :: events) + else pure events) + [] + (Map.values maps.stock_class_splits) + case (issuanceData.stock_class_id, stockClassIds, splitEvents) of + (None, _ :: _ :: _, _ :: _) -> do + assertMsg + ("Equity compensation issuance without stock_class_id cannot be split-adjusted across multiple stock-plan classes: " <> issuanceData.id) + False + pure [] + _ -> pure splitEvents + +equityCompensationPlanAllocationEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [StockClassShareEvent] +equityCompensationPlanAllocationEvents maps issuanceData = do + retractionEvents <- foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == issuanceData.security_id + then pure ((StockClassShareRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) :: events) + else pure events) + [] + (Map.values maps.equity_compensation_retractions) + + splitEvents <- equityCompensationPlanSplitEvents maps issuanceData + + pure ((StockClassShareDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: (retractionEvents ++ splitEvents)) + +equityCompensationPlanAllocatedQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) +equityCompensationPlanAllocatedQuantity maps issuanceData = do + events <- equityCompensationPlanAllocationEvents maps issuanceData + pure (foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey events)) + +equityCompensationPlanAllocatedQuantityAt : CapTableMaps -> EquityCompensationIssuanceOcfData -> Time -> Update (Optional Decimal) +equityCompensationPlanAllocatedQuantityAt maps issuanceData asOf = do + if issuanceData.date <= asOf + then do + events <- equityCompensationPlanAllocationEvents maps issuanceData + pure (foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn + stockClassShareEventSortKey + (filter (stockClassShareEventOnOrBefore asOf) events))) + else pure (Some 0.0) + +equityCompensationPlanAllocationIndex : StockPlanSuccessorIndex -> CapTableMaps -> Update (Map Text Decimal) +equityCompensationPlanAllocationIndex successorIndex maps = + foldlA + (\totals issuanceCid -> do + issuance <- fetch issuanceCid + if successorCarriesPlanAllocation + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id + then pure totals + else case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity + allocationQuantityOpt <- equityCompensationPlanAllocatedQuantity maps issuance.issuance_data + case allocationQuantityOpt of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Equity compensation plan allocation has an invalid quantity history for " <> issuance.issuance_data.id) False + pure totals + None -> pure totals) + Map.empty + (Map.values maps.equity_compensation_issuances) + +stockPlanReturnToPoolStockClassId : CapTableMaps -> Text -> Update (Optional Text) +stockPlanReturnToPoolStockClassId maps securityId = + case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + pure (Some issuance.issuance_data.stock_class_id) + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + stockClassIds <- equityCompensationPlanStockClassIds maps issuance.issuance_data + case stockClassIds of + [stockClassId] -> pure (Some stockClassId) + _ -> pure None + (Some _, Some _) -> do + assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> securityId) False + pure None + (None, None) -> pure None + +stockPlanReturnToPoolEvents : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update [StockClassShareEvent] +stockPlanReturnToPoolEvents maps returnData = do + stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnData.security_id + splitEvents <- case stockClassIdOpt of + Some stockClassId -> + foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id == stockClassId && split.split_data.date >= returnData.date + then pure ((StockClassShareMultiplier with + event_date = split.split_data.date + event_priority = 2 + ratio = split.split_data.split_ratio) :: events) + else pure events) + [] + (Map.values maps.stock_class_splits) + None -> pure [] + + pure ((StockClassShareDelta with + event_date = returnData.date + event_priority = 0 + quantity_delta = returnData.quantity) :: splitEvents) + +stockPlanReturnToPoolQuantity : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update (Optional Decimal) +stockPlanReturnToPoolQuantity maps returnData = do + events <- stockPlanReturnToPoolEvents maps returnData + pure (foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey events)) + +stockPlanReturnToPoolQuantityAt : CapTableMaps -> StockPlanReturnToPoolOcfData -> Time -> Update (Optional Decimal) +stockPlanReturnToPoolQuantityAt maps returnData asOf = do + if returnData.date <= asOf + then do + events <- stockPlanReturnToPoolEvents maps returnData + pure (foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn + stockClassShareEventSortKey + (filter (stockClassShareEventOnOrBefore asOf) events))) + else pure (Some 0.0) + +stockPlanReturnedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanReturnedQuantityIndex maps = + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id) returnToPool.return_data.quantity + returnedQuantityOpt <- stockPlanReturnToPoolQuantity maps returnToPool.return_data + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id) returnedQuantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnedQuantity totals) + None -> do + assertMsg ("Stock plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id) False + pure totals) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + +stockPlanReturnedQuantityBySecurityAt : CapTableMaps -> Text -> Time -> Update Decimal +stockPlanReturnedQuantityBySecurityAt maps securityId asOf = + foldlA + (\total returnCid -> do + returnToPool <- fetch returnCid + if returnToPool.return_data.security_id == securityId && returnToPool.return_data.date <= asOf + then do + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnToPool.return_data.quantity + returnedQuantityOpt <- stockPlanReturnToPoolQuantityAt maps returnToPool.return_data asOf + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity ("Stock plan split-adjusted return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnedQuantity + pure (total + returnedQuantity) + None -> do + assertMsg ("Stock plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id <> " on " <> show asOf) False + pure total + else pure total) + 0.0 + (Map.values maps.stock_plan_return_to_pools) + +equityCompensationSecurityEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [StockClassShareEvent] +equityCompensationSecurityEvents maps issuanceData = do + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + && equityCompensationBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_security_id + then pure (stockSecurityReductionEvents + cancellation.cancellation_data.date + cancellation.cancellation_data.quantity + cancellation.cancellation_data.balance_security_id ++ events) + else pure events) + [] + (Map.values maps.equity_compensation_cancellations) + + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == issuanceData.security_id + then do + let label = "Equity compensation transfer " <> transfer.transfer_data.id + assertNoPartialEquityCompensationSuccessorCoverage + label + maps + transfer.transfer_data.resulting_security_ids + transfer.transfer_data.balance_security_id + if allEquityCompensationSuccessorIssuancesExist + maps + transfer.transfer_data.resulting_security_ids + transfer.transfer_data.balance_security_id + then pure (stockSecurityReductionEvents + transfer.transfer_data.date + transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id ++ events) + else pure events + else pure events) + cancellationEvents + (Map.values maps.equity_compensation_transfers) + + exerciseEvents <- foldlA + (\events exerciseCid -> do + exercise <- fetch exerciseCid + if exercise.exercise_data.security_id == issuanceData.security_id + then pure ((StockClassShareDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - exercise.exercise_data.quantity) :: events) + else pure events) + transferEvents + (Map.values maps.equity_compensation_exercises) + + releaseEvents <- foldlA + (\events releaseCid -> do + release <- fetch releaseCid + if release.release_data.security_id == issuanceData.security_id + then pure ((StockClassShareDelta with + event_date = release.release_data.settlement_date + event_priority = 1 + quantity_delta = 0.0 - release.release_data.quantity) :: events) + else pure events) + exerciseEvents + (Map.values maps.equity_compensation_releases) + + retractionEvents <- foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == issuanceData.security_id + then pure ((StockClassShareRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) :: events) + else pure events) + releaseEvents + (Map.values maps.equity_compensation_retractions) + + splitEvents <- equityCompensationPlanSplitEvents maps issuanceData + + pure (retractionEvents ++ splitEvents) + +equityCompensationSecurityQuantityEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [SecurityQuantityEvent] +equityCompensationSecurityQuantityEvents maps issuanceData = do + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == issuanceData.security_id + then pure (securityQuantityReductionEvent transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id :: events) + else pure events) + [] + (Map.values maps.equity_compensation_transfers) + + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + then pure (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id :: events) + else pure events) + [] + (Map.values maps.equity_compensation_cancellations) + + exerciseEvents <- foldlA + (\events exerciseCid -> do + exercise <- fetch exerciseCid + if exercise.exercise_data.security_id == issuanceData.security_id + then do + assertNonNegativeQuantity + ("Equity compensation exercise quantity for " <> exercise.exercise_data.id) + exercise.exercise_data.quantity + pure ((SecurityQuantityDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - exercise.exercise_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.equity_compensation_exercises) + + releaseEvents <- foldlA + (\events releaseCid -> do + release <- fetch releaseCid + if release.release_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityDelta with + event_date = release.release_data.settlement_date + event_priority = 1 + quantity_delta = 0.0 - release.release_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.equity_compensation_releases) + + retractionEvents <- foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.equity_compensation_retractions) + + stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id `elem` stockClassIds && split.split_data.date >= issuanceData.date + then pure ((SecurityQuantityMultiplier with + event_date = split.split_data.date + event_priority = 2 + ratio = split.split_data.split_ratio) + :: events) + else pure events) + [] + (Map.values maps.stock_class_splits) + case (issuanceData.stock_class_id, stockClassIds, splitEvents) of + (None, _ :: _ :: _, _ :: _) -> do + assertMsg + ("Equity compensation issuance without stock_class_id cannot be split-adjusted across multiple stock-plan classes: " <> issuanceData.id) + False + _ -> pure () + + pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ releaseEvents ++ retractionEvents ++ splitEvents) + +validateEquityCompensationSecurityBalance : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update () +validateEquityCompensationSecurityBalance maps issuanceData = do + events <- equityCompensationSecurityQuantityEvents maps issuanceData + validateSecurityQuantityBalance + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: events) + +validateEquityCompensationSecurityBalances : CapTableMaps -> Update () +validateEquityCompensationSecurityBalances maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateEquityCompensationSecurityBalance maps issuance.issuance_data) + (Map.values maps.equity_compensation_issuances) + +equityCompensationCurrentQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) +equityCompensationCurrentQuantity maps issuanceData = do + events <- equityCompensationSecurityEvents maps issuanceData + pure (foldl + applyStockClassShareEventOptional + (Some issuanceData.quantity) + (sortOn stockClassShareEventSortKey events)) + +equityCompensationQuantityAt : CapTableMaps -> EquityCompensationIssuanceOcfData -> Time -> Update (Optional Decimal) +equityCompensationQuantityAt maps issuanceData asOf = do + if issuanceData.date <= asOf + then do + events <- equityCompensationSecurityEvents maps issuanceData + pure (foldl + applyStockClassShareEventOptional + (Some issuanceData.quantity) + (sortOn stockClassShareEventSortKey (filter (stockClassShareEventOnOrBefore asOf) events))) + else pure (Some 0.0) + +equityCompensationSecurityQuantityBeforeEvent : + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + (Time, Int, Int) -> + Update Decimal +equityCompensationSecurityQuantityBeforeEvent maps issuanceData targetSortKey = do + events <- equityCompensationSecurityQuantityEvents maps issuanceData + quantityOpt <- securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: events) + targetSortKey + case quantityOpt of + Some quantity -> pure quantity + None -> do + assertMsg ("Equity compensation quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False + pure 0.0 + +validateEquityCompensationBalanceSuccessorQuantity : + CapTableMaps -> + Text -> + [Optional Text] -> + Time -> + (Time, Int, Int) -> + Decimal -> + Text -> + Update () +validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClassIds expectedDate targetSortKey expectedQuantity securityId = + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " balance date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == expectedDate) + assertMsg + (label <> " balance stock class mismatch for " <> securityId <> ": expected one of " <> show expectedStockClassIds <> ", found " <> show issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id `elem` expectedStockClassIds) + actualQuantity <- equityCompensationIssuanceQuantityAtSortKey maps issuance.issuance_data targetSortKey + assertMsg + (label <> " balance quantity mismatch for " <> securityId <> ": expected " <> show expectedQuantity <> ", found " <> show actualQuantity) + (actualQuantity == expectedQuantity) + None -> + assertMsg (label <> " balance equity compensation issuance not found: " <> securityId) False + +validateEquityCompensationCancellationSuccessorContinuity : CapTableMaps -> Update () +validateEquityCompensationCancellationSuccessorContinuity maps = + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + case cancellation.cancellation_data.balance_security_id of + Some balanceSecurityId -> do + let label = "Equity compensation cancellation " <> cancellation.cancellation_data.id + assertMsg + (label <> " balance security must differ from source security " <> cancellation.cancellation_data.security_id) + (balanceSecurityId /= cancellation.cancellation_data.security_id) + case Map.lookup cancellation.cancellation_data.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let terminalSortKey = (cancellation.cancellation_data.date, 2, 1) + currentQuantity <- equityCompensationSecurityQuantityBeforeEvent + maps + sourceIssuance.issuance_data + terminalSortKey + let expectedBalanceQuantity = currentQuantity - cancellation.cancellation_data.quantity + assertMsg + (label <> " exceeds the current source quantity for " <> sourceIssuance.issuance_data.security_id) + (expectedBalanceQuantity >= 0.0) + expectedStockClassIds <- equityCompensationSuccessorStockClassIds maps sourceIssuance.issuance_data + validateEquityCompensationBalanceSuccessorQuantity + maps + label + expectedStockClassIds + cancellation.cancellation_data.date + terminalSortKey + expectedBalanceQuantity + balanceSecurityId + None -> + assertMsg (label <> " source equity compensation issuance not found: " <> cancellation.cancellation_data.security_id) False + None -> pure ()) + (Map.values maps.equity_compensation_cancellations) + +validateEquityCompensationTransferSuccessorContinuity : CapTableMaps -> Update () +validateEquityCompensationTransferSuccessorContinuity maps = + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Equity compensation transfer " <> transfer.transfer_data.id + assertNoPartialEquityCompensationSuccessorCoverage + label + maps + transfer.transfer_data.resulting_security_ids + transfer.transfer_data.balance_security_id + if allEquityCompensationSuccessorIssuancesExist + maps + transfer.transfer_data.resulting_security_ids + transfer.transfer_data.balance_security_id + then case Map.lookup transfer.transfer_data.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let targetSortKey = stockReductionSortKey transfer.transfer_data.date transfer.transfer_data.balance_security_id + expectedStockClassIds <- equityCompensationSuccessorStockClassIds maps sourceIssuance.issuance_data + resultingQuantity <- equityCompensationIssuanceQuantityTotal + maps + label + expectedStockClassIds + transfer.transfer_data.date + targetSortKey + transfer.transfer_data.resulting_security_ids + assertMsg + (label <> " resulting quantity mismatch: expected " <> show transfer.transfer_data.quantity <> ", found " <> show resultingQuantity) + (resultingQuantity == transfer.transfer_data.quantity) + case transfer.transfer_data.balance_security_id of + Some balanceSecurityId -> do + let terminalSortKey = (transfer.transfer_data.date, 2, 1) + currentQuantity <- equityCompensationSecurityQuantityBeforeEvent + maps + sourceIssuance.issuance_data + terminalSortKey + let expectedBalanceQuantity = currentQuantity - transfer.transfer_data.quantity + assertMsg + (label <> " exceeds the current source quantity for " <> sourceIssuance.issuance_data.security_id) + (expectedBalanceQuantity >= 0.0) + validateEquityCompensationBalanceSuccessorQuantity + maps + label + expectedStockClassIds + transfer.transfer_data.date + terminalSortKey + expectedBalanceQuantity + balanceSecurityId + None -> pure () + None -> + assertMsg (label <> " source equity compensation issuance not found: " <> transfer.transfer_data.security_id) False + else pure ()) + (Map.values maps.equity_compensation_transfers) + +validateStockPlanEquityCompensationSourceQuantities : CapTableMaps -> Update () +validateStockPlanEquityCompensationSourceQuantities maps = do + successorIndex <- stockPlanSuccessorIndex maps + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case effectivePlanForSecurity + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of + Some _ -> do + hasExercise <- foldlA + (\found exerciseCid -> do + exercise <- fetch exerciseCid + pure (found || exercise.exercise_data.security_id == issuance.issuance_data.security_id)) + False + (Map.values maps.equity_compensation_exercises) + hasRelease <- foldlA + (\found releaseCid -> do + release <- fetch releaseCid + pure (found || release.release_data.security_id == issuance.issuance_data.security_id)) + False + (Map.values maps.equity_compensation_releases) + hasRetraction <- foldlA + (\found retractionCid -> do + retraction <- fetch retractionCid + pure (found || retraction.retraction_data.security_id == issuance.issuance_data.security_id)) + False + (Map.values maps.equity_compensation_retractions) + assertMsg + ("Stock-plan equity compensation cannot be retracted after producing stock: " <> issuance.issuance_data.security_id) + (not (hasRetraction && (hasExercise || hasRelease))) + currentQuantityOpt <- equityCompensationCurrentQuantity maps issuance.issuance_data + case currentQuantityOpt of + Some currentQuantity -> + assertNonNegativeQuantity + ("Stock-plan equity compensation source quantity for " <> issuance.issuance_data.security_id) + currentQuantity + None -> + assertMsg + ("Stock-plan equity compensation source was reduced below zero: " <> issuance.issuance_data.security_id) + False + None -> pure ()) + (Map.values maps.equity_compensation_issuances) + +stockPlanReturnableSplitEvents : CapTableMaps -> Optional Text -> Update [StockClassShareEvent] +stockPlanReturnableSplitEvents maps stockClassIdOpt = case stockClassIdOpt of + Some stockClassId -> + foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id == stockClassId + then pure ((StockClassShareMultiplier with + event_date = split.split_data.date + event_priority = 2 + ratio = split.split_data.split_ratio) :: events) + else pure events) + [] + (Map.values maps.stock_class_splits) + None -> pure [] + +stockPlanCancellationQuantityAt : Text -> [StockClassShareEvent] -> Time -> Update Decimal +stockPlanCancellationQuantityAt securityId events asOf = + case foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey (filter (stockClassShareEventOnOrBefore asOf) events)) of + Some quantity -> do + assertNonNegativeQuantity ("Stock plan split-adjusted cancellation quantity for " <> securityId <> " on " <> show asOf) quantity + pure quantity + None -> do + assertMsg ("Stock plan cancellation quantity history is invalid for " <> securityId <> " on " <> show asOf) False + pure 0.0 + +stockCancellationQuantityAt : CapTableMaps -> StockIssuanceOcfData -> Time -> Update Decimal +stockCancellationQuantityAt maps issuanceData asOf = do + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + && stockBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_security_id + then pure ((StockClassShareDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = cancellation.cancellation_data.quantity) :: events) + else pure events) + [] + (Map.values maps.stock_cancellations) + splitEvents <- stockPlanReturnableSplitEvents maps (Some issuanceData.stock_class_id) + stockPlanCancellationQuantityAt issuanceData.security_id (cancellationEvents ++ splitEvents) asOf + +equityCompensationCancellationQuantityAt : CapTableMaps -> EquityCompensationIssuanceOcfData -> Time -> Update Decimal +equityCompensationCancellationQuantityAt maps issuanceData asOf = do + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + && equityCompensationBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_security_id + then pure ((StockClassShareDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = cancellation.cancellation_data.quantity) :: events) + else pure events) + [] + (Map.values maps.equity_compensation_cancellations) + splitEvents <- equityCompensationPlanSplitEvents maps issuanceData + stockPlanCancellationQuantityAt issuanceData.security_id (cancellationEvents ++ splitEvents) asOf + +stockPlanSecurityReturnableQuantityAt : StockPlanSuccessorIndex -> CapTableMaps -> StockPlanReturnToPoolOcfData -> Time -> Update Decimal +stockPlanSecurityReturnableQuantityAt successorIndex maps returnData asOf = do + let stockCidOpt = Map.lookup returnData.security_id maps.stock_issuances_by_security_id + let equityCompensationCidOpt = Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id + case (stockCidOpt, equityCompensationCidOpt) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of + Some _ -> stockCancellationQuantityAt maps issuance.issuance_data asOf + None -> do + assertMsg ("Stock plan return to pool references non-plan stock security: " <> returnData.security_id) False + pure 0.0 + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + case effectivePlanForSecurity + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of + Some _ -> equityCompensationCancellationQuantityAt maps issuance.issuance_data asOf + None -> do + assertMsg ("Stock plan return to pool references non-plan equity compensation security: " <> returnData.security_id) False + pure 0.0 + (Some _, Some _) -> do + assertMsg ("Stock plan return to pool security id is ambiguous: " <> returnData.security_id) False + pure 0.0 + (None, None) -> do + assertMsg ("Stock plan return to pool security not found: " <> returnData.security_id) False + pure 0.0 + +stockPlanSecurityEffectivePlanId : StockPlanSuccessorIndex -> CapTableMaps -> Text -> Update (Optional Text) +stockPlanSecurityEffectivePlanId successorIndex maps securityId = + case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + pure (effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id) + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + pure (effectivePlanForSecurity + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id) + (Some _, Some _) -> do + assertMsg ("Stock plan return to pool security id is ambiguous: " <> securityId) False + pure None + (None, None) -> pure None + +stockPlanCrossPlanReturnedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanCrossPlanReturnedQuantityIndex maps = do + successorIndex <- stockPlanSuccessorIndex maps + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id + case sourcePlanIdOpt of + Some sourcePlanId -> + if sourcePlanId /= returnToPool.return_data.stock_plan_id + then do + returnedQuantityOpt <- stockPlanReturnToPoolQuantity maps returnToPool.return_data + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity + ("Cross-plan return-to-pool quantity for " <> returnToPool.return_data.id) + returnedQuantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnedQuantity totals) + None -> do + assertMsg + ("Cross-plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id) + False + pure totals + else pure totals + None -> pure totals) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + +stockPlanCrossPlanReturnedQuantityIndexAt : CapTableMaps -> Time -> Update (Map Text Decimal) +stockPlanCrossPlanReturnedQuantityIndexAt maps asOf = do + successorIndex <- stockPlanSuccessorIndex maps + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + if returnToPool.return_data.date <= asOf + then do + sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id + case sourcePlanIdOpt of + Some sourcePlanId -> + if sourcePlanId /= returnToPool.return_data.stock_plan_id + then do + returnedQuantityOpt <- stockPlanReturnToPoolQuantityAt maps returnToPool.return_data asOf + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity + ("Cross-plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) + returnedQuantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnedQuantity totals) + None -> do + assertMsg + ("Cross-plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id <> " on " <> show asOf) + False + pure totals + else pure totals + None -> pure totals + else pure totals) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + +stockPlanSecurityRecordedStockClassId : CapTableMaps -> Text -> Update (Optional Text) +stockPlanSecurityRecordedStockClassId maps securityId = + case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + pure (Some issuance.issuance_data.stock_class_id) + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + pure issuance.issuance_data.stock_class_id + (Some _, Some _) -> do + assertMsg ("Stock plan return to pool security id is ambiguous: " <> securityId) False + pure None + (None, None) -> pure None + +validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () +validateStockPlanReturnToPoolQuantities maps = do + successorIndex <- stockPlanSuccessorIndex maps + mapA_ + (\returnCid -> do + returnToPool <- fetch returnCid + securityAllocation <- stockPlanSecurityReturnableQuantityAt successorIndex maps returnToPool.return_data returnToPool.return_data.date + returnedQuantity <- stockPlanReturnedQuantityBySecurityAt maps returnToPool.return_data.security_id returnToPool.return_data.date + assertMsg + ("Stock plan return to pool exceeds returnable quantity for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds returnable " <> show securityAllocation) + (returnedQuantity <= securityAllocation)) + (Map.values maps.stock_plan_return_to_pools) + +validateStockPlanReturnToPoolClasses : CapTableMaps -> Update () +validateStockPlanReturnToPoolClasses maps = do + successorIndex <- stockPlanSuccessorIndex maps + mapA_ + (\returnCid -> do + returnToPool <- fetch returnCid + stockClassIdOpt <- stockPlanSecurityRecordedStockClassId maps returnToPool.return_data.security_id + sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id + case stockClassIdOpt of + Some stockClassId -> + assertStockPlanIncludesClass + ("Stock plan return-to-pool " <> returnToPool.return_data.id) + maps + returnToPool.return_data.stock_plan_id + stockClassId + None -> case sourcePlanIdOpt of + Some sourcePlanId -> + assertMsg + ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll a security without a recorded stock class into a different stock plan") + (returnToPool.return_data.stock_plan_id == sourcePlanId) + None -> + assertMsg + ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot determine the source stock plan") + False) + (Map.values maps.stock_plan_return_to_pools) + +stockPlanNetAllocatedQuantityIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text Decimal) +stockPlanNetAllocatedQuantityIndex eventIndex maps = do + successorIndex <- stockPlanSuccessorIndex maps + stockAllocation <- stockIssuancePlanAllocationIndex eventIndex successorIndex maps + equityCompensationAllocation <- equityCompensationPlanAllocationIndex successorIndex maps + successorExcessAllocation <- stockPlanSuccessorExcessAllocationIndex eventIndex successorIndex maps + conversionAdjustment <- stockPlanConversionAllocationAdjustmentIndex eventIndex successorIndex maps + returnedQuantity <- stockPlanReturnedQuantityIndex maps + let combinedAllocation = mergeQuantityMaps + (mergeQuantityMaps + (mergeQuantityMaps stockAllocation equityCompensationAllocation) + successorExcessAllocation) + conversionAdjustment + let netAllocatedByPlan = mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity) + pure netAllocatedByPlan + +stockIssuancePlanAllocationIndexAt : StockClassShareEventIndex -> StockPlanSuccessorIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) +stockIssuancePlanAllocationIndexAt eventIndex successorIndex maps asOf = + foldlA + (\totals issuanceCid -> do + issuance <- fetch issuanceCid + if successorCarriesPlanAllocationOnOrBefore + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id + asOf + then pure totals + else case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + if issuance.issuance_data.date <= asOf + then do + let lot = stockIssuanceShareLot issuance.issuance_data + case stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Stock issuance plan allocation has an invalid quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) False + pure totals + else pure totals + None -> pure totals) + Map.empty + (Map.values maps.stock_issuances) + +equityCompensationPlanAllocationIndexAt : StockPlanSuccessorIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) +equityCompensationPlanAllocationIndexAt successorIndex maps asOf = + foldlA + (\totals issuanceCid -> do + issuance <- fetch issuanceCid + if successorCarriesPlanAllocationOnOrBefore + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id + asOf + then pure totals + else case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + if issuance.issuance_data.date <= asOf + then do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) issuance.issuance_data.quantity + allocationQuantityOpt <- equityCompensationPlanAllocatedQuantityAt maps issuance.issuance_data asOf + case allocationQuantityOpt of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Equity compensation plan allocation has an invalid quantity history for " <> issuance.issuance_data.id <> " on " <> show asOf) False + pure totals + else pure totals + None -> pure totals) + Map.empty + (Map.values maps.equity_compensation_issuances) + +stockPlanReturnedQuantityIndexAt : CapTableMaps -> Time -> Update (Map Text Decimal) +stockPlanReturnedQuantityIndexAt maps asOf = + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + if returnToPool.return_data.date <= asOf + then do + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnToPool.return_data.quantity + returnedQuantityOpt <- stockPlanReturnToPoolQuantityAt maps returnToPool.return_data asOf + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnedQuantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnedQuantity totals) + None -> do + assertMsg ("Stock plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id <> " on " <> show asOf) False + pure totals + else pure totals) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + +stockPlanNetAllocatedQuantityIndexAt : StockClassShareEventIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) +stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf = do + successorIndex <- stockPlanSuccessorIndex maps + stockAllocation <- stockIssuancePlanAllocationIndexAt eventIndex successorIndex maps asOf + equityCompensationAllocation <- equityCompensationPlanAllocationIndexAt successorIndex maps asOf + successorExcessAllocation <- stockPlanSuccessorExcessAllocationIndexAt eventIndex successorIndex maps asOf + conversionAdjustment <- stockPlanConversionAllocationAdjustmentIndexAt eventIndex successorIndex maps asOf + returnedQuantity <- stockPlanReturnedQuantityIndexAt maps asOf + let combinedAllocation = mergeQuantityMaps + (mergeQuantityMaps + (mergeQuantityMaps stockAllocation equityCompensationAllocation) + successorExcessAllocation) + conversionAdjustment + pure (mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity)) + +validateStockPlanAllocationReferences : CapTableMaps -> Map Text Decimal -> Update () +validateStockPlanAllocationReferences maps netAllocatedQuantityIndex = + mapA_ + (\(stockPlanId, _) -> + assertMsg + ("Stock plan allocation references missing stock plan: " <> stockPlanId) + (isSome (Map.lookup stockPlanId maps.stock_plans))) + (Map.toList netAllocatedQuantityIndex) + +validateStockPlanPoolAdjustmentReferences : CapTableMaps -> Update () +validateStockPlanPoolAdjustmentReferences maps = + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + assertMsg + ("Stock plan pool adjustment references missing stock plan: " <> adjustment.adjustment_data.stock_plan_id) + (isSome (Map.lookup adjustment.adjustment_data.stock_plan_id maps.stock_plans))) + (Map.values maps.stock_plan_pool_adjustments) + +assertStockPlanIncludesClass : Text -> CapTableMaps -> Text -> Text -> Update () +assertStockPlanIncludesClass label maps stockPlanId stockClassId = + case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + assertMsg + (label <> " references stock plan " <> stockPlanId <> " outside stock class " <> stockClassId) + (stockClassId `elem` stockPlan.plan_data.stock_class_ids) + None -> assertMsg (label <> " references missing stock plan " <> stockPlanId) False + +validateStockPlanAllocationClasses : CapTableMaps -> Update () +validateStockPlanAllocationClasses maps = do + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + assertStockPlanIncludesClass + ("Stock issuance " <> issuance.issuance_data.id) + maps + stockPlanId + issuance.issuance_data.stock_class_id + None -> pure ()) + (Map.values maps.stock_issuances) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case (issuance.issuance_data.stock_plan_id, issuance.issuance_data.stock_class_id) of + (Some stockPlanId, Some stockClassId) -> + assertStockPlanIncludesClass + ("Equity compensation issuance " <> issuance.issuance_data.id) + maps + stockPlanId + stockClassId + _ -> pure ()) + (Map.values maps.equity_compensation_issuances) + +assertStockPlanEffectiveForAllocation : Text -> CapTableMaps -> Text -> Time -> Update () +assertStockPlanEffectiveForAllocation label maps stockPlanId allocationDate = + case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + case stockPlanEffectiveDate stockPlan.plan_data of + Some effectiveDate -> + assertMsg + (label <> " references stock plan " <> stockPlanId <> " before it is effective") + (allocationDate >= effectiveDate) + None -> pure () + None -> pure () + +validateStockPlanAllocationEffectiveDates : CapTableMaps -> Update () +validateStockPlanAllocationEffectiveDates maps = do + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + assertStockPlanEffectiveForAllocation + ("Stock issuance " <> issuance.issuance_data.id) + maps + stockPlanId + issuance.issuance_data.date + None -> pure ()) + (Map.values maps.stock_issuances) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + assertStockPlanEffectiveForAllocation + ("Equity compensation issuance " <> issuance.issuance_data.id) + maps + stockPlanId + issuance.issuance_data.date + None -> pure ()) + (Map.values maps.equity_compensation_issuances) + +validateStockPlanReturnToPoolEffectiveDates : CapTableMaps -> Update () +validateStockPlanReturnToPoolEffectiveDates maps = + mapA_ + (\returnCid -> do + returnToPool <- fetch returnCid + assertStockPlanEffectiveForAllocation + ("Stock plan return-to-pool " <> returnToPool.return_data.id) + maps + returnToPool.return_data.stock_plan_id + returnToPool.return_data.date) + (Map.values maps.stock_plan_return_to_pools) + +validateStockPlanReservedShares : + StockClassShareEventIndex -> + CapTableMaps -> + Map Text StockPlanPoolAdjustmentOcfData -> + Update () +validateStockPlanReservedShares eventIndex maps adjustmentIndex = do + netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndex eventIndex maps + crossPlanReturnedQuantityIndex <- stockPlanCrossPlanReturnedQuantityIndex maps + validateStockPlanAllocationReferences maps netAllocatedQuantityIndex + mapA_ + (\stockPlanCid -> do + stockPlan <- fetch stockPlanCid + let reservedShares = currentStockPlanReservedShares adjustmentIndex stockPlan.plan_data + let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) + let crossPlanReturnedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id crossPlanReturnedQuantityIndex) + assertMsg + ("Stock plan net allocated quantity for " <> stockPlan.plan_data.id <> " is below its validated cross-plan return credit") + (allocatedQuantity >= 0.0 - crossPlanReturnedQuantity) + assertMsg + ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) + (allocatedQuantity <= reservedShares)) + (Map.values maps.stock_plans) + +stockPlanEffectiveDate : StockPlanOcfData -> Optional Time +stockPlanEffectiveDate stockPlanData = + latestOptionalTime stockPlanData.board_approval_date stockPlanData.stockholder_approval_date + +stockPlanEffectiveOnOrBefore : Time -> StockPlanOcfData -> Bool +stockPlanEffectiveOnOrBefore asOf stockPlanData = + case stockPlanEffectiveDate stockPlanData of + Some effectiveDate -> effectiveDate <= asOf + None -> True + +addStockPlanDate : Text -> Time -> Map Text [Time] -> Map Text [Time] +addStockPlanDate = addDateById + +addStockSecurityPlanDate : + StockPlanSuccessorIndex -> + CapTableMaps -> + Text -> + Time -> + Map Text [Time] -> + Update (Map Text [Time]) +addStockSecurityPlanDate successorIndex maps securityId eventDate index = + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of + Some stockPlanId -> addStockPlanDate stockPlanId eventDate index + None -> index + None -> pure index + +addEquityCompensationSecurityPlanDate : + StockPlanSuccessorIndex -> + CapTableMaps -> + Text -> + Time -> + Map Text [Time] -> + Update (Map Text [Time]) +addEquityCompensationSecurityPlanDate successorIndex maps securityId eventDate index = + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case effectivePlanForSecurity + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of + Some stockPlanId -> addStockPlanDate stockPlanId eventDate index + None -> index + None -> pure index + +addStockClassSplitDateToStockPlans : StockClassSplitOcfData -> CapTableMaps -> Map Text [Time] -> Update (Map Text [Time]) +addStockClassSplitDateToStockPlans splitData maps index = + foldlA + (\dates stockPlanCid -> do + stockPlan <- fetch stockPlanCid + if splitData.stock_class_id `elem` stockPlan.plan_data.stock_class_ids + then pure (addStockPlanDate stockPlan.plan_data.id splitData.date dates) + else pure dates) + index + (Map.values maps.stock_plans) + +addStockClassSplitDateToConvertedStockPlans : + [(Text, StockClassShareLot, [StockClassShareLot])] -> + StockClassSplitOcfData -> + Map Text [Time] -> + Map Text [Time] +addStockClassSplitDateToConvertedStockPlans transitions splitData index = + foldl + (\dates (stockPlanId, _, outputLots) -> + if any (\outputLot -> outputLot.stock_class_id == splitData.stock_class_id) outputLots + then addStockPlanDate stockPlanId splitData.date dates + else dates) + index + transitions + +stockPlanValidationDateIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text [Time]) +stockPlanValidationDateIndex eventIndex maps = do + successorIndex <- stockPlanSuccessorIndex maps + conversionTransitions <- stockPlanConversionAllocationTransitions eventIndex successorIndex maps + planDates <- foldlA + (\index stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure $ case stockPlanEffectiveDate stockPlan.plan_data of + Some effectiveDate -> addStockPlanDate stockPlan.plan_data.id effectiveDate index + None -> index) + Map.empty + (Map.values maps.stock_plans) + + adjustmentDates <- foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addStockPlanDate adjustment.adjustment_data.stock_plan_id adjustment.adjustment_data.date index)) + planDates + (Map.values maps.stock_plan_pool_adjustments) + + stockIssuanceDates <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> addStockPlanDate stockPlanId issuance.issuance_data.date index + None -> index) + adjustmentDates + (Map.values maps.stock_issuances) + + equityCompensationDates <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> addStockPlanDate stockPlanId issuance.issuance_data.date index + None -> index) + stockIssuanceDates + (Map.values maps.equity_compensation_issuances) + + returnDates <- foldlA + (\index returnCid -> do + returnToPool <- fetch returnCid + pure (addStockPlanDate returnToPool.return_data.stock_plan_id returnToPool.return_data.date index)) + equityCompensationDates + (Map.values maps.stock_plan_return_to_pools) + + transferDates <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + addStockSecurityPlanDate + successorIndex maps transfer.transfer_data.security_id transfer.transfer_data.date index) + returnDates + (Map.values maps.stock_transfers) + + reissuanceDates <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + addStockSecurityPlanDate + successorIndex maps reissuance.reissuance_data.security_id reissuance.reissuance_data.date index) + transferDates + (Map.values maps.stock_reissuances) + + conversionDates <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + addStockSecurityPlanDate + successorIndex maps conversion.conversion_data.security_id conversion.conversion_data.date index) + reissuanceDates + (Map.values maps.stock_conversions) + + stockRetractionDates <- foldlA + (\index retractionCid -> do + retraction <- fetch retractionCid + addStockSecurityPlanDate + successorIndex maps retraction.retraction_data.security_id retraction.retraction_data.date index) + conversionDates + (Map.values maps.stock_retractions) + + equityCompensationRetractionDates <- foldlA + (\index retractionCid -> do + retraction <- fetch retractionCid + addEquityCompensationSecurityPlanDate + successorIndex maps retraction.retraction_data.security_id retraction.retraction_data.date index) + stockRetractionDates + (Map.values maps.equity_compensation_retractions) + + foldlA + (\index splitCid -> do + split <- fetch splitCid + planSplitDates <- addStockClassSplitDateToStockPlans split.split_data maps index + pure (addStockClassSplitDateToConvertedStockPlans conversionTransitions split.split_data planSplitDates)) + equityCompensationRetractionDates + (Map.values maps.stock_class_splits) + +validateStockPlanReservedSharesAtDates : + StockClassShareEventIndex -> + CapTableMaps -> + Map Text [StockPlanPoolAdjustmentOcfData] -> + Update () +validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do + validationDates <- stockPlanValidationDateIndex eventIndex maps + mapA_ + (\stockPlanCid -> do + stockPlan <- fetch stockPlanCid + mapA_ + (\asOf -> do + netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf + crossPlanReturnedQuantityIndex <- stockPlanCrossPlanReturnedQuantityIndexAt maps asOf + validateStockPlanAllocationReferences maps netAllocatedQuantityIndex + let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) + let crossPlanReturnedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id crossPlanReturnedQuantityIndex) + assertMsg + ("Stock plan allocated quantity for " <> stockPlan.plan_data.id <> " on " <> show asOf <> " is below its validated cross-plan return credit") + (allocatedQuantity >= 0.0 - crossPlanReturnedQuantity) + if stockPlanEffectiveOnOrBefore asOf stockPlan.plan_data + then do + let reservedShares = stockPlanReservedSharesAt adjustmentHistory stockPlan.plan_data asOf + assertMsg + ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> " on " <> show asOf <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) + (allocatedQuantity <= reservedShares) + else pure ()) + (fromOptional [] (Map.lookup stockPlan.plan_data.id validationDates))) + (Map.values maps.stock_plans) + +validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockPlanPoolCeilings eventIndex maps = do + validateStockPlanPoolAdjustmentReferences maps + adjustmentIndex <- stockPlanPoolAdjustmentIndex maps + adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps + validateStockPlanAllocationClasses maps + validateStockPlanAllocationEffectiveDates maps + validateStockPlanReturnToPoolEffectiveDates maps + validateStockPlanEquityCompensationSourceQuantities maps + validateStockPlanReturnToPoolClasses maps + validateStockPlanReturnToPoolQuantities maps + validateStockPlanReservedShares eventIndex maps adjustmentIndex + validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory + +assertSuccessorSecurityIdDistinct : Text -> Text -> Text -> Update () +assertSuccessorSecurityIdDistinct label sourceSecurityId successorSecurityId = + assertMsg + (label <> " must use a new successor security ID instead of reusing " <> sourceSecurityId) + (successorSecurityId /= sourceSecurityId) + +validateSuccessorSecurityIds : Text -> Text -> Optional Text -> [Text] -> Update () +validateSuccessorSecurityIds label sourceSecurityId balanceSecurityId resultingSecurityIds = do + mapA_ (assertSuccessorSecurityIdDistinct label sourceSecurityId) resultingSecurityIds + case balanceSecurityId of + Some balanceId -> do + assertSuccessorSecurityIdDistinct label sourceSecurityId balanceId + assertMsg + (label <> " cannot use its balance security as a resulting security: " <> balanceId) + (balanceId `notElem` resultingSecurityIds) + None -> pure () + +validateSuccessorSecurityIdTypes : Text -> [Text] -> [Text] -> Update () +validateSuccessorSecurityIdTypes label unexpectedIssuanceIds successorSecurityIds = + mapA_ + (\successorSecurityId -> + assertMsg + (label <> " successor security ID collides with a live security of another type: " <> successorSecurityId) + (successorSecurityId `notElem` unexpectedIssuanceIds)) + successorSecurityIds + +successorSecurityIdsFor : Optional Text -> [Text] -> [Text] +successorSecurityIdsFor balanceSecurityId resultingSecurityIds = + case balanceSecurityId of + Some balanceId -> balanceId :: resultingSecurityIds + None -> resultingSecurityIds + +validateGlobalSuccessorSecurityIdUniqueness : CapTableMaps -> Update () +validateGlobalSuccessorSecurityIdUniqueness maps = do + stockTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + [] + (Map.values maps.stock_transfers) + stockCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + stockTransferIds + (Map.values maps.stock_cancellations) + stockRepurchaseIds <- foldlA + (\securityIds repurchaseCid -> do + repurchase <- fetch repurchaseCid + pure (successorSecurityIdsFor repurchase.repurchase_data.balance_security_id [] ++ securityIds)) + stockCancellationIds + (Map.values maps.stock_repurchases) + stockConversionIds <- foldlA + (\securityIds conversionCid -> do + conversion <- fetch conversionCid + pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) + stockRepurchaseIds + (Map.values maps.stock_conversions) + stockReissuanceIds <- foldlA + (\securityIds reissuanceCid -> do + reissuance <- fetch reissuanceCid + pure (reissuance.reissuance_data.resulting_security_ids ++ securityIds)) + stockConversionIds + (Map.values maps.stock_reissuances) + stockConsolidationIds <- foldlA + (\securityIds consolidationCid -> do + consolidation <- fetch consolidationCid + pure (consolidation.consolidation_data.resulting_security_id :: securityIds)) + stockReissuanceIds + (Map.values maps.stock_consolidations) + warrantTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + stockConsolidationIds + (Map.values maps.warrant_transfers) + warrantCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + warrantTransferIds + (Map.values maps.warrant_cancellations) + warrantExerciseIds <- foldlA + (\securityIds exerciseCid -> do + exercise <- fetch exerciseCid + pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) + warrantCancellationIds + (Map.values maps.warrant_exercises) + convertibleTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + warrantExerciseIds + (Map.values maps.convertible_transfers) + convertibleCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + convertibleTransferIds + (Map.values maps.convertible_cancellations) + convertibleConversionIds <- foldlA + (\securityIds conversionCid -> do + conversion <- fetch conversionCid + pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) + convertibleCancellationIds + (Map.values maps.convertible_conversions) + equityCompensationTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + convertibleConversionIds + (Map.values maps.equity_compensation_transfers) + equityCompensationCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + equityCompensationTransferIds + (Map.values maps.equity_compensation_cancellations) + equityCompensationExerciseIds <- foldlA + (\securityIds exerciseCid -> do + exercise <- fetch exerciseCid + pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) + equityCompensationCancellationIds + (Map.values maps.equity_compensation_exercises) + allSuccessorIds <- foldlA + (\securityIds releaseCid -> do + release <- fetch releaseCid + pure (release.release_data.resulting_security_ids ++ securityIds)) + equityCompensationExerciseIds + (Map.values maps.equity_compensation_releases) + assertMsg + "Each successor security ID may be produced by only one lifecycle transaction" + (unique allSuccessorIds) + +validateSuccessorSecurityIdContinuity : CapTableMaps -> Update () +validateSuccessorSecurityIdContinuity maps = do + validateGlobalSuccessorSecurityIdUniqueness maps + let stockUnexpectedIssuanceIds = + Map.keys maps.warrant_issuances_by_security_id + ++ Map.keys maps.convertible_issuances_by_security_id + ++ Map.keys maps.equity_compensation_issuances_by_security_id + let warrantUnexpectedIssuanceIds = + Map.keys maps.stock_issuances_by_security_id + ++ Map.keys maps.convertible_issuances_by_security_id + ++ Map.keys maps.equity_compensation_issuances_by_security_id + let convertibleUnexpectedIssuanceIds = + Map.keys maps.stock_issuances_by_security_id + ++ Map.keys maps.warrant_issuances_by_security_id + ++ Map.keys maps.equity_compensation_issuances_by_security_id + let equityCompensationUnexpectedIssuanceIds = + Map.keys maps.stock_issuances_by_security_id + ++ Map.keys maps.warrant_issuances_by_security_id + ++ Map.keys maps.convertible_issuances_by_security_id + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Stock transfer " <> transfer.transfer_data.id + validateSuccessorSecurityIds + label + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) + (Map.values maps.stock_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + let label = "Stock cancellation " <> cancellation.cancellation_data.id + validateSuccessorSecurityIds + label + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + [] + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) + (Map.values maps.stock_cancellations) + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + let label = "Stock repurchase " <> repurchase.repurchase_data.id + validateSuccessorSecurityIds + label + repurchase.repurchase_data.security_id + repurchase.repurchase_data.balance_security_id + [] + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + (successorSecurityIdsFor repurchase.repurchase_data.balance_security_id [])) + (Map.values maps.stock_repurchases) + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + let label = "Stock conversion " <> conversion.conversion_data.id + validateSuccessorSecurityIds + label + conversion.conversion_data.security_id + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids)) + (Map.values maps.stock_conversions) + mapA_ + (\reissuanceCid -> do + reissuance <- fetch reissuanceCid + let label = "Stock reissuance " <> reissuance.reissuance_data.id + validateSuccessorSecurityIds + label + reissuance.reissuance_data.security_id + None + reissuance.reissuance_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + reissuance.reissuance_data.resulting_security_ids) + (Map.values maps.stock_reissuances) + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + let label = "Stock consolidation " <> consolidation.consolidation_data.id + mapA_ + (\sourceSecurityId -> + assertSuccessorSecurityIdDistinct + label + sourceSecurityId + consolidation.consolidation_data.resulting_security_id) + consolidation.consolidation_data.security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + [consolidation.consolidation_data.resulting_security_id]) + (Map.values maps.stock_consolidations) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Warrant transfer " <> transfer.transfer_data.id + validateSuccessorSecurityIds + label + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + warrantUnexpectedIssuanceIds + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) + (Map.values maps.warrant_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + let label = "Warrant cancellation " <> cancellation.cancellation_data.id + validateSuccessorSecurityIds + label + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + [] + validateSuccessorSecurityIdTypes + label + warrantUnexpectedIssuanceIds + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) + (Map.values maps.warrant_cancellations) + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + let label = "Warrant exercise " <> exercise.exercise_data.id + validateSuccessorSecurityIds + label + exercise.exercise_data.security_id + None + exercise.exercise_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + exercise.exercise_data.resulting_security_ids) + (Map.values maps.warrant_exercises) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Convertible transfer " <> transfer.transfer_data.id + validateSuccessorSecurityIds + label + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + convertibleUnexpectedIssuanceIds + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) + (Map.values maps.convertible_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + let label = "Convertible cancellation " <> cancellation.cancellation_data.id + validateSuccessorSecurityIds + label + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + [] + validateSuccessorSecurityIdTypes + label + convertibleUnexpectedIssuanceIds + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) + (Map.values maps.convertible_cancellations) + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + let label = "Convertible conversion " <> conversion.conversion_data.id + validateSuccessorSecurityIds + label + conversion.conversion_data.security_id + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + convertibleUnexpectedIssuanceIds + (successorSecurityIdsFor conversion.conversion_data.balance_security_id []) + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + conversion.conversion_data.resulting_security_ids) + (Map.values maps.convertible_conversions) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Equity compensation transfer " <> transfer.transfer_data.id + validateSuccessorSecurityIds + label + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + equityCompensationUnexpectedIssuanceIds + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) + (Map.values maps.equity_compensation_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + let label = "Equity compensation cancellation " <> cancellation.cancellation_data.id + validateSuccessorSecurityIds + label + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + [] + validateSuccessorSecurityIdTypes + label + equityCompensationUnexpectedIssuanceIds + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) + (Map.values maps.equity_compensation_cancellations) + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + let label = "Equity compensation exercise " <> exercise.exercise_data.id + validateSuccessorSecurityIds + label + exercise.exercise_data.security_id + None + exercise.exercise_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + exercise.exercise_data.resulting_security_ids) + (Map.values maps.equity_compensation_exercises) + mapA_ + (\releaseCid -> do + release <- fetch releaseCid + let label = "Equity compensation release " <> release.release_data.id + validateSuccessorSecurityIds + label + release.release_data.security_id + None + release.release_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + release.release_data.resulting_security_ids) + (Map.values maps.equity_compensation_releases) + +validateAuthorizedShareCeilings : + CapTableMaps -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + ContractId Issuer -> + Update () +validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do + eventIndex <- stockClassShareEventIndex maps + validateFinalSecurityIdUniqueness maps + validateFinalOcfObjectIdUniqueness maps + validateSuccessorSecurityIdContinuity maps + validateStockSecurityBalances eventIndex maps + validateWarrantSecurityBalances maps + validateEquityCompensationSecurityBalances maps + validateStockBalanceSuccessors eventIndex maps + validateStockTransferResultIssuances maps + validateStockReissuanceResultIssuances eventIndex maps + validateStockConsolidationResultIssuances eventIndex maps + validateEquityCompensationCancellationSuccessorContinuity maps + validateEquityCompensationTransferSuccessorContinuity maps + stockClassIndex <- stockClassDataIndex maps + stockClasses <- stockClassDataList maps + lotsByStockClass <- stockClassShareLotsByStockClass maps stockClassIndex + stockClassAdjustmentHistory <- stockClassAuthorizedAdjustmentHistoryIndex maps + stockClassValidationDates <- stockClassAuthorizedValidationDateIndex maps lotsByStockClass + issuerAdjustmentHistory <- issuerAuthorizedAdjustmentHistoryIndex maps + validateStockClassShareLotReferences stockClassIndex lotsByStockClass + validateStockClassAuthorizedShareAdjustmentReferences maps stockClassIndex + validateIssuerAuthorizedShareAdjustmentReferences maps issuerCid + validateAllStockClassAuthorizedShares eventIndex stockClasses lotsByStockClass adjustmentIndex + validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates + validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid + validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid + validateStockPlanPoolCeilings eventIndex maps + -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int getCreateTier createData = case createData of @@ -130,7 +4871,7 @@ processCreate ctx maps createData = case createData of {{> create-case}} {{/each}} --- | Process a single edit operation, returning updated maps and the edited ContractId +-- | Process a single edit operation, returning updated maps and the edited ContractId. -- Note: OcfEditIssuer is handled separately in UpdateCapTable since issuer is not a map processEdit : Context -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId) processEdit ctx maps editData = case editData of @@ -277,6 +5018,12 @@ template CapTable (mapsAfterEdits, []) sortedCreates + authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps + validateAuthorizedShareCeilings + finalMaps + authorizedShareAdjustmentIndex + newIssuerCid + -- Validate Financing references only after the complete batch state is known. validateFinancingIssuanceReferences finalMaps