diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 454ede48..76f7c6c1 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.15 +version: 0.0.16 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml index bbd1d172..c7022e42 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml @@ -7,6 +7,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, validateOcfSecurityExemption) import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType, OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), validateOcfConvertibleConversionRight, validateOcfConversionTriggerFields) import Fairmint.Shared.TypeHelpers (validateOptionalText) +import DA.List (unique) template ConvertibleIssuance @@ -85,6 +86,7 @@ validateConvertibleIssuanceOcfData d = validateOptionalText d.consideration_text && -- Required arrays per schema not (null d.conversion_triggers) && + unique (map (\trigger -> trigger.trigger_id) d.conversion_triggers) && all validateOcfConvertibleConversionTrigger d.conversion_triggers diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml index a70f783b..74a22490 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml @@ -8,6 +8,7 @@ import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType, OcfSecurityExem import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting) import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTrigger, validateOcfConversionTrigger) import Fairmint.Shared.TypeHelpers (validateOptionalText) +import DA.List (unique) template WarrantIssuance with @@ -95,6 +96,7 @@ validateOcfWarrantIssuanceDataTx d = optional True validateOcfMonetary d.exercise_price && validateOptionalText d.consideration_text && -- exercise_triggers: OCF schema requires array but no minItems, so empty array is valid + unique (map (\trigger -> trigger.trigger_id) d.exercise_triggers) && all validateOcfConversionTrigger d.exercise_triggers && validateOptionalText d.vesting_terms_id && -- If vestings array present, enforce items are valid and minItems >= 1 diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 4340b5e5..bf87f402 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -8,6 +8,7 @@ module Fairmint.OpenCapTable.Types.Conversion where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/ import DA.Optional (isNone, isSome) +import DA.List (sortOn) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateRequiredText) import Fairmint.OpenCapTable.Types.Validation (validateOcfPercentage) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) @@ -72,7 +73,23 @@ data OcfInterestRate = OcfInterestRate deriving (Eq, Show) validateOcfInterestRate : OcfInterestRate -> Bool -validateOcfInterestRate r = validateOcfPercentage r.rate +validateOcfInterestRate r = + validateOcfPercentage r.rate && + optional True (\endDate -> endDate >= r.accrual_start_date) r.accrual_end_date + +validateSortedOcfInterestRateSchedule : [OcfInterestRate] -> Bool +validateSortedOcfInterestRateSchedule rates = case rates of + [] -> True + [rate] -> validateOcfInterestRate rate + current :: next :: rest -> + validateOcfInterestRate current && + current.accrual_start_date < next.accrual_start_date && + optional True (\endDate -> endDate < next.accrual_start_date) current.accrual_end_date && + validateSortedOcfInterestRateSchedule (next :: rest) + +validateOcfInterestRateSchedule : [OcfInterestRate] -> Bool +validateOcfInterestRateSchedule rates = + validateSortedOcfInterestRateSchedule (sortOn (\rate -> rate.accrual_start_date) rates) -- ============================================================================= -- CAPITALIZATION DEFINITION TYPES @@ -360,7 +377,7 @@ data OcfNoteConversionMechanism = OcfNoteConversionMechanism validateOcfNoteConversionMechanism : OcfNoteConversionMechanism -> Bool validateOcfNoteConversionMechanism m = - all validateOcfInterestRate m.interest_rates && + validateOcfInterestRateSchedule m.interest_rates && optional True (\d -> d >= (0.0) && d < (1.0 : Decimal)) m.conversion_discount && optional True validateOcfMonetary m.conversion_valuation_cap && optional True validateOcfRatio m.exit_multiple && @@ -611,6 +628,9 @@ validateOcfConversionTriggerFields triggerType startDate endDate triggerConditio noTriggerDate = isNone triggerDate hasStartDate = isSome startDate hasEndDate = isSome endDate + hasValidRange = case (startDate, endDate) of + (Some start, Some end) -> start <= end + _ -> False noRange = isNone startDate && isNone endDate in case triggerType of OcfTriggerTypeTypeAutomaticOnDate -> @@ -620,7 +640,7 @@ validateOcfConversionTriggerFields triggerType startDate endDate triggerConditio OcfTriggerTypeTypeElectiveOnCondition -> hasCondition && noTriggerDate && noRange OcfTriggerTypeTypeElectiveInRange -> - hasStartDate && hasEndDate && noCondition && noTriggerDate + hasStartDate && hasEndDate && hasValidRange && noCondition && noTriggerDate OcfTriggerTypeTypeElectiveAtWill -> noCondition && noTriggerDate && noRange OcfTriggerTypeTypeUnspecified -> diff --git a/Test/daml.yaml b/Test/daml.yaml index 3cf3a85b..5693c135 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.15.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.16.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestConvertibleSecurityBalances.daml b/Test/daml/OpenCapTable/TestConvertibleSecurityBalances.daml index 59a3cac5..2080a406 100644 --- a/Test/daml/OpenCapTable/TestConvertibleSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestConvertibleSecurityBalances.daml @@ -68,7 +68,7 @@ convertibleConversion transactionId securityId triggerId date resultingSecurityI convertibleIssuance : Text -> Text -> Text -> Time -> Decimal -> ConvertibleIssuanceOcfData convertibleIssuance issuanceId securityId stakeholderId date amount = - (defaultConvertibleIssuance issuanceId securityId stakeholderId ("TRIGGER-" <> issuanceId)) with + (defaultConvertibleIssuance issuanceId securityId stakeholderId "TRIGGER-CONVERTIBLE-CONTINUITY") with date = date investment_amount = OcfMonetary with amount = amount, currency = "USD" @@ -282,6 +282,57 @@ testConvertibleSecurityBalance_TransferCannotChangeSuccessorConversionTerms = sc edits = [] deletes = [] +testConvertibleSecurityBalance_TransferCannotRenameSuccessorTrigger = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let source = convertibleIssuance "CV-ISS-TRIGGER-ID-SOURCE" "CSEC-TRIGGER-ID-SOURCE" "SH-1" jan01 100.0 + baseResult = convertibleIssuance "CV-ISS-TRIGGER-ID-RESULT" "CSEC-TRIGGER-ID-RESULT" "SH-1" jan02 60.0 + result = baseResult with + conversion_triggers = map + (\trigger -> trigger with trigger_id = "TRIGGER-CONVERTIBLE-RENAMED") + baseResult.conversion_triggers + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance source + , CT.OcfCreateConvertibleIssuance result + , CT.OcfCreateConvertibleTransfer (convertibleTransfer + "TX-CV-TRIGGER-ID-TRANSFER" + source.security_id + 60.0 + jan02 + [result.security_id] + None) + ] + edits = [] + deletes = [] + +testConvertibleSecurityBalance_CancellationCannotRenameBalanceTrigger = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let source = convertibleIssuance "CV-ISS-CANCEL-TRIGGER-ID-SOURCE" "CSEC-CANCEL-TRIGGER-ID-SOURCE" "SH-1" jan01 100.0 + baseBalance = convertibleIssuance "CV-ISS-CANCEL-TRIGGER-ID-BALANCE" "CSEC-CANCEL-TRIGGER-ID-BALANCE" "SH-1" jan02 60.0 + balance = baseBalance with + conversion_triggers = map + (\trigger -> trigger with trigger_id = "TRIGGER-CONVERTIBLE-RENAMED") + baseBalance.conversion_triggers + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance source + , CT.OcfCreateConvertibleIssuance balance + , CT.OcfCreateConvertibleCancellation (convertibleCancellation + "TX-CV-CANCEL-TRIGGER-ID" + source.security_id + 40.0 + jan02 + (Some balance.security_id)) + ] + edits = [] + deletes = [] + testConvertibleSecurityBalance_PartialTransferScalesSuccessorProRata = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table @@ -420,7 +471,7 @@ testConvertibleSecurityBalance_ConversionWithBalanceRetiresSource = script do , CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-CONVERSION-BALANCE" "CSEC-CONVERSION-SOURCE" - "TRIGGER-CV-ISS-CONVERSION-SOURCE" + "TRIGGER-CONVERTIBLE-SUCCESSOR" jan02 ["SSEC-CONVERSION-RESULT"] (Some "CSEC-CONVERSION-BALANCE") @@ -447,7 +498,7 @@ testConvertibleSecurityBalance_ConversionWithoutBalanceRetiresSource = script do , CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-FULL-CONVERSION" "CSEC-FULL-CONVERSION-SOURCE" - "TRIGGER-CV-ISS-FULL-CONVERSION-SOURCE" + "TRIGGER-CONVERTIBLE-SUCCESSOR" jan02 ["SSEC-FULL-CONVERSION-RESULT"] None @@ -535,7 +586,7 @@ testConvertibleSecurityBalance_ConversionCannotCreateZeroBalance = script do , CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-CONVERSION-ZERO-BALANCE" "CSEC-CONVERSION-ZERO-SOURCE" - "TRIGGER-CV-ISS-CONVERSION-ZERO-SOURCE" + "TRIGGER-CONVERTIBLE-SUCCESSOR" jan02 ["SSEC-CONVERSION-ZERO-RESULT"] (Some "CSEC-CONVERSION-ZERO-BALANCE") diff --git a/Test/daml/OpenCapTable/TestConvertibleTransfer.daml b/Test/daml/OpenCapTable/TestConvertibleTransfer.daml index 9667f5ff..cb002555 100644 --- a/Test/daml/OpenCapTable/TestConvertibleTransfer.daml +++ b/Test/daml/OpenCapTable/TestConvertibleTransfer.daml @@ -108,7 +108,7 @@ testConvertibleTransfer_NoteFullAssignment = script do (capTableCid, assigneeId) <- setupStakeholderWithId issuer capTableCid "SH-ASSIGNEE" "Convertible Assignee" let securityId = "CSEC-NOTE-001" -- Create a convertible note issuance - let noteTriggerDate = DT.time (DA.date 2025 Jun 15) 0 0 0 + let noteTriggerDate = DT.time (DA.date 2026 Jun 15) 0 0 0 result <- submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleIssuance (convertibleTransferIssuance @@ -133,3 +133,35 @@ testConvertibleTransfer_NoteFullAssignment = script do edits = [] deletes = [] pure () + +testConvertibleTransfer_PreservesRootReachableTriggers = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + (capTableCid, assigneeId) <- setupStakeholderWithId issuer capTableCid "SH-ASSIGNEE" "Convertible Assignee" + let securityId = "CSEC-NOTE-EXPIRED-TRIGGER" + triggerDate = DT.time (DA.date 2025 Jun 15) 0 0 0 + transferDate = DT.time (DA.date 2025 Dec 01) 0 0 0 + result <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleTransferIssuance + "TX_NOTE_EXPIRED_TRIGGER_ISSUANCE" securityId "SH-1" 250000.0 + (DT.time (DA.date 2023 Jun 15) 0 0 0) OcfConvertibleNote triggerDate)] + edits = [] + deletes = [] + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleTransferIssuance + "TX_NOTE_EXPIRED_TRIGGER_SUCCESSOR" "CSEC-NOTE-EXPIRED-TRIGGER-SUCCESSOR" assigneeId 250000.0 + transferDate OcfConvertibleNote triggerDate) + , CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with + id = "TX_NOTE_EXPIRED_TRIGGER_TRANSFER" + date = transferDate + amount = OcfMonetary with amount = 250000.0, currency = "USD" + security_id = securityId + resulting_security_ids = ["CSEC-NOTE-EXPIRED-TRIGGER-SUCCESSOR"] + comments = [] + balance_security_id = None + consideration_text = None] + edits = [] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml index e47f5ff7..e82e6cf6 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml @@ -12,6 +12,9 @@ import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +equityCompensationExpiration : Time +equityCompensationExpiration = DT.time (DA.date 2032 Jan 15) 0 0 0 + -- Create an equity compensation issuance to transfer (follows real workflow) createEquityCompensationIssuance issuer capTableCid securityId quantity issuanceDate = submit issuer do @@ -33,7 +36,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity issuance base_price = None exercise_price = Some (OcfMonetary with amount = 0.50, currency = "USD") early_exercisable = None - expiration_date = Some (DT.time (DA.date 2032 Jan 15) 0 0 0) + expiration_date = Some equityCompensationExpiration stock_class_id = None stock_plan_id = None vesting_terms_id = None @@ -54,7 +57,7 @@ testEquityCompensationTransfer_NSOToFamilyTrust = script do balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "ESEC-NSO-REMAIN" 5000.0 transferDate _ <- submit issuer do exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with - creates = [equityCompensationSuccessorIssuance "EC-ISS-TRANSFERRED-001" "ESEC-NSO-TRANSFERRED-001" transfereeId transferDate 5000.0 + creates = [equityCompensationSuccessorIssuance "EC-ISS-TRANSFERRED-001" "ESEC-NSO-TRANSFERRED-001" transfereeId transferDate 5000.0 (Some equityCompensationExpiration) , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX_EQUITY_COMP_TRANSFER_1" date = transferDate @@ -78,7 +81,7 @@ testEquityCompensationTransfer_ResultQuantityMustMatchTransfer = script do submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = - [ equityCompensationSuccessorIssuance "EC-ISS-QUANTITY-MISMATCH" "ESEC-QUANTITY-MISMATCH-RESULT" transfereeId (DT.time (DA.date 2025 Jun 01) 0 0 0) 59.0 + [ equityCompensationSuccessorIssuance "EC-ISS-QUANTITY-MISMATCH" "ESEC-QUANTITY-MISMATCH-RESULT" transfereeId (DT.time (DA.date 2025 Jun 01) 0 0 0) 59.0 (Some equityCompensationExpiration) , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX-EC-TRANSFER-QUANTITY-MISMATCH" date = DT.time (DA.date 2025 Jun 01) 0 0 0 @@ -103,8 +106,8 @@ testEquityCompensationTransfer_BalanceMustKeepSourceStakeholder = script do submitMustFail issuer do exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with creates = - [ equityCompensationSuccessorIssuance "EC-ISS-BALANCE-HOLDER-RESULT" "ESEC-BALANCE-HOLDER-RESULT" transfereeId transferDate 60.0 - , equityCompensationSuccessorIssuance "EC-ISS-BALANCE-HOLDER-REMAINDER" "ESEC-BALANCE-HOLDER-REMAINDER" transfereeId transferDate 40.0 + [ equityCompensationSuccessorIssuance "EC-ISS-BALANCE-HOLDER-RESULT" "ESEC-BALANCE-HOLDER-RESULT" transfereeId transferDate 60.0 (Some equityCompensationExpiration) + , equityCompensationSuccessorIssuance "EC-ISS-BALANCE-HOLDER-REMAINDER" "ESEC-BALANCE-HOLDER-REMAINDER" transfereeId transferDate 40.0 (Some equityCompensationExpiration) , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX-EC-BALANCE-HOLDER-MISMATCH" date = transferDate @@ -127,7 +130,7 @@ testEquityCompensationTransfer_FullTransfer = script do result <- createEquityCompensationIssuance issuer capTableCid securityId 2500.0 (DT.time (DA.date 2022 Jan 15) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [equityCompensationSuccessorIssuance "EC-ISS-NEWHOLDER" "ESEC-NSO-NEWHOLDER" transfereeId (DT.time (DA.date 2025 Sep 15) 0 0 0) 2500.0 + creates = [equityCompensationSuccessorIssuance "EC-ISS-NEWHOLDER" "ESEC-NSO-NEWHOLDER" transfereeId (DT.time (DA.date 2025 Sep 15) 0 0 0) 2500.0 (Some equityCompensationExpiration) , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX_EQUITY_COMP_TRANSFER_FULL" date = DT.time (DA.date 2025 Sep 15) 0 0 0 @@ -152,7 +155,7 @@ testEquityCompensationTransfer_WithApprovalComments = script do balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "ESEC-NSO-REMAINING" 7000.0 transferDate _ <- submit issuer do exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with - creates = [equityCompensationSuccessorIssuance "EC-ISS-TRUST" "ESEC-NSO-TRUST" transfereeId transferDate 8000.0 + creates = [equityCompensationSuccessorIssuance "EC-ISS-TRUST" "ESEC-NSO-TRUST" transfereeId transferDate 8000.0 (Some equityCompensationExpiration) , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX_EQUITY_COMP_TRANSFER_APPROVED" date = transferDate diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index c506758c..e039c415 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -345,15 +345,16 @@ createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = convertibleSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData convertibleSuccessorIssuance issuanceId securityId stakeholderId date investmentAmount = - CT.OcfCreateConvertibleIssuance ((defaultConvertibleIssuance issuanceId securityId stakeholderId ("TRIGGER-" <> issuanceId)) with + CT.OcfCreateConvertibleIssuance ((defaultConvertibleIssuance issuanceId securityId stakeholderId "TRIGGER-CONVERTIBLE-SUCCESSOR") with date = date investment_amount = OcfMonetary with amount = investmentAmount, currency = "USD") -equityCompensationSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData -equityCompensationSuccessorIssuance issuanceId securityId stakeholderId date quantity = +equityCompensationSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> Optional Time -> CT.OcfCreateData +equityCompensationSuccessorIssuance issuanceId securityId stakeholderId date quantity expirationDate = CT.OcfCreateEquityCompensationIssuance ((defaultEquityCompensationIssuance issuanceId securityId stakeholderId) with date = date - quantity = quantity) + quantity = quantity + expiration_date = expirationDate) -- | Returns valid StockLegendTemplateOcfData with sensible defaults. defaultStockLegendTemplate : Text -> StockLegendTemplateOcfData diff --git a/Test/daml/OpenCapTable/TestInstrumentTemporalEligibility.daml b/Test/daml/OpenCapTable/TestInstrumentTemporalEligibility.daml new file mode 100644 index 00000000..60422515 --- /dev/null +++ b/Test/daml/OpenCapTable/TestInstrumentTemporalEligibility.daml @@ -0,0 +1,856 @@ +module OpenCapTable.TestInstrumentTemporalEligibility where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.ConvertibleConversion (ConvertibleConversionOcfData(..)) +import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) +import Fairmint.OpenCapTable.OCF.ConvertibleTransfer (ConvertibleTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise (EquityCompensationExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantExercise (WarrantExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers + ( addPrerequisites + , defaultConvertibleIssuance + , defaultEquityCompensationIssuance + , defaultWarrantIssuance + ) +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +dec31 : Time +dec31 = DT.time (DA.date 2023 Dec 31) 0 0 0 + +jan01 : Time +jan01 = DT.time (DA.date 2024 Jan 01) 0 0 0 + +jan02 : Time +jan02 = DT.time (DA.date 2024 Jan 02) 0 0 0 + +jan03 : Time +jan03 = DT.time (DA.date 2024 Jan 03) 0 0 0 + +jan04 : Time +jan04 = DT.time (DA.date 2024 Jan 04) 0 0 0 + +fixedConvertibleTrigger : Text -> OcfConversionTriggerType -> Optional Time -> Optional Time -> Optional Time -> OcfConvertibleConversionTrigger +fixedConvertibleTrigger triggerId triggerType startDate endDate triggerDate = OcfConvertibleConversionTrigger with + conversion_right = OcfConvertibleConversionRight with + conversion_mechanism = OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0) + converts_to_future_round = None + converts_to_stock_class_id = Some "SC_COMMON" + type_ = "CONVERTIBLE_CONVERSION_RIGHT" + end_date = endDate + nickname = None + start_date = startDate + trigger_condition = None + trigger_date = triggerDate + trigger_description = None + trigger_id = triggerId + type_ = triggerType + +fixedWarrantTrigger : Text -> OcfConversionTriggerType -> Optional Time -> Optional Time -> Optional Time -> OcfConversionTrigger +fixedWarrantTrigger triggerId triggerType startDate endDate triggerDate = 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 "SC_COMMON" + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = endDate + nickname = None + start_date = startDate + trigger_condition = None + trigger_date = triggerDate + trigger_description = None + trigger_id = triggerId + type_ = triggerType + +atWillWarrantTrigger : Text -> OcfConversionTrigger +atWillWarrantTrigger triggerId = + fixedWarrantTrigger triggerId OcfTriggerTypeTypeElectiveAtWill None None None + +noteTrigger : Text -> [OcfInterestRate] -> OcfConvertibleConversionTrigger +noteTrigger triggerId interestRates = OcfConvertibleConversionTrigger with + conversion_right = OcfConvertibleConversionRight with + conversion_mechanism = OcfConvMechNote OcfNoteConversionMechanism with + capitalization_definition = None + capitalization_definition_rules = None + compounding_type = OcfSimple + conversion_discount = None + conversion_mfn = None + conversion_valuation_cap = None + day_count_convention = OcfDayCountActual365 + exit_multiple = None + interest_accrual_period = OcfAccrualAnnual + interest_payout = OcfInterestPayoutDeferred + interest_rates = interestRates + converts_to_future_round = None + converts_to_stock_class_id = None + 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_ = OcfTriggerTypeTypeUnspecified + +interestRate : Time -> Optional Time -> OcfInterestRate +interestRate startDate endDate = OcfInterestRate with + accrual_start_date = startDate + accrual_end_date = endDate + rate = 0.08 + +convertibleIssuance : Text -> Text -> OcfConvertibleConversionTrigger -> ConvertibleIssuanceOcfData +convertibleIssuance issuanceId securityId trigger = + (defaultConvertibleIssuance issuanceId securityId "SH-1" trigger.trigger_id) with + date = jan01 + conversion_triggers = [trigger] + +warrantIssuance : Text -> Text -> OcfConversionTrigger -> Optional Time -> WarrantIssuanceOcfData +warrantIssuance issuanceId securityId trigger expirationDate = + (defaultWarrantIssuance issuanceId securityId "SH-1") with + date = jan01 + exercise_triggers = [trigger] + warrant_expiration_date = expirationDate + +equityCompensationIssuance : Text -> Text -> Optional Time -> EquityCompensationIssuanceOcfData +equityCompensationIssuance issuanceId securityId expirationDate = + (defaultEquityCompensationIssuance issuanceId securityId "SH-1") with + date = jan01 + expiration_date = expirationDate + stock_class_id = Some "SC_COMMON" + +convertibleConversion : Text -> Text -> Text -> Time -> ConvertibleConversionOcfData +convertibleConversion transactionId securityId triggerId date = ConvertibleConversionOcfData with + id = transactionId + date = date + security_id = securityId + trigger_id = triggerId + reason_text = "Exercise the selected conversion trigger" + resulting_security_ids = [] + comments = [] + balance_security_id = None + capitalization_definition = None + quantity_converted = None + +warrantExercise : Text -> Text -> Text -> Time -> WarrantExerciseOcfData +warrantExercise transactionId securityId triggerId date = WarrantExerciseOcfData with + id = transactionId + date = date + security_id = securityId + trigger_id = triggerId + quantity = Some 100.0 + resulting_security_ids = [] + consideration_text = None + comments = [] + +equityCompensationExercise : Text -> Text -> Time -> EquityCompensationExerciseOcfData +equityCompensationExercise transactionId securityId date = EquityCompensationExerciseOcfData with + id = transactionId + date = date + security_id = securityId + quantity = 10.0 + resulting_security_ids = [] + consideration_text = None + comments = [] + +equityCompensationTransfer : Text -> Text -> Text -> Time -> Decimal -> EquityCompensationTransferOcfData +equityCompensationTransfer transactionId securityId resultingSecurityId date quantity = EquityCompensationTransferOcfData with + id = transactionId + date = date + security_id = securityId + quantity = quantity + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + +equityCompensationCancellation : Text -> Text -> Text -> Time -> Decimal -> EquityCompensationCancellationOcfData +equityCompensationCancellation transactionId securityId balanceSecurityId date quantity = EquityCompensationCancellationOcfData with + id = transactionId + date = date + security_id = securityId + quantity = quantity + balance_security_id = Some balanceSecurityId + reason_text = "Partial cancellation" + comments = [] + +testInstrumentTemporalEligibility_ReversedTriggerRangesFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let convertibleTrigger = fixedConvertibleTrigger + "CV-RANGE-REVERSED" + OcfTriggerTypeTypeElectiveInRange + (Some jan03) + (Some jan02) + None + warrantTrigger = fixedWarrantTrigger + "WR-RANGE-REVERSED" + OcfTriggerTypeTypeElectiveInRange + (Some jan03) + (Some jan02) + None + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuance "TX-CV-RANGE-REVERSED" "SEC-CV-RANGE-REVERSED" convertibleTrigger)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX-WR-RANGE-REVERSED" "SEC-WR-RANGE-REVERSED" warrantTrigger None)] + edits = [] + deletes = [] + +testInstrumentTemporalEligibility_TriggerTermsCannotOutliveWarrantExpiration = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let automaticTrigger = fixedWarrantTrigger + "WR-AUTO-AFTER-EXPIRATION" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan04) + rangeTrigger = fixedWarrantTrigger + "WR-RANGE-AFTER-EXPIRATION" + OcfTriggerTypeTypeElectiveInRange + (Some jan02) + (Some jan04) + None + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX-WR-AUTO-AFTER-EXPIRATION" "SEC-WR-AUTO-AFTER-EXPIRATION" automaticTrigger (Some jan03))] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX-WR-RANGE-AFTER-EXPIRATION" "SEC-WR-RANGE-AFTER-EXPIRATION" rangeTrigger (Some jan03))] + edits = [] + deletes = [] + +testInstrumentTemporalEligibility_TriggerSetMustRemainReachableAfterIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let automaticTrigger = fixedConvertibleTrigger + "CV-AUTO-BEFORE-ISSUANCE" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some dec31) + rangeTrigger = fixedWarrantTrigger + "WR-RANGE-BEFORE-ISSUANCE" + OcfTriggerTypeTypeElectiveInRange + (Some dec31) + (Some dec31) + None + futureTrigger = fixedConvertibleTrigger + "CV-AUTO-AFTER-ISSUANCE" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + mixedTriggerIssuance = + (convertibleIssuance "TX-CV-MIXED-TRIGGERS" "SEC-CV-MIXED-TRIGGERS" automaticTrigger) with + conversion_triggers = [automaticTrigger, futureTrigger] + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuance "TX-CV-AUTO-BEFORE-ISSUANCE" "SEC-CV-AUTO-BEFORE-ISSUANCE" automaticTrigger)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX-WR-RANGE-BEFORE-ISSUANCE" "SEC-WR-RANGE-BEFORE-ISSUANCE" rangeTrigger None)] + edits = [] + deletes = [] + _ <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance mixedTriggerIssuance] + edits = [] + deletes = [] + pure () + +testInstrumentTemporalEligibility_ConvertibleSuccessorKeepsRootReachableTrigger = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let trigger = fixedConvertibleTrigger + "CV-AUTO-BEFORE-SUCCESSOR" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + sourceIssuance = convertibleIssuance + "TX-CV-ROOT-REACHABLE-TRIGGER" + "SEC-CV-ROOT-REACHABLE-TRIGGER" + trigger + successorIssuance = sourceIssuance with + id = "TX-CV-SUCCESSOR-REACHABLE-TRIGGER" + custom_id = "CV-TX-CV-SUCCESSOR-REACHABLE-TRIGGER" + date = jan03 + security_id = "SEC-CV-SUCCESSOR-REACHABLE-TRIGGER" + transfer = ConvertibleTransferOcfData with + id = "TX-CV-TRANSFER-REACHABLE-TRIGGER" + date = jan03 + amount = OcfMonetary with amount = 10000.0, currency = "USD" + security_id = sourceIssuance.security_id + resulting_security_ids = [successorIssuance.security_id] + balance_security_id = None + consideration_text = None + comments = [] + sourceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance sourceIssuance] + edits = [] + deletes = [] + _ <- submit issuer do + exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance successorIssuance + , CT.OcfCreateConvertibleTransfer transfer + ] + edits = [] + deletes = [] + pure () + +testInstrumentTemporalEligibility_WarrantSuccessorKeepsRootReachableTrigger = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let trigger = fixedWarrantTrigger + "WR-AUTO-BEFORE-SUCCESSOR" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + sourceIssuance = warrantIssuance + "TX-WR-ROOT-REACHABLE-TRIGGER" + "SEC-WR-ROOT-REACHABLE-TRIGGER" + trigger + (Some jan04) + successorIssuance = sourceIssuance with + id = "TX-WR-SUCCESSOR-REACHABLE-TRIGGER" + custom_id = "WR-TX-WR-SUCCESSOR-REACHABLE-TRIGGER" + date = jan03 + security_id = "SEC-WR-SUCCESSOR-REACHABLE-TRIGGER" + transfer = WarrantTransferOcfData with + id = "TX-WR-TRANSFER-REACHABLE-TRIGGER" + date = jan03 + security_id = sourceIssuance.security_id + quantity = 100.0 + resulting_security_ids = [successorIssuance.security_id] + balance_security_id = None + consideration_text = None + comments = [] + sourceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance sourceIssuance] + edits = [] + deletes = [] + _ <- submit issuer do + exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance successorIssuance + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + pure () + +testInstrumentTemporalEligibility_TriggerIdsMustBeUniqueWithinIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let convertibleTriggerA = fixedConvertibleTrigger + "CV-DUPLICATE" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + convertibleTriggerB = fixedConvertibleTrigger + "CV-DUPLICATE" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan03) + duplicateConvertibleIssuance = + (convertibleIssuance "TX-CV-DUPLICATE-TRIGGER" "SEC-CV-DUPLICATE-TRIGGER" convertibleTriggerA) with + conversion_triggers = [convertibleTriggerA, convertibleTriggerB] + warrantTriggerA = fixedWarrantTrigger + "WR-DUPLICATE" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + warrantTriggerB = fixedWarrantTrigger + "WR-DUPLICATE" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan03) + duplicateWarrantIssuance = + (warrantIssuance "TX-WR-DUPLICATE-TRIGGER" "SEC-WR-DUPLICATE-TRIGGER" warrantTriggerA None) with + exercise_triggers = [warrantTriggerA, warrantTriggerB] + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance duplicateConvertibleIssuance] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance duplicateWarrantIssuance] + edits = [] + deletes = [] + +-- Current issuance templates reject duplicate trigger IDs in their own ensure, +-- so upgraded legacy state cannot be constructed directly in this package. +-- These predicates are the final-state guard for contracts created by older packages. +testInstrumentTemporalEligibility_UpgradeGuardPredicatesRejectDuplicateTriggerIds = script do + let convertibleTrigger = fixedConvertibleTrigger + "CV-FINAL-DUPLICATE" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + warrantTrigger = fixedWarrantTrigger + "WR-FINAL-DUPLICATE" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + + assert (not (CT.convertibleTriggerIdsAreUnique [convertibleTrigger, convertibleTrigger])) + assert (not (CT.warrantTriggerIdsAreUnique [warrantTrigger, warrantTrigger])) + assert (CT.convertibleTriggerIdsAreUnique [convertibleTrigger]) + assert (CT.warrantTriggerIdsAreUnique [warrantTrigger]) + +testInstrumentTemporalEligibility_AutomaticConversionRequiresExactDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let trigger = fixedConvertibleTrigger + "CV-AUTO-EXACT" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + issuance = convertibleIssuance "TX-CV-AUTO-EXACT-ISSUANCE" "SEC-CV-AUTO-EXACT" trigger + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance issuance] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-AUTO-EXACT" issuance.security_id trigger.trigger_id jan02)] + edits = [] + deletes = [] + pure () + +testInstrumentTemporalEligibility_AutomaticConversionRejectsOtherDates = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let trigger = fixedConvertibleTrigger + "CV-AUTO-MISMATCH" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + issuance = convertibleIssuance "TX-CV-AUTO-MISMATCH-ISSUANCE" "SEC-CV-AUTO-MISMATCH" trigger + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-AUTO-MISMATCH" issuance.security_id trigger.trigger_id jan03)] + edits = [] + deletes = [] + +testInstrumentTemporalEligibility_ElectiveConversionRangeIsInclusive = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let trigger = fixedConvertibleTrigger + "CV-RANGE-INCLUSIVE" + OcfTriggerTypeTypeElectiveInRange + (Some jan02) + (Some jan03) + None + startIssuance = convertibleIssuance "TX-CV-RANGE-START-ISSUANCE" "SEC-CV-RANGE-START" trigger + endIssuance = convertibleIssuance "TX-CV-RANGE-END-ISSUANCE" "SEC-CV-RANGE-END" trigger + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance startIssuance + , CT.OcfCreateConvertibleIssuance endIssuance + ] + edits = [] + deletes = [] + startResult <- submit issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-RANGE-START" startIssuance.security_id trigger.trigger_id jan02)] + edits = [] + deletes = [] + _ <- submit issuer do + exerciseCmd startResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-RANGE-END" endIssuance.security_id trigger.trigger_id jan03)] + edits = [] + deletes = [] + pure () + +testInstrumentTemporalEligibility_ElectiveConversionRejectsOutsideRange = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let trigger = fixedConvertibleTrigger + "CV-RANGE-OUTSIDE" + OcfTriggerTypeTypeElectiveInRange + (Some jan02) + (Some jan03) + None + beforeIssuance = convertibleIssuance "TX-CV-RANGE-BEFORE-ISSUANCE" "SEC-CV-RANGE-BEFORE" trigger + afterIssuance = convertibleIssuance "TX-CV-RANGE-AFTER-ISSUANCE" "SEC-CV-RANGE-AFTER" trigger + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance beforeIssuance + , CT.OcfCreateConvertibleIssuance afterIssuance + ] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-RANGE-BEFORE" beforeIssuance.security_id trigger.trigger_id jan01)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-RANGE-AFTER" afterIssuance.security_id trigger.trigger_id jan04)] + edits = [] + deletes = [] + +testInstrumentTemporalEligibility_WarrantRangeAndExpirationAreInclusive = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let trigger = fixedWarrantTrigger + "WR-RANGE-INCLUSIVE" + OcfTriggerTypeTypeElectiveInRange + (Some jan02) + (Some jan03) + None + startIssuance = warrantIssuance "TX-WR-RANGE-START-ISSUANCE" "SEC-WR-RANGE-START" trigger (Some jan03) + endIssuance = warrantIssuance "TX-WR-RANGE-END-ISSUANCE" "SEC-WR-RANGE-END" trigger (Some jan03) + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance startIssuance + , CT.OcfCreateWarrantIssuance endIssuance + ] + edits = [] + deletes = [] + startResult <- submit issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX-WR-RANGE-START" startIssuance.security_id trigger.trigger_id jan02)] + edits = [] + deletes = [] + _ <- submit issuer do + exerciseCmd startResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX-WR-RANGE-END" endIssuance.security_id trigger.trigger_id jan03)] + edits = [] + deletes = [] + pure () + +testInstrumentTemporalEligibility_WarrantRejectsOutsideRangeAndAfterExpiration = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let rangeTrigger = fixedWarrantTrigger + "WR-RANGE-OUTSIDE" + OcfTriggerTypeTypeElectiveInRange + (Some jan02) + (Some jan03) + None + atWillTrigger = atWillWarrantTrigger "WR-AT-WILL-EXPIRES" + rangeIssuance = warrantIssuance "TX-WR-RANGE-OUTSIDE-ISSUANCE" "SEC-WR-RANGE-OUTSIDE" rangeTrigger None + expiringIssuance = warrantIssuance "TX-WR-EXPIRES-ISSUANCE" "SEC-WR-EXPIRES" atWillTrigger (Some jan03) + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance rangeIssuance + , CT.OcfCreateWarrantIssuance expiringIssuance + ] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX-WR-RANGE-OUTSIDE" rangeIssuance.security_id rangeTrigger.trigger_id jan01)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX-WR-AFTER-EXPIRATION" expiringIssuance.security_id atWillTrigger.trigger_id jan04)] + edits = [] + deletes = [] + +testInstrumentTemporalEligibility_EquityCompensationExpirationIsInclusive = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let exactIssuance = equityCompensationIssuance "TX-EC-EXPIRATION-EXACT-ISSUANCE" "SEC-EC-EXPIRATION-EXACT" (Some jan03) + expiredIssuance = equityCompensationIssuance "TX-EC-EXPIRATION-LATE-ISSUANCE" "SEC-EC-EXPIRATION-LATE" (Some jan03) + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance exactIssuance + , CT.OcfCreateEquityCompensationIssuance expiredIssuance + ] + edits = [] + deletes = [] + exactResult <- submit issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX-EC-EXPIRATION-EXACT" exactIssuance.security_id jan03)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX-EC-AFTER-EXPIRATION" expiredIssuance.security_id jan04)] + edits = [] + deletes = [] + +testInstrumentTemporalEligibility_ExpirationCannotPrecedeIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let warrant = warrantIssuance "TX-WR-INVALID-EXPIRATION" "SEC-WR-INVALID-EXPIRATION" (atWillWarrantTrigger "WR-INVALID-EXPIRATION") (Some dec31) + equityCompensation = equityCompensationIssuance "TX-EC-INVALID-EXPIRATION" "SEC-EC-INVALID-EXPIRATION" (Some dec31) + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance warrant] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance equityCompensation] + edits = [] + deletes = [] + +testInstrumentTemporalEligibility_NoteInterestIntervalsMustBeConsistent = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let validRates = + [ interestRate jan03 None + , interestRate jan01 (Some jan02) + ] + implicitEndRates = + [ interestRate jan01 None + , interestRate jan03 None + ] + preIssuanceRates = [interestRate dec31 None] + reversedRates = [interestRate jan03 (Some jan02)] + overlappingRates = + [ interestRate jan01 (Some jan03) + , interestRate jan03 None + ] + duplicateStartRates = + [ interestRate jan01 None + , interestRate jan01 None + ] + validResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance (convertibleIssuance "TX-CV-NOTE-VALID" "SEC-CV-NOTE-VALID" (noteTrigger "CV-NOTE-VALID" validRates)) + , CT.OcfCreateConvertibleIssuance (convertibleIssuance "TX-CV-NOTE-IMPLICIT-END" "SEC-CV-NOTE-IMPLICIT-END" (noteTrigger "CV-NOTE-IMPLICIT-END" implicitEndRates)) + ] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd validResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuance "TX-CV-NOTE-PRE-ISSUANCE" "SEC-CV-NOTE-PRE-ISSUANCE" (noteTrigger "CV-NOTE-PRE-ISSUANCE" preIssuanceRates))] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd validResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuance "TX-CV-NOTE-REVERSED" "SEC-CV-NOTE-REVERSED" (noteTrigger "CV-NOTE-REVERSED" reversedRates))] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd validResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuance "TX-CV-NOTE-OVERLAP" "SEC-CV-NOTE-OVERLAP" (noteTrigger "CV-NOTE-OVERLAP" overlappingRates))] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd validResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuance "TX-CV-NOTE-DUPLICATE-START" "SEC-CV-NOTE-DUPLICATE-START" (noteTrigger "CV-NOTE-DUPLICATE-START" duplicateStartRates))] + edits = [] + deletes = [] + +testInstrumentTemporalEligibility_EditCannotInvalidateRecordedConversion = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let validTrigger = fixedConvertibleTrigger + "CV-EDITED-TRIGGER" + OcfTriggerTypeTypeAutomaticOnDate + None + None + (Some jan02) + sourceIssuance = convertibleIssuance "TX-CV-EDITED-ISSUANCE" "SEC-CV-EDITED" validTrigger + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance sourceIssuance] + edits = [] + deletes = [] + conversionResult <- submit issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion (convertibleConversion "TX-CV-BEFORE-EDIT" sourceIssuance.security_id validTrigger.trigger_id jan02)] + edits = [] + deletes = [] + let invalidatingTrigger = validTrigger with trigger_date = Some jan03 + submitMustFail issuer do + exerciseCmd conversionResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditConvertibleIssuance (sourceIssuance with conversion_triggers = [invalidatingTrigger])] + deletes = [] + +testInstrumentTemporalEligibility_EditCannotExpireExercisedWarrant = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let trigger = atWillWarrantTrigger "WR-EDITED-EXPIRATION" + sourceIssuance = warrantIssuance "TX-WR-EDITED-ISSUANCE" "SEC-WR-EDITED" trigger (Some jan03) + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance sourceIssuance] + edits = [] + deletes = [] + exerciseResult <- submit issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX-WR-BEFORE-EDIT" sourceIssuance.security_id trigger.trigger_id jan02)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditWarrantIssuance (sourceIssuance with warrant_expiration_date = Some jan01)] + deletes = [] + +testInstrumentTemporalEligibility_EditCannotExpireExercisedEquityCompensation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let sourceIssuance = equityCompensationIssuance "TX-EC-EDITED-ISSUANCE" "SEC-EC-EDITED" (Some jan03) + issuanceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance sourceIssuance] + edits = [] + deletes = [] + exerciseResult <- submit issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX-EC-BEFORE-EDIT" sourceIssuance.security_id jan02)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditEquityCompensationIssuance (sourceIssuance with expiration_date = Some jan01)] + deletes = [] + +testInstrumentTemporalEligibility_EquityCompensationSuccessorsPreserveExpiration = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let sourceIssuance = equityCompensationIssuance "TX-EC-TRANSFER-SOURCE" "SEC-EC-TRANSFER-SOURCE" (Some jan03) + successorIssuance = (equityCompensationIssuance "TX-EC-TRANSFER-SUCCESSOR" "SEC-EC-TRANSFER-SUCCESSOR" (Some jan03)) with date = jan02 + transfer = equityCompensationTransfer + "TX-EC-TRANSFER-PRESERVED-EXPIRATION" + sourceIssuance.security_id + successorIssuance.security_id + jan02 + 100.0 + sourceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance sourceIssuance] + edits = [] + deletes = [] + transferResult <- submit issuer do + exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance successorIssuance + , CT.OcfCreateEquityCompensationTransfer transfer + ] + edits = [] + deletes = [] + exerciseResult <- submit issuer do + exerciseCmd transferResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX-EC-SUCCESSOR-EXPIRATION-EXACT" successorIssuance.security_id jan03)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditEquityCompensationIssuance (successorIssuance with expiration_date = Some jan04)] + deletes = [] + +testInstrumentTemporalEligibility_EquityCompensationSuccessorExpirationCannotChange = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let missingSource = equityCompensationIssuance "TX-EC-MISSING-EXPIRATION-SOURCE" "SEC-EC-MISSING-EXPIRATION-SOURCE" (Some jan03) + extendedSource = equityCompensationIssuance "TX-EC-EXTENDED-EXPIRATION-SOURCE" "SEC-EC-EXTENDED-EXPIRATION-SOURCE" (Some jan03) + cancelledSource = equityCompensationIssuance "TX-EC-CANCELLED-EXPIRATION-SOURCE" "SEC-EC-CANCELLED-EXPIRATION-SOURCE" (Some jan03) + missingSuccessor = (equityCompensationIssuance "TX-EC-MISSING-EXPIRATION-SUCCESSOR" "SEC-EC-MISSING-EXPIRATION-SUCCESSOR" None) with date = jan02 + extendedSuccessor = (equityCompensationIssuance "TX-EC-EXTENDED-EXPIRATION-SUCCESSOR" "SEC-EC-EXTENDED-EXPIRATION-SUCCESSOR" (Some jan04)) with date = jan02 + cancellationBalance = ((equityCompensationIssuance "TX-EC-CANCELLED-EXPIRATION-BALANCE" "SEC-EC-CANCELLED-EXPIRATION-BALANCE" (Some jan04)) with + date = jan02 + quantity = 90.0) + sourceResult <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance missingSource + , CT.OcfCreateEquityCompensationIssuance extendedSource + , CT.OcfCreateEquityCompensationIssuance cancelledSource + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance missingSuccessor + , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer + "TX-EC-MISSING-SUCCESSOR-EXPIRATION" + missingSource.security_id + missingSuccessor.security_id + jan02 + 100.0) + ] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance extendedSuccessor + , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer + "TX-EC-EXTENDED-SUCCESSOR-EXPIRATION" + extendedSource.security_id + extendedSuccessor.security_id + jan02 + 100.0) + ] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance cancellationBalance + , CT.OcfCreateEquityCompensationCancellation (equityCompensationCancellation + "TX-EC-CANCELLATION-BALANCE-EXPIRATION" + cancelledSource.security_id + cancellationBalance.security_id + jan02 + 10.0) + ] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 4fb616ca..22501969 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -940,7 +940,7 @@ testEquityCompensationSecurityBalance_BalanceSecurityReductionPreservesRemainder creates = [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BALANCE_REMAINDER_SOURCE" securityId defaultStakeholderId 100.0) , CT.OcfCreateEquityCompensationIssuance ((equityCompensationIssuance "TX_E_BALANCE_REMAINDER" "ESEC_BALANCE_REMAINDER" defaultStakeholderId 30.0) with date = jan02) - , equityCompensationSuccessorIssuance "EC-ISS-TRANSFER-BEFORE-BALANCE-CANCEL" "TX_E_TRANSFER_BEFORE_BALANCE_CANCEL_RESULT" defaultStakeholderId jan02 40.0 + , equityCompensationSuccessorIssuance "EC-ISS-TRANSFER-BEFORE-BALANCE-CANCEL" "TX_E_TRANSFER_BEFORE_BALANCE_CANCEL_RESULT" defaultStakeholderId jan02 40.0 None , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer "TX_E_TRANSFER_BEFORE_BALANCE_CANCEL" securityId 40.0 jan02) , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_BALANCE_CANCEL_REMAINDER" securityId 30.0 jan02) with balance_security_id = Some "ESEC_BALANCE_REMAINDER") ] diff --git a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml index 939e36fd..12376d7f 100644 --- a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml +++ b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml @@ -136,7 +136,7 @@ testEquityCompensationTransfer_NonExistentSecurityIdFails = script do -- Attempt to create an equity compensation transfer without an existing issuance submitMustFail issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [equityCompensationSuccessorIssuance "EC-ISS-VALID-TRANSFER" "NEW-EC-1" "SH-1" (DT.time (DA.date 2024 Jan 01) 0 0 0) 100.0 + creates = [equityCompensationSuccessorIssuance "EC-ISS-VALID-TRANSFER" "NEW-EC-1" "SH-1" (DT.time (DA.date 2024 Jan 01) 0 0 0) 100.0 None , CT.OcfCreateEquityCompensationTransfer ECT.EquityCompensationTransferOcfData with id = "TX_EC_TRANSFER_1" date = DT.time (DA.date 2024 Jan 01) 0 0 0 @@ -233,7 +233,7 @@ testEquityCompensationTransfer_WrongSecurityTypeFails = script do -- Attempt to create an equity compensation transfer using the convertible's security_id submitMustFail issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [equityCompensationSuccessorIssuance "EC-ISS-WRONG-SOURCE-TYPE" "NEW-EC-1" "SH-1" (DT.time (DA.date 2024 Jan 01) 0 0 0) 100.0 + creates = [equityCompensationSuccessorIssuance "EC-ISS-WRONG-SOURCE-TYPE" "NEW-EC-1" "SH-1" (DT.time (DA.date 2024 Jan 01) 0 0 0) 100.0 None , CT.OcfCreateEquityCompensationTransfer ECT.EquityCompensationTransferOcfData with id = "TX_EC_TRANSFER_WRONG_TYPE" date = DT.time (DA.date 2024 Jan 01) 0 0 0 diff --git a/dars/OpenCapTable-v34/0.0.16/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.16/OpenCapTable-v34.dar new file mode 100644 index 00000000..a06c77a8 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.16/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2556607ba1f16b0df0ed91ef2bbc0877c4250563b8fde0cdc9cb2e99ffbe9928 +size 3331338 diff --git a/dars/dars.lock b/dars/dars.lock index a3201385..cddee110 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -53,6 +53,13 @@ "uploadedAt": "2026-07-11T17:07:52.000Z", "networks": [] }, + "OpenCapTable-v34/0.0.16/OpenCapTable-v34.dar": { + "sha256": "2556607ba1f16b0df0ed91ef2bbc0877c4250563b8fde0cdc9cb2e99ffbe9928", + "size": 3331338, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-11T21:42:33.000Z", + "networks": [] + }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { "sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6", "size": 2446238, diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index aec918a7..70d7d272 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -15,12 +15,12 @@ import qualified DA.Map as Map import DA.List (sortOn, unique) import DA.Set (Set) import qualified DA.Set as Set -import DA.Action (foldlA, when) +import DA.Action (foldlA, unless, when) import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..), validateOcfInterestRateSchedule) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..)) import qualified Fairmint.OpenCapTable.OCF.ConvertibleIssuance as ConvertibleIssuance (OcfConvertibleConversionTrigger(..)) @@ -2171,11 +2171,6 @@ convertiblePrincipalBeforeConversion maps issuanceData conversionData = do (\event -> convertibleBalanceEventSortKey event < targetSortKey) (sortOn convertibleBalanceEventSortKey events)) -normalizeConvertibleConversionTrigger : - ConvertibleIssuance.OcfConvertibleConversionTrigger -> - ConvertibleIssuance.OcfConvertibleConversionTrigger -normalizeConvertibleConversionTrigger trigger = trigger with trigger_id = "" - scaleConvertibleSuccessorTrigger : Decimal -> Decimal -> @@ -2233,8 +2228,8 @@ validateConvertibleSuccessorTerms label sourceIssuanceData successorIssuanceData (successorIssuanceData.seniority == sourceIssuanceData.seniority) assertMsg (label <> " successor conversion terms mismatch for " <> successorIssuanceData.security_id) - (map normalizeConvertibleConversionTrigger successorIssuanceData.conversion_triggers == - map normalizeConvertibleConversionTrigger (expectedConvertibleSuccessorTriggers sourceIssuanceData successorIssuanceData)) + (successorIssuanceData.conversion_triggers == + expectedConvertibleSuccessorTriggers sourceIssuanceData successorIssuanceData) assertMsg (label <> " successor pro rata terms mismatch for " <> successorIssuanceData.security_id) (successorIssuanceData.pro_rata == expectedConvertibleSuccessorProRata sourceIssuanceData successorIssuanceData) @@ -4524,6 +4519,319 @@ validateWarrantExerciseTriggerReferences maps = None -> assertMsg ("Security not found: " <> exercise.exercise_data.security_id) False) (Map.values maps.warrant_exercises) +convertibleSuccessorSecurityIds : CapTableMaps -> Update (Set Text) +convertibleSuccessorSecurityIds maps = do + transferIds <- foldlA + (\ids transferCid -> do + transfer <- fetch transferCid + pure (Set.union + (Set.fromList (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) + ids)) + Set.empty + (Map.values maps.convertible_transfers) + cancellationIds <- foldlA + (\ids cancellationCid -> do + cancellation <- fetch cancellationCid + pure (Set.union + (Set.fromList (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) + ids)) + transferIds + (Map.values maps.convertible_cancellations) + foldlA + (\ids conversionCid -> do + conversion <- fetch conversionCid + pure (Set.union + (Set.fromList (successorSecurityIdsFor conversion.conversion_data.balance_security_id [])) + ids)) + cancellationIds + (Map.values maps.convertible_conversions) + +warrantSuccessorSecurityIds : CapTableMaps -> Update (Set Text) +warrantSuccessorSecurityIds maps = do + transferIds <- foldlA + (\ids transferCid -> do + transfer <- fetch transferCid + pure (Set.union + (Set.fromList (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) + ids)) + Set.empty + (Map.values maps.warrant_transfers) + foldlA + (\ids cancellationCid -> do + cancellation <- fetch cancellationCid + pure (Set.union + (Set.fromList (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) + ids)) + transferIds + (Map.values maps.warrant_cancellations) + +conversionTriggerDefinitionIsTemporallyValid : + Optional Time -> + OcfConversionTriggerType -> + Optional Time -> + Optional Time -> + Optional Time -> + Bool +conversionTriggerDefinitionIsTemporallyValid expirationDate triggerType startDate endDate triggerDate = + let notAfterExpiration date = optional True (\expiration -> date <= expiration) expirationDate + in case triggerType of + OcfTriggerTypeTypeAutomaticOnDate -> + optional False notAfterExpiration triggerDate + OcfTriggerTypeTypeElectiveInRange -> case (startDate, endDate) of + (Some start, Some end) -> + start <= end && notAfterExpiration end + _ -> False + _ -> True + +conversionTriggerCanBeEligibleOnOrAfter : + Time -> + OcfConversionTriggerType -> + Optional Time -> + Optional Time -> + Bool +conversionTriggerCanBeEligibleOnOrAfter issuanceDate triggerType endDate triggerDate = + case triggerType of + OcfTriggerTypeTypeAutomaticOnDate -> + optional False (>= issuanceDate) triggerDate + OcfTriggerTypeTypeElectiveInRange -> + optional False (>= issuanceDate) endDate + _ -> True + +conversionTriggerIsEligibleAt : + Time -> + OcfConversionTriggerType -> + Optional Time -> + Optional Time -> + Optional Time -> + Bool +conversionTriggerIsEligibleAt eventDate triggerType startDate endDate triggerDate = + case triggerType of + OcfTriggerTypeTypeAutomaticOnDate -> case triggerDate of + Some date -> eventDate == date + None -> False + OcfTriggerTypeTypeElectiveInRange -> case (startDate, endDate) of + (Some start, Some end) -> eventDate >= start && eventDate <= end + _ -> False + _ -> True + +assertConversionTriggerDefinitionIsTemporallyValid : + Text -> + Optional Time -> + OcfConversionTriggerType -> + Optional Time -> + Optional Time -> + Optional Time -> + Update () +assertConversionTriggerDefinitionIsTemporallyValid label expirationDate triggerType startDate endDate triggerDate = + assertMsg + (label <> " has invalid trigger timing or extends beyond the instrument expiration") + (conversionTriggerDefinitionIsTemporallyValid expirationDate triggerType startDate endDate triggerDate) + +assertConversionTriggerIsEligibleAt : + Text -> + Time -> + OcfConversionTriggerType -> + Optional Time -> + Optional Time -> + Optional Time -> + Update () +assertConversionTriggerIsEligibleAt label eventDate triggerType startDate endDate triggerDate = + assertMsg + (label <> " is not eligible on " <> show eventDate <> " under its selected trigger") + (conversionTriggerIsEligibleAt eventDate triggerType startDate endDate triggerDate) + +assertExpirationOnOrAfterIssuance : Text -> Time -> Optional Time -> Update () +assertExpirationOnOrAfterIssuance label issuanceDate expirationDate = + case expirationDate of + Some expiration -> + assertMsg + (label <> " expiration " <> show expiration <> " cannot precede issuance " <> show issuanceDate) + (expiration >= issuanceDate) + None -> pure () + +assertEventOnOrBeforeExpiration : Text -> Time -> Optional Time -> Update () +assertEventOnOrBeforeExpiration label eventDate expirationDate = + case expirationDate of + Some expiration -> + assertMsg + (label <> " date " <> show eventDate <> " cannot follow expiration " <> show expiration) + (eventDate <= expiration) + None -> pure () + +convertibleTriggerIdsAreUnique : [ConvertibleIssuance.OcfConvertibleConversionTrigger] -> Bool +convertibleTriggerIdsAreUnique triggers = + unique (map (\trigger -> trigger.trigger_id) triggers) + +warrantTriggerIdsAreUnique : [OcfConversionTrigger] -> Bool +warrantTriggerIdsAreUnique triggers = + unique (map (\trigger -> trigger.trigger_id) triggers) + +validateConvertibleIssuanceTemporalTerms : CapTableMaps -> Update () +validateConvertibleIssuanceTemporalTerms maps = do + successorIds <- convertibleSuccessorSecurityIds maps + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Convertible issuance " <> issuance.issuance_data.id <> " has duplicate conversion trigger IDs") + (convertibleTriggerIdsAreUnique issuance.issuance_data.conversion_triggers) + let rootIssuanceDate = + if Set.member issuance.issuance_data.security_id successorIds + then None + else Some issuance.issuance_data.date + case rootIssuanceDate of + Some issuanceDate -> + assertMsg + ("Convertible issuance " <> issuance.issuance_data.id <> " has no conversion trigger reachable on or after issuance") + (any + (\trigger -> conversionTriggerCanBeEligibleOnOrAfter + issuanceDate + trigger.type_ + trigger.end_date + trigger.trigger_date) + issuance.issuance_data.conversion_triggers) + None -> pure () + mapA_ + (\trigger -> do + let label = "Convertible issuance " <> issuance.issuance_data.id <> " trigger " <> trigger.trigger_id + assertConversionTriggerDefinitionIsTemporallyValid + label + None + trigger.type_ + trigger.start_date + trigger.end_date + trigger.trigger_date + case trigger.conversion_right.conversion_mechanism of + OcfConvMechNote noteMechanism -> do + assertMsg + (label <> " has invalid or overlapping interest rate intervals") + (validateOcfInterestRateSchedule noteMechanism.interest_rates) + case rootIssuanceDate of + Some issuanceDate -> + assertMsg + (label <> " has interest accruing before issuance " <> show issuanceDate) + (all (\rate -> rate.accrual_start_date >= issuanceDate) noteMechanism.interest_rates) + None -> pure () + _ -> pure ()) + issuance.issuance_data.conversion_triggers) + (Map.values maps.convertible_issuances) + +validateWarrantIssuanceTemporalTerms : CapTableMaps -> Update () +validateWarrantIssuanceTemporalTerms maps = do + successorIds <- warrantSuccessorSecurityIds maps + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Warrant issuance " <> issuance.issuance_data.id <> " has duplicate exercise trigger IDs") + (warrantTriggerIdsAreUnique issuance.issuance_data.exercise_triggers) + assertExpirationOnOrAfterIssuance + ("Warrant issuance " <> issuance.issuance_data.id) + issuance.issuance_data.date + issuance.issuance_data.warrant_expiration_date + unless (Set.member issuance.issuance_data.security_id successorIds) do + assertMsg + ("Warrant issuance " <> issuance.issuance_data.id <> " has no exercise trigger reachable on or after issuance") + (null issuance.issuance_data.exercise_triggers || any + (\trigger -> conversionTriggerCanBeEligibleOnOrAfter + issuance.issuance_data.date + trigger.type_ + trigger.end_date + trigger.trigger_date) + issuance.issuance_data.exercise_triggers) + mapA_ + (\trigger -> + assertConversionTriggerDefinitionIsTemporallyValid + ("Warrant issuance " <> issuance.issuance_data.id <> " trigger " <> trigger.trigger_id) + issuance.issuance_data.warrant_expiration_date + trigger.type_ + trigger.start_date + trigger.end_date + trigger.trigger_date) + issuance.issuance_data.exercise_triggers) + (Map.values maps.warrant_issuances) + +validateEquityCompensationIssuanceTemporalTerms : CapTableMaps -> Update () +validateEquityCompensationIssuanceTemporalTerms maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + assertExpirationOnOrAfterIssuance + ("Equity compensation issuance " <> issuance.issuance_data.id) + issuance.issuance_data.date + issuance.issuance_data.expiration_date) + (Map.values maps.equity_compensation_issuances) + +validateConvertibleConversionTemporalEligibility : CapTableMaps -> Update () +validateConvertibleConversionTemporalEligibility maps = + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + case Map.lookup conversion.conversion_data.security_id maps.convertible_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + case convertibleConversionTrigger issuance.issuance_data conversion.conversion_data.trigger_id of + Some trigger -> + assertConversionTriggerIsEligibleAt + ("Convertible conversion " <> conversion.conversion_data.id) + conversion.conversion_data.date + trigger.type_ + trigger.start_date + trigger.end_date + trigger.trigger_date + None -> assertMsg ("Convertible conversion trigger not found: " <> conversion.conversion_data.trigger_id) False + None -> assertMsg ("Convertible conversion security not found: " <> conversion.conversion_data.security_id) False) + (Map.values maps.convertible_conversions) + +validateWarrantExerciseTemporalEligibility : CapTableMaps -> Update () +validateWarrantExerciseTemporalEligibility maps = + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertEventOnOrBeforeExpiration + ("Warrant exercise " <> exercise.exercise_data.id) + exercise.exercise_data.date + issuance.issuance_data.warrant_expiration_date + case warrantExerciseTrigger issuance.issuance_data exercise.exercise_data.trigger_id of + Some trigger -> + assertConversionTriggerIsEligibleAt + ("Warrant exercise " <> exercise.exercise_data.id) + exercise.exercise_data.date + trigger.type_ + trigger.start_date + trigger.end_date + trigger.trigger_date + None -> assertMsg ("Warrant exercise trigger not found: " <> exercise.exercise_data.trigger_id) False + None -> assertMsg ("Warrant exercise security not found: " <> exercise.exercise_data.security_id) False) + (Map.values maps.warrant_exercises) + +validateEquityCompensationExerciseTemporalEligibility : CapTableMaps -> Update () +validateEquityCompensationExerciseTemporalEligibility maps = + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertEventOnOrBeforeExpiration + ("Equity compensation exercise " <> exercise.exercise_data.id) + exercise.exercise_data.date + issuance.issuance_data.expiration_date + None -> assertMsg ("Equity compensation exercise security not found: " <> exercise.exercise_data.security_id) False) + (Map.values maps.equity_compensation_exercises) + +validateInstrumentTemporalEligibility : CapTableMaps -> Update () +validateInstrumentTemporalEligibility maps = do + validateConvertibleIssuanceTemporalTerms maps + validateWarrantIssuanceTemporalTerms maps + validateEquityCompensationIssuanceTemporalTerms maps + validateConvertibleConversionTemporalEligibility maps + validateWarrantExerciseTemporalEligibility maps + validateEquityCompensationExerciseTemporalEligibility maps + validateStockReissuanceSplitReferences : CapTableMaps -> Update () validateStockReissuanceSplitReferences maps = mapA_ @@ -5585,6 +5893,22 @@ validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClass None -> assertMsg (label <> " balance equity compensation issuance not found: " <> securityId) False +validateEquityCompensationSuccessorExpiration : + CapTableMaps -> + Text -> + Optional Time -> + Text -> + Update () +validateEquityCompensationSuccessorExpiration maps label sourceExpiration securityId = + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " successor expiration mismatch for " <> securityId) + (issuance.issuance_data.expiration_date == sourceExpiration) + None -> + assertMsg (label <> " successor equity compensation issuance not found: " <> securityId) False + validateEquityCompensationCancellationSuccessorContinuity : CapTableMaps -> Update () validateEquityCompensationCancellationSuccessorContinuity maps = mapA_ @@ -5609,6 +5933,11 @@ validateEquityCompensationCancellationSuccessorContinuity maps = (label <> " exceeds the current source quantity for " <> sourceIssuance.issuance_data.security_id) (expectedBalanceQuantity >= 0.0) expectedStockClassIds <- equityCompensationSuccessorStockClassIds maps sourceIssuance.issuance_data + validateEquityCompensationSuccessorExpiration + maps + label + sourceIssuance.issuance_data.expiration_date + balanceSecurityId validateEquityCompensationBalanceSuccessorQuantity maps label @@ -5642,6 +5971,12 @@ validateEquityCompensationTransferSuccessorContinuity maps = sourceIssuance <- fetch sourceIssuanceCid let targetSortKey = stockReductionSortKey transfer.transfer_data.date transfer.transfer_data.balance_security_id expectedStockClassIds <- equityCompensationSuccessorStockClassIds maps sourceIssuance.issuance_data + mapA_ + (validateEquityCompensationSuccessorExpiration + maps + label + sourceIssuance.issuance_data.expiration_date) + transfer.transfer_data.resulting_security_ids resultingQuantity <- equityCompensationIssuanceQuantityTotal maps label @@ -5655,6 +5990,11 @@ validateEquityCompensationTransferSuccessorContinuity maps = case transfer.transfer_data.balance_security_id of Some balanceSecurityId -> do let terminalSortKey = (transfer.transfer_data.date, 2, 1) + validateEquityCompensationSuccessorExpiration + maps + label + sourceIssuance.issuance_data.expiration_date + balanceSecurityId currentQuantity <- equityCompensationSecurityQuantityBeforeEvent maps sourceIssuance.issuance_data @@ -7121,6 +7461,7 @@ template CapTable validateFinalSecurityIdUniqueness finalMaps validateSuccessorSecurityIdContinuity finalMaps validateTransactionLifecycleDates finalMaps + validateInstrumentTemporalEligibility finalMaps validateStockBalanceSuccessorQuantities stockEventIndex finalMaps validateNonStockBalanceSuccessorQuantities stockEventIndex nonStockBalanceEventIndex finalMaps validateStockSecurityBalances stockEventIndex finalMaps