diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 70b5e670..4654c331 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.6 +version: 0.0.11 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml index 853fc544..a0b0e564 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleConversion.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.ConvertibleConversion where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/conversion/ConvertibleConversion.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.OpenCapTable.Types.Conversion (OcfCapitalizationDefinition, validateOcfCapitalizationDefinition) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateOptionalDecimal) @@ -75,4 +75,4 @@ validateConvertibleConversionOcfData d = -- Arrays element validation (resulting_security_ids required per Conversion base schema) validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.resulting_security_ids && - not (null d.resulting_security_ids) + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml index 631cbe2f..bbd1d172 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleIssuance.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, validateOcfSecurityExemption) -import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType, OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), validateOcfConvertibleConversionRight) +import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType, OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), validateOcfConvertibleConversionRight, validateOcfConversionTriggerFields) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -124,17 +124,8 @@ data OcfConvertibleConversionTrigger = OcfConvertibleConversionTrigger validateOcfConvertibleConversionTrigger : OcfConvertibleConversionTrigger -> Bool validateOcfConvertibleConversionTrigger t = - let hasDate = case t.trigger_date of Some _ -> True; None -> False - hasCond = case t.trigger_condition of Some c -> c /= ""; None -> False - in t.trigger_id /= "" && + t.trigger_id /= "" && validateOptionalText t.nickname && validateOptionalText t.trigger_description && - validateOptionalText t.trigger_condition && validateOcfConvertibleConversionRight t.conversion_right && - case t.type_ of - OcfTriggerTypeTypeAutomaticOnDate -> hasDate - OcfTriggerTypeTypeAutomaticOnCondition -> hasCond - OcfTriggerTypeTypeElectiveOnCondition -> hasCond - OcfTriggerTypeTypeElectiveInRange -> True - OcfTriggerTypeTypeElectiveAtWill -> True - OcfTriggerTypeTypeUnspecified -> True + validateOcfConversionTriggerFields t.type_ t.start_date t.end_date t.trigger_condition t.trigger_date diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml index 35f6b119..59847220 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/ConvertibleTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.ConvertibleTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/ConvertibleTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -64,9 +64,7 @@ validateConvertibleTransferOcfData d = -- Consistent with: StockTransfer (quantity > 0), WarrantTransfer (quantity > 0), -- EquityCompensationTransfer (quantity > 0), and ConvertibleCancellation (amount > 0) d.amount.amount > 0.0 && - not (null d.resulting_security_ids) && - validateNonEmptyStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Document.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Document.daml index ccc35a9c..f3861add 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Document.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Document.daml @@ -54,10 +54,10 @@ validateOcfDocument d = validateOcfMd5 d.md5 && validateOptionalText d.path && validateOptionalText d.uri && - ((case (d.path, d.uri) of - (Some _, _) -> True - (_, Some _) -> True - _ -> False)) && + (case (d.path, d.uri) of + (Some _, None) -> True + (None, Some _) -> True + _ -> False) && validateNonEmptyStringsArray d.comments && all validateOcfObjectReference d.related_objects @@ -95,4 +95,3 @@ data OcfObjectType = | OcfObjTxWarrantAcceptance | OcfObjTxWarrantCancellation | OcfObjTxWarrantExercise | OcfObjTxWarrantIssuance | OcfObjTxWarrantRetraction | OcfObjTxWarrantTransfer | OcfObjTxVestingAcceleration | OcfObjTxVestingStart | OcfObjTxVestingEvent deriving (Eq, Show) - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml index c11a4ba1..c3ca01bc 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationExercise.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationExercise where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -59,6 +59,7 @@ validateOcfEquityCompensationExerciseData d = -- Required numbers may be zero unless schema specifies otherwise -- Optional Text values, if present, must be non-empty validateOptionalText d.consideration_text && - -- Arrays element validation (allow empty arrays where schema does not require minItems) + -- Arrays element validation validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.resulting_security_ids + validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml index cdd5a21f..56ddca61 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationRelease.daml @@ -2,7 +2,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationRelease where import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,7 +62,7 @@ validateEquityCompensationReleaseOcfData d = d.security_id /= "" && d.quantity > 0.0 && validateOcfMonetary d.release_price && - not (null d.resulting_security_ids) && validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.resulting_security_ids && + validateUniqueStringsArray d.resulting_security_ids && validateOptionalText d.consideration_text diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml index 619916df..bd0094c5 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.EquityCompensationTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/EquityCompensationTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -59,9 +59,7 @@ validateEquityCompensationTransferOcfData d = d.id /= "" && d.security_id /= "" && d.quantity > 0.0 && - not (null d.resulting_security_ids) && - validateNonEmptyStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Issuer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Issuer.daml index 690f5200..673ce160 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Issuer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/Issuer.daml @@ -6,12 +6,11 @@ module Fairmint.OpenCapTable.OCF.Issuer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/Issuer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateOcfCountryCode, validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateOcfCountryCode, validateOcfCountrySubdivisionCode, validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Contact (OcfEmail, OcfPhone, validateOcfEmail, validateOcfPhone) import Fairmint.OpenCapTable.Types.Monetary (OcfAddress, OcfTaxID, validateOcfAddress, validateOcfTaxID) import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..)) import Fairmint.Shared.TypeHelpers (validateOptionalText) -import qualified DA.Text as Text template Issuer with @@ -65,12 +64,6 @@ data IssuerOcfData = IssuerOcfData phone: Optional OcfPhone deriving (Eq, Show) -validateOcfCountrySubdivisionCode : Text -> Bool -validateOcfCountrySubdivisionCode subdivisionCode = - subdivisionCode /= "" && - Text.length subdivisionCode >= 1 && - Text.length subdivisionCode <= 3 - validateOcfLegalName : Text -> Bool validateOcfLegalName name = name /= "" diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml index 43dff6b0..eb39544b 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, validateUniqueStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -61,6 +61,5 @@ validateStockConsolidationOcfData d = validateOptionalText d.reason_text && -- Arrays element validation (security_ids required per Consolidation base schema) validateNonEmptyStringsArray d.comments && - validateNonEmptyStringsArray d.security_ids && - validateUniqueStringsArray d.security_ids && + validateNonEmptyUniqueStringsArray d.security_ids && not (null d.security_ids) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml index 5bce698b..2433d243 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConversion.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockConversion where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/conversion/StockConversion.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) @@ -63,4 +63,4 @@ validateStockConversionOcfData d = -- Arrays element validation (resulting_security_ids required per Conversion base schema) validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.resulting_security_ids && - not (null d.resulting_security_ids) + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml index 18a72c4a..c08016ad 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml @@ -4,7 +4,7 @@ module Fairmint.OpenCapTable.OCF.StockIssuance where import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) -import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, OcfShareNumberRange, validateOcfSecurityExemption, validateOcfShareNumberRange) +import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, OcfShareNumberRange, validateOcfSecurityExemption, validateOcfShareNumberRangesForQuantity) import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -90,13 +90,14 @@ validateOcfStockIssuanceData d = d.stock_class_id /= "" && -- Arrays element validation (allow empty arrays where schema does not require minItems) all validateOcfSecurityExemption d.security_law_exemptions && - all validateOcfShareNumberRange d.share_numbers_issued && + validateOcfShareNumberRangesForQuantity d.quantity d.share_numbers_issued && all validateOcfVesting d.vestings && validateNonEmptyStringsArray d.comments && -- Arrays per schema: schema requires at least 1 legend, but we allow empty validateNonEmptyStringsArray d.stock_legend_ids && -- Required amounts validateOcfMonetary d.share_price && + optional True validateOcfMonetary d.cost_basis && d.quantity > (0.0) && -- Optional Text values, if present, must be non-empty validateOptionalText d.consideration_text && @@ -108,5 +109,3 @@ validateOcfStockIssuanceData d = -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/enums/StockIssuanceType.schema.json data OcfStockIssuanceType = OcfStockIssuanceRSA | OcfStockIssuanceFounders deriving (Eq, Show) - - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml index 2904841c..e385236a 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockReissuance.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockReissuance where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/reissuance/StockReissuance.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) @@ -63,4 +63,4 @@ validateStockReissuanceOcfData d = -- Arrays element validation (resulting_security_ids required per Reissuance base schema) validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.resulting_security_ids && - not (null d.resulting_security_ids) + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml index 2b20b1a9..779a2984 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/StockTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -59,9 +59,7 @@ validateStockTransferOcfData d = d.id /= "" && d.security_id /= "" && d.quantity > 0.0 && - not (null d.resulting_security_ids) && - validateNonEmptyStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml index 03358e82..548bd337 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantExercise.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.WarrantExercise where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/exercise/WarrantExercise.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, validateOptionalDecimal) @@ -67,4 +67,4 @@ validateWarrantExerciseOcfData d = -- Arrays element validation (resulting_security_ids required per Exercise base schema) validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.resulting_security_ids && - not (null d.resulting_security_ids) + validateUniqueStringsArray d.resulting_security_ids diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml index 8c12c063..a70f783b 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml @@ -92,6 +92,7 @@ validateOcfWarrantIssuanceDataTx d = -- Required amounts and lists -- quantity is optional per schema; exercise_price is optional per schema validateOcfMonetary d.purchase_price && + optional True validateOcfMonetary d.exercise_price && validateOptionalText d.consideration_text && -- exercise_triggers: OCF schema requires array but no minItems, so empty array is valid all validateOcfConversionTrigger d.exercise_triggers && diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml index 2238151d..adc4b42f 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantTransfer.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.WarrantTransfer where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/transfer/WarrantTransfer.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateNonEmptyUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -59,9 +59,7 @@ validateWarrantTransferOcfData d = d.id /= "" && d.security_id /= "" && d.quantity > 0.0 && - not (null d.resulting_security_ids) && - validateNonEmptyStringsArray d.resulting_security_ids && + validateNonEmptyUniqueStringsArray d.resulting_security_ids && validateNonEmptyStringsArray d.comments && validateOptionalText d.balance_security_id && validateOptionalText d.consideration_text - diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml index 08d67ec3..557eb0e7 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Contact.daml @@ -31,7 +31,42 @@ data OcfPhone = OcfPhone deriving (Eq, Show) validateOcfPhone : OcfPhone -> Bool -validateOcfPhone phone = validateRequiredText phone.phone_number +validateOcfPhone phone = validateOcfPhoneNumber phone.phone_number + +validDigitsBetween : Int -> Int -> Text -> Bool +validDigitsBetween minLength maxLength digits = + let n = Text.length digits in + n >= minLength && n <= maxLength && all Text.isDigit (Text.explode digits) + +validDigitsAtLeast : Int -> Text -> Bool +validDigitsAtLeast minLength digits = + Text.length digits >= minLength && all Text.isDigit (Text.explode digits) + +validPhoneCountryCode : Text -> Bool +validPhoneCountryCode country = + case Text.stripPrefix "+" country of + Some digits -> validDigitsBetween 1 3 digits + None -> False + +validateOcfPhoneNumberParts : [Text] -> Bool +validateOcfPhoneNumberParts parts = + case parts of + [country, area, exchange, line] -> + validPhoneCountryCode country && + validDigitsBetween 2 3 area && + validDigitsBetween 2 3 exchange && + validDigitsBetween 4 4 line + [country, area, exchange, line, extensionMarker, extension] -> + validateOcfPhoneNumberParts [country, area, exchange, line] && + (extensionMarker == "ext." || extensionMarker == "extension") && + validDigitsAtLeast 1 extension + -- Empty parts from consecutive spaces are intentionally rejected here. + _ -> False + +validateOcfPhoneNumber : Text -> Bool +validateOcfPhoneNumber phoneNumber = + validateRequiredText phoneNumber && + validateOcfPhoneNumberParts (Text.splitOn " " phoneNumber) -- Enum - Email Type -- Type of e-mail address diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml index 5fe62cdb..4340b5e5 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml @@ -7,6 +7,7 @@ module Fairmint.OpenCapTable.Types.Conversion where -- All conversion mechanism types for convertible securities, warrants, and stock. -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/ +import DA.Optional (isNone, isSome) import Fairmint.Shared.TypeHelpers (validateOptionalText, validateRequiredText) import Fairmint.OpenCapTable.Types.Validation (validateOcfPercentage) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) @@ -239,14 +240,24 @@ validateOcfSharePriceBasedConversionMechanism m = (Some _, Some _) -> False else case (m.discount_percentage, m.discount_amount) of + (None, None) -> True + (Some _, None) -> percentOk + (None, Some _) -> amountOk (Some _, Some _) -> False - _ -> percentOk && amountOk -- Enum - Valuation Based Formula Type -- Formula for valuation-based conversion data OcfValuationBasedFormulaType = OcfValuationCap | OcfValuationFixed | OcfValuationActual deriving (Eq, Show) +valuationBasedFormulaTypeFromText : Text -> Optional OcfValuationBasedFormulaType +valuationBasedFormulaTypeFromText valuationType = + case valuationType of + "CAP" -> Some OcfValuationCap + "FIXED" -> Some OcfValuationFixed + "ACTUAL" -> Some OcfValuationActual + _ -> None + -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/conversion_mechanisms/ValuationBasedConversionMechanism.schema.json data OcfValuationBasedConversionMechanism = OcfValuationBasedConversionMechanism with @@ -269,11 +280,12 @@ data OcfValuationBasedConversionMechanism = OcfValuationBasedConversionMechanism validateOcfValuationBasedConversionMechanism : OcfValuationBasedConversionMechanism -> Bool validateOcfValuationBasedConversionMechanism m = - case m.valuation_type of - OcfValuationActual -> True - OcfValuationCap -> optional False validateOcfMonetary m.valuation_amount - OcfValuationFixed -> optional False validateOcfMonetary m.valuation_amount - && validateOptionalText m.capitalization_definition + let amountOk = case m.valuation_type of + OcfValuationActual -> optional True validateOcfMonetary m.valuation_amount + OcfValuationCap -> optional False validateOcfMonetary m.valuation_amount + OcfValuationFixed -> optional False validateOcfMonetary m.valuation_amount + in amountOk && + validateOptionalText m.capitalization_definition -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/conversion_mechanisms/SAFEConversionMechanism.schema.json data OcfSAFEConversionMechanism = OcfSAFEConversionMechanism @@ -434,10 +446,15 @@ validateOcfWarrantConversionMechanism mechanism = validateOptionalText capitalization_definition OcfWarrantMechanismFixedAmount with converts_to_quantity -> converts_to_quantity > (0.0) - OcfWarrantMechanismValuationBased with valuation_type; valuation_amount; capitalization_definition -> - validateRequiredText valuation_type && - optional False validateOcfMonetary valuation_amount && - validateOptionalText capitalization_definition + OcfWarrantMechanismValuationBased with valuation_type; valuation_amount; capitalization_definition; capitalization_definition_rules -> + case valuationBasedFormulaTypeFromText valuation_type of + Some formulaType -> + validateOcfValuationBasedConversionMechanism OcfValuationBasedConversionMechanism with + valuation_type = formulaType + valuation_amount = valuation_amount + capitalization_definition = capitalization_definition + capitalization_definition_rules = capitalization_definition_rules + None -> False OcfWarrantMechanismPpsBased with description; discount; discount_percentage; discount_amount -> validateOcfSharePriceBasedConversionMechanism OcfSharePriceBasedConversionMechanism with description = description @@ -471,9 +488,10 @@ validateOcfConvertibleConversionMechanism mechanism = OcfConvMechCustom m -> validateOcfCustomConversionMechanism m OcfConvMechPercentCapitalization m -> validateOcfPercentCapitalizationConversionMechanism m OcfConvMechFixedAmount m -> validateOcfFixedAmountConversionMechanism m - OcfConvMechPpsBased m -> validateOcfSharePriceBasedConversionMechanism m - OcfConvMechValuationBased m -> validateOcfValuationBasedConversionMechanism m - OcfConvMechRatio r -> validateOcfRatio r + -- Kept for package upgrades; OCF does not support these for convertibles. + OcfConvMechPpsBased _ -> False + OcfConvMechValuationBased _ -> False + OcfConvMechRatio _ -> False -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/conversion_rights/ConvertibleConversionRight.schema.json data OcfConvertibleConversionRight = OcfConvertibleConversionRight @@ -510,7 +528,8 @@ data OcfStockClassConversionRight = OcfStockClassConversionRight -- Mechanism by which conversion occurs conversion_mechanism: OcfConversionMechanism - -- Trigger that would cause conversion + -- Required by the DAML representation, but absent from the OCF stock-class + -- conversion-right schema and therefore ignored by OCF validation. conversion_trigger: OcfConversionTrigger -- Identifier of stock class to which this converts converts_to_stock_class_id: Text @@ -552,32 +571,18 @@ validateOcfStockClassConversionRight conversionRight = validateRequiredText conversionRight.converts_to_stock_class_id && (case conversionRight.conversion_mechanism of OcfConversionMechanismRatioConversion -> - optional False validateOcfRatio conversionRight.ratio - OcfConversionMechanismPercentCapitalizationConversion -> - optional False (\p -> p > (0.0) && p <= (1.0 : Decimal)) conversionRight.percent_of_capitalization - OcfConversionMechanismFixedAmountConversion -> - optional False validateOcfMonetary conversionRight.conversion_price - OcfConversionMechanismValuationBasedConversion -> - optional False validateOcfMonetary conversionRight.reference_valuation_price_per_share - OcfConversionMechanismPpsBasedConversion -> - optional False validateOcfMonetary conversionRight.reference_share_price - OcfConversionMechanismSAFEConversion -> - ((case (conversionRight.discount_rate, conversionRight.valuation_cap) of - (Some d, _) -> d > (0.0) && d < (1.0 : Decimal) - (_, Some cap) -> validateOcfMonetary cap - _ -> False) && - optional True validateOcfMonetary conversionRight.floor_price_per_share && - optional True validateOcfMonetary conversionRight.ceiling_price_per_share) - OcfConversionMechanismNoteConversion -> - ((case (conversionRight.discount_rate, conversionRight.valuation_cap) of - (Some d, _) -> d > (0.0) && d < (1.0 : Decimal) - (_, Some cap) -> validateOcfMonetary cap - _ -> False) && - optional True validateOcfMonetary conversionRight.floor_price_per_share && - optional True validateOcfMonetary conversionRight.ceiling_price_per_share) - OcfConversionMechanismCustomConversion -> - optional False (/= "") conversionRight.custom_description) && - optional True validateOcfMonetary conversionRight.conversion_price + optional False validateOcfRatio conversionRight.ratio && + optional False validateOcfMonetary conversionRight.conversion_price && + isNone conversionRight.percent_of_capitalization && + isNone conversionRight.reference_share_price && + isNone conversionRight.reference_valuation_price_per_share && + isNone conversionRight.discount_rate && + isNone conversionRight.valuation_cap && + isNone conversionRight.custom_description && + isNone conversionRight.floor_price_per_share && + isNone conversionRight.ceiling_price_per_share && + isNone conversionRight.expires_at + _ -> False) -- ============================================================================= -- CONVERSION TRIGGER @@ -597,6 +602,30 @@ validateOcfAnyConversionRight right = OcfRightWarrant warrantRight -> validateOcfWarrantConversionRight warrantRight OcfRightStockClass stockClassRight -> validateOcfStockClassConversionRight stockClassRight +-- Conversion trigger variants have additionalProperties: false in OCF; optional fields +-- must be present only on the variants whose schemas define them. +validateOcfConversionTriggerFields : OcfConversionTriggerType -> Optional Time -> Optional Time -> Optional Text -> Optional Time -> Bool +validateOcfConversionTriggerFields triggerType startDate endDate triggerCondition triggerDate = + let hasCondition = optional False validateRequiredText triggerCondition + noCondition = isNone triggerCondition + noTriggerDate = isNone triggerDate + hasStartDate = isSome startDate + hasEndDate = isSome endDate + noRange = isNone startDate && isNone endDate + in case triggerType of + OcfTriggerTypeTypeAutomaticOnDate -> + isSome triggerDate && noCondition && noRange + OcfTriggerTypeTypeAutomaticOnCondition -> + hasCondition && noTriggerDate && noRange + OcfTriggerTypeTypeElectiveOnCondition -> + hasCondition && noTriggerDate && noRange + OcfTriggerTypeTypeElectiveInRange -> + hasStartDate && hasEndDate && noCondition && noTriggerDate + OcfTriggerTypeTypeElectiveAtWill -> + noCondition && noTriggerDate && noRange + OcfTriggerTypeTypeUnspecified -> + noCondition && noTriggerDate && noRange + -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/primitives/types/conversion_triggers/ConversionTrigger.schema.json data OcfConversionTrigger = OcfConversionTrigger with @@ -629,17 +658,12 @@ data OcfConversionTrigger = OcfConversionTrigger validateOcfConversionTrigger : OcfConversionTrigger -> Bool validateOcfConversionTrigger t = - let hasDate = case t.trigger_date of Some _ -> True; None -> False - hasCond = optional False validateRequiredText t.trigger_condition - in validateRequiredText t.trigger_id && - validateOptionalText t.nickname && - validateOptionalText t.trigger_description && - validateOptionalText t.trigger_condition && - validateOcfAnyConversionRight t.conversion_right && - case t.type_ of - OcfTriggerTypeTypeAutomaticOnDate -> hasDate - OcfTriggerTypeTypeAutomaticOnCondition -> hasCond - OcfTriggerTypeTypeElectiveOnCondition -> hasCond - OcfTriggerTypeTypeElectiveInRange -> True - OcfTriggerTypeTypeElectiveAtWill -> True - OcfTriggerTypeTypeUnspecified -> True + validateOcfConversionTriggerShape t && + validateOcfAnyConversionRight t.conversion_right + +validateOcfConversionTriggerShape : OcfConversionTrigger -> Bool +validateOcfConversionTriggerShape t = + validateRequiredText t.trigger_id && + validateOptionalText t.nickname && + validateOptionalText t.trigger_description && + validateOcfConversionTriggerFields t.type_ t.start_date t.end_date t.trigger_condition t.trigger_date diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml index fc466e31..f9c26a6d 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Monetary.daml @@ -9,7 +9,7 @@ module Fairmint.OpenCapTable.Types.Monetary where import qualified DA.Text as Text import Fairmint.Shared.TypeHelpers (validateOptionalText, validateRequiredText) -import Fairmint.OpenCapTable.Types.Validation (validateOcfCountryCode) +import Fairmint.OpenCapTable.Types.Validation (validUppercaseLetters, validateOcfCountryCode, validateOcfCountrySubdivisionCode) -- Type - Monetary -- Type representation of a money amount and currency @@ -29,7 +29,12 @@ validateOcfMonetary : OcfMonetary -> Bool validateOcfMonetary monetary = monetary.amount >= (0.0) && validateRequiredText monetary.currency && - Text.length monetary.currency == 3 + validateOcfCurrencyCode monetary.currency + +validateOcfCurrencyCode : Text -> Bool +validateOcfCurrencyCode currency = + Text.length currency == 3 && + all (`elem` validUppercaseLetters) (Text.explode currency) -- Type - Tax ID -- Type representation of a tax identifier and issuing country @@ -69,7 +74,7 @@ data OcfAddress = OcfAddress -- City name city: Optional Text - -- Country subdivision/state/province code or name + -- Country subdivision/state/province code country_subdivision: Optional Text -- Postal or ZIP code postal_code: Optional Text @@ -82,6 +87,6 @@ validateOcfAddress address = validateRequiredText address.country && validateOcfCountryCode address.country && validateOptionalText address.city && - validateOptionalText address.country_subdivision && + optional True validateOcfCountrySubdivisionCode address.country_subdivision && validateOptionalText address.postal_code && validateOptionalText address.street_suite diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml index 8dc55283..98ee9e65 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml @@ -7,6 +7,7 @@ module Fairmint.OpenCapTable.Types.Stock where -- Stock-related types and enums. -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/ +import DA.List (sortOn) import Fairmint.Shared.TypeHelpers (validateRequiredText) -- Enum - Stock Class Type @@ -81,6 +82,46 @@ data OcfShareNumberRange = OcfShareNumberRange validateOcfShareNumberRange : OcfShareNumberRange -> Bool validateOcfShareNumberRange r = r.starting_share_number > (0.0) && r.ending_share_number >= r.starting_share_number +shareNumberRangeQuantity : OcfShareNumberRange -> Decimal +shareNumberRangeQuantity r = r.ending_share_number - r.starting_share_number + 1.0 + +shareNumberRangesOverlap : OcfShareNumberRange -> OcfShareNumberRange -> Bool +shareNumberRangesOverlap a b = + a.starting_share_number <= b.ending_share_number && + b.starting_share_number <= a.ending_share_number + +shareNumberRangeIntersectionQuantity : OcfShareNumberRange -> OcfShareNumberRange -> Decimal +shareNumberRangeIntersectionQuantity a b = + let startingShareNumber = max a.starting_share_number b.starting_share_number + endingShareNumber = min a.ending_share_number b.ending_share_number + in if startingShareNumber <= endingShareNumber + then endingShareNumber - startingShareNumber + 1.0 + else 0.0 + +shareNumberRangeCoveredByRanges : [OcfShareNumberRange] -> OcfShareNumberRange -> Bool +shareNumberRangeCoveredByRanges coveringRanges candidate = + sum (map (shareNumberRangeIntersectionQuantity candidate) coveringRanges) == shareNumberRangeQuantity candidate + +shareNumberRangeSortKey : OcfShareNumberRange -> (Decimal, Decimal) +shareNumberRangeSortKey r = (r.starting_share_number, r.ending_share_number) + +hasOverlappingSortedShareNumberRanges : [OcfShareNumberRange] -> Bool +hasOverlappingSortedShareNumberRanges ranges = case ranges of + first :: second :: rest -> + shareNumberRangesOverlap first second || hasOverlappingSortedShareNumberRanges (second :: rest) + _ -> False + +hasOverlappingShareNumberRanges : [OcfShareNumberRange] -> Bool +hasOverlappingShareNumberRanges ranges = + hasOverlappingSortedShareNumberRanges (sortOn shareNumberRangeSortKey ranges) + +validateOcfShareNumberRangesForQuantity : Decimal -> [OcfShareNumberRange] -> Bool +validateOcfShareNumberRangesForQuantity quantity ranges = + null ranges || + (all validateOcfShareNumberRange ranges && + not (hasOverlappingShareNumberRanges ranges) && + sum (map shareNumberRangeQuantity ranges) == quantity) + -- Enum - Authorized Shares -- Special values for initial_shares_authorized -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/Issuer.schema.json diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml index 016dc20f..3a8eaa8b 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml @@ -13,6 +13,12 @@ import Fairmint.Shared.TypeHelpers (validateRequiredText) -- Shared Text array validator -- Ensures no element is an empty string; empty arrays are allowed +validUppercaseLetters : [Text] +validUppercaseLetters = + [ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M" + , "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" + ] + validateNonEmptyStringsArray : [Text] -> Bool validateNonEmptyStringsArray xs = all validateRequiredText xs @@ -29,6 +35,12 @@ hasDuplicateTextsWith seen remaining = case remaining of validateUniqueStringsArray : [Text] -> Bool validateUniqueStringsArray xs = not (hasDuplicateTexts xs) +validateNonEmptyUniqueStringsArray : [Text] -> Bool +validateNonEmptyUniqueStringsArray xs = + not (null xs) && + validateNonEmptyStringsArray xs && + validateUniqueStringsArray xs + -- Country code -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/CountryCode.schema.json validCountryCodes : [Text] @@ -64,6 +76,19 @@ validateOcfCountryCode : Text -> Bool validateOcfCountryCode countryCode = Text.length countryCode == 2 && countryCode `elem` validCountryCodes +-- Country subdivision code +-- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/CountrySubdivisionCode.schema.json +validCountrySubdivisionCodeCharacters : [Text] +validCountrySubdivisionCodeCharacters = + validUppercaseLetters ++ + [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ] + +validateOcfCountrySubdivisionCode : Text -> Bool +validateOcfCountrySubdivisionCode subdivisionCode = + Text.length subdivisionCode >= 1 && + Text.length subdivisionCode <= 3 && + all (`elem` validCountrySubdivisionCodeCharacters) (Text.explode subdivisionCode) + -- Percentage -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/Percentage.schema.json validateOcfPercentage : Decimal -> Bool diff --git a/Test/daml.yaml b/Test/daml.yaml index 0c90f894..6d66d15f 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.6.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.11.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestContact.daml b/Test/daml/OpenCapTable/TestContact.daml new file mode 100644 index 00000000..c029daf2 --- /dev/null +++ b/Test/daml/OpenCapTable/TestContact.daml @@ -0,0 +1,32 @@ +module OpenCapTable.TestContact where + +import Fairmint.OpenCapTable.Types.Contact (OcfPhone(..), OcfPhoneType(..), validateOcfPhone) +import Daml.Script + +assertPhoneValid : Text -> Script () +assertPhoneValid phoneNumber = + assertMsg (phoneNumber <> " should validate") (validateOcfPhone (phone phoneNumber)) + +assertPhoneInvalid : Text -> Script () +assertPhoneInvalid phoneNumber = + assertMsg (phoneNumber <> " should not validate") (not (validateOcfPhone (phone phoneNumber))) + +phone : Text -> OcfPhone +phone phoneNumber = OcfPhone with phone_type = OcfPhoneBusiness, phone_number = phoneNumber + +testPhoneNumberPattern = script do + assertPhoneValid "+1 555 123 0100" + assertPhoneValid "+123 12 34 5678" + assertPhoneValid "+1 555 123 0100 ext. 100" + assertPhoneValid "+1 555 123 0100 extension 100" + assertPhoneInvalid "+14155552671" + assertPhoneInvalid "+442071838750" + assertPhoneInvalid "1 555 123 0100" + assertPhoneInvalid "+1234 555 123 0100" + assertPhoneInvalid "+1 5 123 0100" + assertPhoneInvalid "+1 555 1 0100" + assertPhoneInvalid "+1 555 123 010" + assertPhoneInvalid "+1 555 0100" + assertPhoneInvalid "+1 555 123 0100 ext 100" + assertPhoneInvalid "+1 555 123 0100 ext. 10A" + assertPhoneInvalid "+1 555 123 0100" diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index a1ea8057..134be523 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -5,7 +5,7 @@ import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script import qualified Fairmint.OpenCapTable.CapTable as CT -import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) +import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..), validateOcfConvertibleConversionTrigger) import Fairmint.OpenCapTable.OCF.StockClass (StockClassOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.Types.Conversion @@ -76,6 +76,13 @@ validPpsMechanism = OcfSharePriceBasedConversionMechanism with discount_amount = None discount_percentage = Some 0.20 +validPpsMechanismWithoutDiscount : OcfSharePriceBasedConversionMechanism +validPpsMechanismWithoutDiscount = validPpsMechanism with + description = "No discount to share price" + discount = False + discount_amount = None + discount_percentage = None + invalidPpsMechanism : OcfSharePriceBasedConversionMechanism invalidPpsMechanism = validPpsMechanism with discount_amount = Some (usd 1.0) @@ -85,6 +92,26 @@ invalidPpsMechanismDiscountDisabled = validPpsMechanism with discount_amount = Some (usd 1.0) discount_percentage = Some 0.20 +validPpsMechanismDiscountDisabledWithAmount : OcfSharePriceBasedConversionMechanism +validPpsMechanismDiscountDisabledWithAmount = validPpsMechanism with + discount = False + discount_amount = Some (usd 1.0) + discount_percentage = None + +validPpsMechanismDiscountDisabledWithPercentage : OcfSharePriceBasedConversionMechanism +validPpsMechanismDiscountDisabledWithPercentage = validPpsMechanism with + discount = False + discount_amount = None + discount_percentage = Some 0.20 + +invalidPpsMechanismDiscountDisabledWithInvalidAmount : OcfSharePriceBasedConversionMechanism +invalidPpsMechanismDiscountDisabledWithInvalidAmount = validPpsMechanismDiscountDisabledWithAmount with + discount_amount = Some invalidCurrency + +invalidPpsMechanismDiscountDisabledWithInvalidPercentage : OcfSharePriceBasedConversionMechanism +invalidPpsMechanismDiscountDisabledWithInvalidPercentage = validPpsMechanismDiscountDisabledWithPercentage with + discount_percentage = Some 1.10 + validValuationMechanism : OcfValuationBasedConversionMechanism validValuationMechanism = OcfValuationBasedConversionMechanism with capitalization_definition = None @@ -92,9 +119,34 @@ validValuationMechanism = OcfValuationBasedConversionMechanism with valuation_amount = Some (usd 10_000_000.0) valuation_type = OcfValuationCap +validFixedValuationMechanism : OcfValuationBasedConversionMechanism +validFixedValuationMechanism = validValuationMechanism with + valuation_amount = Some (usd 5_000_000.0) + valuation_type = OcfValuationFixed + +validActualValuationMechanism : OcfValuationBasedConversionMechanism +validActualValuationMechanism = validValuationMechanism with + capitalization_definition = Some "fully-diluted" + valuation_amount = None + valuation_type = OcfValuationActual + +validActualValuationMechanismWithAmount : OcfValuationBasedConversionMechanism +validActualValuationMechanismWithAmount = validValuationMechanism with + valuation_amount = Some (usd 12_000_000.0) + valuation_type = OcfValuationActual + invalidValuationMechanism : OcfValuationBasedConversionMechanism invalidValuationMechanism = validValuationMechanism with valuation_amount = Some invalidCurrency +invalidValuationMechanismMissingAmount : OcfValuationBasedConversionMechanism +invalidValuationMechanismMissingAmount = validValuationMechanism with valuation_amount = None + +invalidActualValuationMechanismWithInvalidAmount : OcfValuationBasedConversionMechanism +invalidActualValuationMechanismWithInvalidAmount = validActualValuationMechanism with valuation_amount = Some invalidCurrency + +invalidActualValuationMechanismWithInvalidCapitalizationDefinition : OcfValuationBasedConversionMechanism +invalidActualValuationMechanismWithInvalidCapitalizationDefinition = validActualValuationMechanism with capitalization_definition = Some "" + validSafeMechanism : OcfSAFEConversionMechanism validSafeMechanism = OcfSAFEConversionMechanism with capitalization_definition = None @@ -141,9 +193,6 @@ validConvertibleMechanisms = , ("CUSTOM", OcfConvMechCustom validCustomMechanism) , ("PERCENT", OcfConvMechPercentCapitalization validPercentMechanism) , ("FIXED", OcfConvMechFixedAmount validFixedMechanism) - , ("PPS", OcfConvMechPpsBased validPpsMechanism) - , ("VALUATION", OcfConvMechValuationBased validValuationMechanism) - , ("RATIO", OcfConvMechRatio validRatio) ] invalidConvertibleMechanisms : [(Text, OcfConvertibleConversionMechanism)] @@ -153,9 +202,9 @@ invalidConvertibleMechanisms = , ("CUSTOM", OcfConvMechCustom invalidCustomMechanism) , ("PERCENT", OcfConvMechPercentCapitalization invalidPercentMechanism) , ("FIXED", OcfConvMechFixedAmount invalidFixedMechanism) - , ("PPS", OcfConvMechPpsBased invalidPpsMechanism) - , ("VALUATION", OcfConvMechValuationBased invalidValuationMechanism) - , ("RATIO", OcfConvMechRatio invalidRatio) + , ("PPS_UNSUPPORTED", OcfConvMechPpsBased validPpsMechanism) + , ("VALUATION_UNSUPPORTED", OcfConvMechValuationBased validValuationMechanism) + , ("RATIO_UNSUPPORTED", OcfConvMechRatio validRatio) ] validWarrantMechanisms : [(Text, OcfWarrantConversionMechanism)] @@ -170,12 +219,27 @@ validWarrantMechanisms = capitalization_definition = None capitalization_definition_rules = None valuation_amount = Some (usd 5_000_000.0) - valuation_type = "valuation_cap") + valuation_type = "CAP") + , ("VALUATION_ACTUAL", OcfWarrantMechanismValuationBased with + capitalization_definition = Some "fully-diluted" + capitalization_definition_rules = None + valuation_amount = None + valuation_type = "ACTUAL") , ("PPS", OcfWarrantMechanismPpsBased with description = "10% warrant discount" discount = True discount_amount = None discount_percentage = Some 0.10) + , ("PPS_DISABLED_AMOUNT", OcfWarrantMechanismPpsBased with + description = "Discount flag disabled with amount" + discount = False + discount_amount = Some (usd 1.0) + discount_percentage = None) + , ("PPS_DISABLED_PERCENTAGE", OcfWarrantMechanismPpsBased with + description = "Discount flag disabled with percentage" + discount = False + discount_amount = None + discount_percentage = Some 0.10) ] invalidWarrantMechanisms : [(Text, OcfWarrantConversionMechanism)] @@ -190,12 +254,37 @@ invalidWarrantMechanisms = capitalization_definition = None capitalization_definition_rules = None valuation_amount = None + valuation_type = "CAP") + , ("VALUATION_DOMAIN", OcfWarrantMechanismValuationBased with + capitalization_definition = None + capitalization_definition_rules = None + valuation_amount = Some (usd 1.0) valuation_type = "valuation_cap") + , ("VALUATION_ACTUAL_INVALID_AMOUNT", OcfWarrantMechanismValuationBased with + capitalization_definition = None + capitalization_definition_rules = None + valuation_amount = Some invalidCurrency + valuation_type = "ACTUAL") + , ("VALUATION_INVALID_CAPITALIZATION", OcfWarrantMechanismValuationBased with + capitalization_definition = Some "" + capitalization_definition_rules = None + valuation_amount = None + valuation_type = "ACTUAL") , ("PPS", OcfWarrantMechanismPpsBased with description = "Conflicting discounts" discount = False discount_amount = Some (usd 1.0) discount_percentage = Some 0.10) + , ("PPS_DISABLED_INVALID_AMOUNT", OcfWarrantMechanismPpsBased with + description = "Disabled discount with invalid amount" + discount = False + discount_amount = Some invalidCurrency + discount_percentage = None) + , ("PPS_DISABLED_INVALID_PERCENTAGE", OcfWarrantMechanismPpsBased with + description = "Disabled discount with invalid percentage" + discount = False + discount_amount = None + discount_percentage = Some 1.10) ] stockClassRightWith : @@ -231,28 +320,30 @@ stockClassRight : OcfConversionMechanism -> OcfStockClassConversionRight stockClassRight mechanism = stockClassRightWith mechanism None None None None None None None None +invalidRangeStockClassConversionTrigger : OcfConversionTrigger +invalidRangeStockClassConversionTrigger = ocfTriggerElectiveAtWill with + trigger_condition = Some "Ignored for stock-class conversion rights" + type_ = OcfTriggerTypeTypeElectiveInRange + validStockClassRights : [OcfStockClassConversionRight] validStockClassRights = - [ stockClassRightWith OcfConversionMechanismRatioConversion None None (Some validRatio) None None None None None - , stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 0.10) None None None None None None - , stockClassRightWith OcfConversionMechanismFixedAmountConversion (Some (usd 1.0)) None None None None None None None - , stockClassRightWith OcfConversionMechanismValuationBasedConversion None None None None (Some (usd 1.0)) None None None - , stockClassRightWith OcfConversionMechanismPpsBasedConversion None None None (Some (usd 1.0)) None None None None - , stockClassRightWith OcfConversionMechanismSAFEConversion None None None None None (Some 0.20) None None - , stockClassRightWith OcfConversionMechanismNoteConversion None None None None None None (Some (usd 10_000_000.0)) None - , stockClassRightWith OcfConversionMechanismCustomConversion None None None None None None None (Some "Custom stock class conversion") + [ stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None + , (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with converts_to_future_round = Some True + , (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with conversion_trigger = invalidRangeStockClassConversionTrigger ] invalidStockClassRights : [(Text, OcfStockClassConversionRight)] invalidStockClassRights = - [ ("RATIO", stockClassRight OcfConversionMechanismRatioConversion) - , ("PERCENT", stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 1.10) None None None None None None) - , ("FIXED", stockClassRight OcfConversionMechanismFixedAmountConversion) - , ("VALUATION", stockClassRight OcfConversionMechanismValuationBasedConversion) - , ("PPS", stockClassRight OcfConversionMechanismPpsBasedConversion) - , ("SAFE", stockClassRight OcfConversionMechanismSAFEConversion) - , ("NOTE", stockClassRight OcfConversionMechanismNoteConversion) - , ("CUSTOM", stockClassRightWith OcfConversionMechanismCustomConversion None None None None None None None (Some "")) + [ ("RATIO_MISSING_FIELDS", stockClassRight OcfConversionMechanismRatioConversion) + , ("RATIO_INVALID_PRICE", stockClassRightWith OcfConversionMechanismRatioConversion (Some invalidCurrency) None (Some validRatio) None None None None None) + , ("RATIO_EXPIRES_AT", (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None) with expires_at = Some testTime) + , ("PERCENT", stockClassRightWith OcfConversionMechanismPercentCapitalizationConversion None (Some 0.10) None None None None None None) + , ("FIXED", stockClassRightWith OcfConversionMechanismFixedAmountConversion (Some (usd 1.0)) None None None None None None None) + , ("VALUATION", stockClassRightWith OcfConversionMechanismValuationBasedConversion None None None None (Some (usd 1.0)) None None None) + , ("PPS", stockClassRightWith OcfConversionMechanismPpsBasedConversion None None None (Some (usd 1.0)) None None None None) + , ("SAFE", stockClassRightWith OcfConversionMechanismSAFEConversion None None None None None (Some 0.20) None None) + , ("NOTE", stockClassRightWith OcfConversionMechanismNoteConversion None None None None None None (Some (usd 10_000_000.0)) None) + , ("CUSTOM", stockClassRightWith OcfConversionMechanismCustomConversion None None None None None None None (Some "Custom stock class conversion")) ] convertibleRight : OcfConvertibleConversionMechanism -> OcfConvertibleConversionRight @@ -293,6 +384,88 @@ warrantTrigger (suffix, mechanism) = OcfConversionTrigger with trigger_id = "WARRANT-" <> suffix type_ = OcfTriggerTypeTypeElectiveAtWill +validConvertibleTriggers : [OcfConvertibleConversionTrigger] +validConvertibleTriggers = + [ convertibleTrigger ("AT-WILL", OcfConvMechCustom validCustomMechanism) + , (convertibleTrigger ("AUTO-DATE", OcfConvMechCustom validCustomMechanism)) with + trigger_date = Some testTime + type_ = OcfTriggerTypeTypeAutomaticOnDate + , (convertibleTrigger ("AUTO-CONDITION", OcfConvMechCustom validCustomMechanism)) with + trigger_condition = Some "Qualified financing closes" + type_ = OcfTriggerTypeTypeAutomaticOnCondition + , (convertibleTrigger ("ELECTIVE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + trigger_condition = Some "Holder elects after qualified financing" + type_ = OcfTriggerTypeTypeElectiveOnCondition + , (convertibleTrigger ("ELECTIVE-RANGE", OcfConvMechCustom validCustomMechanism)) with + start_date = Some testTime + end_date = Some testTime + type_ = OcfTriggerTypeTypeElectiveInRange + , (convertibleTrigger ("UNSPECIFIED", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeUnspecified + ] + +invalidConvertibleTriggers : [(Text, OcfConvertibleConversionTrigger)] +invalidConvertibleTriggers = + [ ("ELECTIVE_RANGE_WITH_CONDITION", (convertibleTrigger ("RANGE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + start_date = Some testTime + end_date = Some testTime + trigger_condition = Some "Not allowed on range triggers" + type_ = OcfTriggerTypeTypeElectiveInRange) + , ("AT_WILL_WITH_DATE", (convertibleTrigger ("AT-WILL-DATE", OcfConvMechCustom validCustomMechanism)) with + trigger_date = Some testTime) + , ("AUTOMATIC_ON_DATE_WITH_CONDITION", (convertibleTrigger ("AUTO-DATE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + trigger_condition = Some "Not allowed on date triggers" + trigger_date = Some testTime + type_ = OcfTriggerTypeTypeAutomaticOnDate) + , ("AUTOMATIC_ON_CONDITION_MISSING_CONDITION", (convertibleTrigger ("AUTO-CONDITION-MISSING", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeAutomaticOnCondition) + , ("AUTOMATIC_ON_DATE_MISSING_DATE", (convertibleTrigger ("AUTO-DATE-NO-DATE", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeAutomaticOnDate) + , ("ELECTIVE_RANGE_MISSING_DATES", (convertibleTrigger ("ELECTIVE-RANGE-NO-DATES", OcfConvMechCustom validCustomMechanism)) with + type_ = OcfTriggerTypeTypeElectiveInRange) + ] + +validWarrantTriggers : [OcfConversionTrigger] +validWarrantTriggers = + [ warrantTrigger ("AT-WILL", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant") + , (warrantTrigger ("AUTO-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_date = Some testTime + type_ = OcfTriggerTypeTypeAutomaticOnDate + , (warrantTrigger ("AUTO-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_condition = Some "Qualified financing closes" + type_ = OcfTriggerTypeTypeAutomaticOnCondition + , (warrantTrigger ("ELECTIVE-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_condition = Some "Holder elects after qualified financing" + type_ = OcfTriggerTypeTypeElectiveOnCondition + , (warrantTrigger ("ELECTIVE-RANGE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + start_date = Some testTime + end_date = Some testTime + type_ = OcfTriggerTypeTypeElectiveInRange + , (warrantTrigger ("UNSPECIFIED", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeUnspecified + ] + +invalidWarrantTriggers : [(Text, OcfConversionTrigger)] +invalidWarrantTriggers = + [ ("ELECTIVE_RANGE_WITH_CONDITION", (warrantTrigger ("RANGE-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + start_date = Some testTime + end_date = Some testTime + trigger_condition = Some "Not allowed on range triggers" + type_ = OcfTriggerTypeTypeElectiveInRange) + , ("AT_WILL_WITH_DATE", (warrantTrigger ("AT-WILL-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_date = Some testTime) + , ("AUTOMATIC_ON_DATE_WITH_CONDITION", (warrantTrigger ("AUTO-DATE-CONDITION", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_condition = Some "Not allowed on date triggers" + trigger_date = Some testTime + type_ = OcfTriggerTypeTypeAutomaticOnDate) + , ("AUTOMATIC_ON_CONDITION_MISSING_CONDITION", (warrantTrigger ("AUTO-CONDITION-MISSING", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeAutomaticOnCondition) + , ("AUTOMATIC_ON_DATE_MISSING_DATE", (warrantTrigger ("AUTO-DATE-NO-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeAutomaticOnDate) + , ("ELECTIVE_RANGE_MISSING_DATES", (warrantTrigger ("ELECTIVE-RANGE-NO-DATES", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + type_ = OcfTriggerTypeTypeElectiveInRange) + ] + assertValid : Text -> Bool -> Script () assertValid label result = assertMsg (label <> " should validate") result @@ -353,6 +526,38 @@ assertInvalidStockClassRights rights = assertInvalid ("stock class " <> label) (validateOcfStockClassConversionRight right) assertInvalidStockClassRights rest +assertValidConvertibleTriggers : [OcfConvertibleConversionTrigger] -> Script () +assertValidConvertibleTriggers triggers = + case triggers of + [] -> pure () + trigger :: rest -> do + assertValid "convertible conversion trigger" (validateOcfConvertibleConversionTrigger trigger) + assertValidConvertibleTriggers rest + +assertInvalidConvertibleTriggers : [(Text, OcfConvertibleConversionTrigger)] -> Script () +assertInvalidConvertibleTriggers triggers = + case triggers of + [] -> pure () + (label, trigger) :: rest -> do + assertInvalid ("convertible trigger " <> label) (validateOcfConvertibleConversionTrigger trigger) + assertInvalidConvertibleTriggers rest + +assertValidWarrantTriggers : [OcfConversionTrigger] -> Script () +assertValidWarrantTriggers triggers = + case triggers of + [] -> pure () + trigger :: rest -> do + assertValid "warrant conversion trigger" (validateOcfConversionTrigger trigger) + assertValidWarrantTriggers rest + +assertInvalidWarrantTriggers : [(Text, OcfConversionTrigger)] -> Script () +assertInvalidWarrantTriggers triggers = + case triggers of + [] -> pure () + (label, trigger) :: rest -> do + assertInvalid ("warrant trigger " <> label) (validateOcfConversionTrigger trigger) + assertInvalidWarrantTriggers rest + testConversionMechanismValidators_AllVariants = script do assertValid "ratio mechanism" (validateOcfRatioConversionMechanism validRatioMechanism) assertInvalid "ratio mechanism" (validateOcfRatioConversionMechanism invalidRatioMechanism) @@ -363,9 +568,16 @@ testConversionMechanismValidators_AllVariants = script do assertValid "percent capitalization mechanism" (validateOcfPercentCapitalizationConversionMechanism validPercentMechanism) assertInvalid "percent capitalization mechanism" (validateOcfPercentCapitalizationConversionMechanism invalidPercentMechanism) assertValid "pps mechanism" (validateOcfSharePriceBasedConversionMechanism validPpsMechanism) + assertValid "pps mechanism without discount" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismWithoutDiscount) assertInvalid "pps mechanism" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanism) assertValid "valuation mechanism" (validateOcfValuationBasedConversionMechanism validValuationMechanism) + assertValid "fixed valuation mechanism" (validateOcfValuationBasedConversionMechanism validFixedValuationMechanism) + assertValid "actual valuation mechanism" (validateOcfValuationBasedConversionMechanism validActualValuationMechanism) + assertValid "actual valuation mechanism with amount" (validateOcfValuationBasedConversionMechanism validActualValuationMechanismWithAmount) assertInvalid "valuation mechanism" (validateOcfValuationBasedConversionMechanism invalidValuationMechanism) + assertInvalid "valuation mechanism missing amount" (validateOcfValuationBasedConversionMechanism invalidValuationMechanismMissingAmount) + assertInvalid "actual valuation mechanism invalid amount" (validateOcfValuationBasedConversionMechanism invalidActualValuationMechanismWithInvalidAmount) + assertInvalid "actual valuation mechanism invalid capitalization" (validateOcfValuationBasedConversionMechanism invalidActualValuationMechanismWithInvalidCapitalizationDefinition) assertValid "SAFE mechanism" (validateOcfSAFEConversionMechanism validSafeMechanism) assertInvalid "SAFE mechanism" (validateOcfSAFEConversionMechanism invalidSafeMechanism) assertInvalid "SAFE mechanism invalid cap" (validateOcfSAFEConversionMechanism invalidSafeMechanismCap) @@ -374,17 +586,25 @@ testConversionMechanismValidators_AllVariants = script do assertInvalid "note mechanism" (validateOcfNoteConversionMechanism invalidNoteMechanism) assertInvalid "note mechanism invalid cap" (validateOcfNoteConversionMechanism invalidNoteMechanismCap) assertInvalid "pps mechanism disabled conflicting discounts" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabled) + assertValid "pps mechanism disabled with amount" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismDiscountDisabledWithAmount) + assertValid "pps mechanism disabled with percentage" (validateOcfSharePriceBasedConversionMechanism validPpsMechanismDiscountDisabledWithPercentage) + assertInvalid "pps mechanism disabled with invalid amount" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithInvalidAmount) + assertInvalid "pps mechanism disabled with invalid percentage" (validateOcfSharePriceBasedConversionMechanism invalidPpsMechanismDiscountDisabledWithInvalidPercentage) assertValidConvertibleMechanisms validConvertibleMechanisms assertInvalidConvertibleMechanisms invalidConvertibleMechanisms assertValidWarrantMechanisms validWarrantMechanisms assertInvalidWarrantMechanisms invalidWarrantMechanisms assertValidStockClassRights validStockClassRights assertInvalidStockClassRights invalidStockClassRights + assertValidConvertibleTriggers validConvertibleTriggers + assertInvalidConvertibleTriggers invalidConvertibleTriggers + assertValidWarrantTriggers validWarrantTriggers + assertInvalidWarrantTriggers invalidWarrantTriggers assertValid "any right convertible SAFE" (validateOcfAnyConversionRight (OcfRightConvertible (convertibleRight (OcfConvMechSAFE validSafeMechanism)))) assertInvalid "any right convertible CUSTOM" (validateOcfAnyConversionRight (OcfRightConvertible (convertibleRight (OcfConvMechCustom invalidCustomMechanism)))) assertValid "any right warrant CUSTOM" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")))) - assertInvalid "any right warrant VALUATION" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismValuationBased with capitalization_definition = None, capitalization_definition_rules = None, valuation_amount = None, valuation_type = "valuation_cap")))) - assertValid "any right stock class RATIO" (validateOcfAnyConversionRight (OcfRightStockClass (stockClassRightWith OcfConversionMechanismRatioConversion None None (Some validRatio) None None None None None))) + assertInvalid "any right warrant VALUATION" (validateOcfAnyConversionRight (OcfRightWarrant (warrantRight (OcfWarrantMechanismValuationBased with capitalization_definition = None, capitalization_definition_rules = None, valuation_amount = None, valuation_type = "CAP")))) + assertValid "any right stock class RATIO" (validateOcfAnyConversionRight (OcfRightStockClass (stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None))) assertInvalid "any right stock class RATIO" (validateOcfAnyConversionRight (OcfRightStockClass (stockClassRight OcfConversionMechanismRatioConversion))) testConversionMechanismRights_AllValidVariantsCreate = script do @@ -479,7 +699,7 @@ testConversionMechanismRights_InvalidVariantsReject = script do price_per_share = None liquidation_preference_multiple = None participation_cap_multiple = None - conversion_rights = [stockClassRightWith OcfConversionMechanismRatioConversion None None (Some validRatio) None None None None None] + conversion_rights = [stockClassRightWith OcfConversionMechanismRatioConversion (Some (usd 1.0)) None (Some validRatio) None None None None None] comments = []) ] edits = [] @@ -524,6 +744,26 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd convertibleControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with + id = "TX_UNSUPPORTED_CONVERTIBLE_CONVERSION_MECHANISM" + date = testTime + security_id = "CONV-UNSUPPORTED-MECHANISM" + custom_id = "CN-UNSUPPORTED" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + investment_amount = usd 100_000.0 + convertible_type = OcfConvertibleSecurity + conversion_triggers = [convertibleTrigger ("PPS-UNSUPPORTED", OcfConvMechPpsBased validPpsMechanism)] + pro_rata = None + seniority = 1 + comments = []] + edits = [] + deletes = [] submitMustFail issuer do exerciseCmd convertibleControl.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with @@ -544,6 +784,28 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd convertibleControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with + id = "TX_INVALID_CONVERTIBLE_TRIGGER_FIELDS" + date = testTime + security_id = "CONV-INVALID-TRIGGER-FIELDS" + custom_id = "CN-INVALID-TRIGGER" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + investment_amount = usd 100_000.0 + convertible_type = OcfConvertibleSecurity + conversion_triggers = [(convertibleTrigger ("RANGE-CONDITION", OcfConvMechCustom validCustomMechanism)) with + trigger_condition = Some "Not allowed on range triggers" + type_ = OcfTriggerTypeTypeElectiveInRange] + pro_rata = None + seniority = 1 + comments = []] + edits = [] + deletes = [] warrantControl <- submit issuer do exerciseCmd convertibleControl.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with @@ -567,6 +829,30 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd warrantControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "TX_INVALID_WARRANT_TRIGGER_FIELDS" + date = testTime + security_id = "WARRANT-INVALID-TRIGGER-FIELDS" + custom_id = "W-INVALID-TRIGGER" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + exercise_price = Some (usd 1.0) + purchase_price = usd 0.0 + exercise_triggers = [(warrantTrigger ("AT-WILL-DATE", OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant")) with + trigger_date = Some testTime] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = []] + edits = [] + deletes = [] submitMustFail issuer do exerciseCmd warrantControl.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with @@ -590,4 +876,58 @@ testConversionMechanismRights_InvalidVariantsReject = script do comments = []] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd warrantControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "TX_INVALID_WARRANT_PPS_DISABLED_DISCOUNT" + date = testTime + security_id = "WARRANT-PPS-DISABLED-DISCOUNT" + custom_id = "W-PPS-DISABLED" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + exercise_price = Some (usd 1.0) + purchase_price = usd 0.0 + exercise_triggers = [warrantTrigger ("PPS-DISABLED-DISCOUNT", OcfWarrantMechanismPpsBased with + description = "Discount flag disabled with conflicting discount fields" + discount = False + discount_amount = Some (usd 1.0) + discount_percentage = Some 0.10)] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = []] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd warrantControl.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "TX_INVALID_WARRANT_VALUATION_ACTUAL_AMOUNT" + date = testTime + security_id = "WARRANT-VALUATION-ACTUAL-INVALID" + custom_id = "W-VALUATION-ACTUAL" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + exercise_price = Some (usd 1.0) + purchase_price = usd 0.0 + exercise_triggers = [warrantTrigger ("VALUATION-ACTUAL-INVALID", OcfWarrantMechanismValuationBased with + capitalization_definition = None + capitalization_definition_rules = None + valuation_amount = Some invalidCurrency + valuation_type = "ACTUAL")] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = []] + edits = [] + deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml b/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml new file mode 100644 index 00000000..eec16b79 --- /dev/null +++ b/Test/daml/OpenCapTable/TestCountrySubdivisionCode.daml @@ -0,0 +1,101 @@ +module OpenCapTable.TestCountrySubdivisionCode where + +import Fairmint.OpenCapTable.OcpFactory +import Fairmint.OpenCapTable.OCF.Issuer +import Fairmint.OpenCapTable.IssuerAuthorization +import Fairmint.OpenCapTable.Types.Monetary (OcfAddress(..), OcfAddressType(..), validateOcfAddress) +import Fairmint.OpenCapTable.Types.Validation (validateOcfCountrySubdivisionCode) +import OpenCapTable.Setup +import Daml.Script +import DA.Date +import DA.Time + +assertSubdivisionValid : Text -> Script () +assertSubdivisionValid subdivision = + assertMsg (subdivision <> " should validate") (validateOcfCountrySubdivisionCode subdivision) + +assertSubdivisionInvalid : Text -> Script () +assertSubdivisionInvalid subdivision = + assertMsg (subdivision <> " should not validate") (not (validateOcfCountrySubdivisionCode subdivision)) + +ocfAddress : Optional Text -> OcfAddress +ocfAddress countrySubdivision = OcfAddress with + address_type = OcfAddressTypeLegal + country = "US" + city = Some "San Francisco" + country_subdivision = countrySubdivision + postal_code = Some "94105" + street_suite = Some "1 Market St" + +issuerData : Optional Text -> Optional OcfAddress -> IssuerOcfData +issuerData formationSubdivision addressOpt = IssuerOcfData with + id = "ISSUER_SUBDIVISION" + legal_name = "Subdivision Test Inc" + formation_date = time (DA.Date.date 2010 Jun 1) 12 0 0 + country_of_formation = "US" + dba = None + country_subdivision_of_formation = formationSubdivision + country_subdivision_name_of_formation = None + tax_ids = [] + email = None + phone = None + address = addressOpt + initial_shares_authorized = None + comments = [] + +createFactory : Party -> Script (ContractId OcpFactory) +createFactory systemOperator = + submit systemOperator do + createCmd OcpFactory with + system_operator = systemOperator + +testCountrySubdivisionCodePattern = script do + assertSubdivisionValid "CA" + assertSubdivisionValid "C1" + assertSubdivisionValid "1" + assertSubdivisionValid "ABC" + assertSubdivisionInvalid "" + assertSubdivisionInvalid "CALI" + assertSubdivisionInvalid "ca" + assertSubdivisionInvalid "C-" + assertSubdivisionInvalid "C A" + assertSubdivisionInvalid "California" + +testAddressCountrySubdivisionCodePattern = script do + assertMsg "address without subdivision should validate" (validateOcfAddress (ocfAddress None)) + assertMsg "uppercase subdivision should validate" (validateOcfAddress (ocfAddress (Some "CA"))) + assertMsg "lowercase subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "ca")))) + assertMsg "symbol subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "C-")))) + assertMsg "long subdivision should not validate" (not (validateOcfAddress (ocfAddress (Some "CALI")))) + assertMsg "subdivision name in code field should not validate" (not (validateOcfAddress (ocfAddress (Some "California")))) + +testIssuerCountrySubdivisionCodePatternRejectsInvalid = script do + TestOcp{system_operator, issuer} <- setupTestOcp + factoryCid <- createFactory system_operator + authCid <- submit system_operator do + exerciseCmd factoryCid AuthorizeIssuer with issuer = issuer + submitMustFail issuer do + exerciseCmd authCid CreateCapTable with + issuer_data = issuerData (Some "ca") None + pure () + +testIssuerAddressCountrySubdivisionCodePatternRejectsInvalid = script do + TestOcp{system_operator, issuer} <- setupTestOcp + factoryCid <- createFactory system_operator + authCid <- submit system_operator do + exerciseCmd factoryCid AuthorizeIssuer with issuer = issuer + submitMustFail issuer do + exerciseCmd authCid CreateCapTable with + issuer_data = issuerData None (Some (ocfAddress (Some "C-"))) + pure () + +testIssuerCountrySubdivisionNameAcceptsPlainName = script do + TestOcp{system_operator, issuer} <- setupTestOcp + factoryCid <- createFactory system_operator + authCid <- submit system_operator do + exerciseCmd factoryCid AuthorizeIssuer with issuer = issuer + _ <- submit issuer do + exerciseCmd authCid CreateCapTable with + issuer_data = (issuerData None None) with + country_subdivision_name_of_formation = Some "California" + pure () diff --git a/Test/daml/OpenCapTable/TestDocument.daml b/Test/daml/OpenCapTable/TestDocument.daml index 88a29844..68a5874e 100644 --- a/Test/daml/OpenCapTable/TestDocument.daml +++ b/Test/daml/OpenCapTable/TestDocument.daml @@ -68,6 +68,32 @@ testCreateDocumentWithOptionalsSome = script do deletes = [] pure () +testDocumentCreateWithPathAndUriFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument DocumentOcfData with + id = "DOC_PATH_AND_URI" + path = Some "/docs/acme/charter.pdf" + uri = Some "https://cdn.example.com/docs/acme/charter.pdf" + md5 = "0123456789abcdef0123456789abcdef" + related_objects = [] + comments = []] + edits = [] + deletes = [] + +testDocumentCreateWithNeitherPathNorUriFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument ((documentData "DOC_NO_PATH_OR_URI" []) with + path = None + uri = None)] + edits = [] + deletes = [] + testEditDocument = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add document via batch @@ -96,6 +122,38 @@ testEditDocument = script do deletes = [] pure () +testDocumentEditWithPathAndUriFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "DOC_EDIT_PATH_URI" [])] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditDocument ((documentData "DOC_EDIT_PATH_URI" []) with + path = Some "/docs/acme/charter.pdf" + uri = Some "https://cdn.example.com/docs/acme/charter.pdf")] + deletes = [] + +testDocumentEditWithNeitherPathNorUriFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "DOC_EDIT_NO_PATH_URI" [])] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditDocument ((documentData "DOC_EDIT_NO_PATH_URI" []) with + path = None + uri = None)] + deletes = [] + testDocumentRelatedIssuerReferenceMustExist = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml index acb53da3..64a8f179 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml @@ -48,6 +48,24 @@ testCreateAndArchiveEquityCompensationExerciseBuiltIn = script do deletes = [] pure () +testEquityCompensationExerciseAllowsEmptyResultingSecurityIds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-EMPTY-RESULT" "EC-EMPTY-RESULT" "SH-1" + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX_EC_EX_EMPTY_RESULT" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "EC-EMPTY-RESULT" + quantity = 10.0 + consideration_text = None + resulting_security_ids = [] + comments = []] + edits = [] + deletes = [] + pure () + -- Optionals Some testEquityCompensationExercise_OptionalsSome = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestIssuer.daml b/Test/daml/OpenCapTable/TestIssuer.daml index c087322f..ce411385 100644 --- a/Test/daml/OpenCapTable/TestIssuer.daml +++ b/Test/daml/OpenCapTable/TestIssuer.daml @@ -74,7 +74,7 @@ testCreateIssuerWithOptionalsSome = script do country_subdivision_name_of_formation = None tax_ids = [ OcfTaxID with tax_id = "12-3456789", country = "US" ] email = Some (OcfEmail with email_type = OcfEmailTypeBusiness, email_address = "info@acme.example") - phone = Some (OcfPhone with phone_type = OcfPhoneBusiness, phone_number = "+1 555 0100") + phone = Some (OcfPhone with phone_type = OcfPhoneBusiness, phone_number = "+1 555 123 0100") address = Some (OcfAddress with address_type = OcfAddressTypeLegal, street_suite = Some "1 Market St", city = Some "SF", country_subdivision = Some "CA", country = "US", postal_code = Some "94105") initial_shares_authorized = Some (OcfInitialSharesNumeric 2000000.0) comments = ["Seed stage"] diff --git a/Test/daml/OpenCapTable/TestMonetary.daml b/Test/daml/OpenCapTable/TestMonetary.daml new file mode 100644 index 00000000..c9064388 --- /dev/null +++ b/Test/daml/OpenCapTable/TestMonetary.daml @@ -0,0 +1,23 @@ +module OpenCapTable.TestMonetary where + +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..), validateOcfMonetary) +import Daml.Script + +assertMonetaryValid : Text -> OcfMonetary -> Script () +assertMonetaryValid label monetary = + assertMsg (label <> " should validate") (validateOcfMonetary monetary) + +assertMonetaryInvalid : Text -> OcfMonetary -> Script () +assertMonetaryInvalid label monetary = + assertMsg (label <> " should not validate") (not (validateOcfMonetary monetary)) + +monetary : Text -> OcfMonetary +monetary currency = OcfMonetary with amount = 1.0, currency = currency + +testMonetaryCurrencyCodePattern = script do + assertMonetaryValid "uppercase currency" (monetary "USD") + assertMonetaryInvalid "lowercase currency" (monetary "usd") + assertMonetaryInvalid "mixed-case currency" (monetary "UsD") + assertMonetaryInvalid "symbol currency" (monetary "U$D") + assertMonetaryInvalid "short currency" (monetary "US") + assertMonetaryInvalid "long currency" (monetary "USDD") diff --git a/Test/daml/OpenCapTable/TestStockClass.daml b/Test/daml/OpenCapTable/TestStockClass.daml index 677643ef..9356ab08 100644 --- a/Test/daml/OpenCapTable/TestStockClass.daml +++ b/Test/daml/OpenCapTable/TestStockClass.daml @@ -52,7 +52,7 @@ testCreateStockClass_OptionalsSome = script do let convRight = OcfStockClassConversionRight with ceiling_price_per_share = None conversion_mechanism = OcfConversionMechanismRatioConversion - conversion_price = None + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = ocfTriggerElectiveAtWill converts_to_future_round = None converts_to_stock_class_id = "SC_COMMON" @@ -91,8 +91,7 @@ testCreateStockClass_OptionalsSome = script do deletes = [] pure () --- Regression test for ENG-724: PPS_BASED_CONVERSION enum variant -testCreateStockClass_PpsBasedConversion = script do +testCreateStockClass_PpsBasedConversionFails = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp let convRight = OcfStockClassConversionRight with ceiling_price_per_share = None @@ -111,7 +110,7 @@ testCreateStockClass_PpsBasedConversion = script do reference_valuation_price_per_share = None type_ = "STOCK_CLASS_CONVERSION_RIGHT" valuation_cap = None - _ <- submit issuer do + submitMustFail issuer do exerciseCmd cap_table CT.UpdateCapTable with creates = [ CT.OcfCreateStockClass commonStockClassData @@ -134,7 +133,6 @@ testCreateStockClass_PpsBasedConversion = script do ] edits = [] deletes = [] - pure () testCreateStockClass_PpsBasedConversionFailsWithoutReferencePrice = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 94d5722b..5c54b1f0 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -97,7 +97,7 @@ stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with ceiling_price_per_share = None conversion_mechanism = OcfConversionMechanismRatioConversion - conversion_price = None + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = ocfTriggerElectiveAtWill converts_to_future_round = None converts_to_stock_class_id = stockClassId @@ -1226,7 +1226,7 @@ testSameDateHigherStockConversionRatioAdjustmentWinsForCeiling = script do edits = [] deletes = [] -testStockConversionWithNonRatioRightFailsForCeiling = script do +testStockClassWithNonRatioConversionRightRejected = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_NON_RATIO_COMMON" 100.0) @@ -1237,30 +1237,22 @@ testStockConversionWithNonRatioRightFailsForCeiling = script do 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 = - [ stockSuccessorIssuance "TX_CONVERSION_NON_RATIO_OUTPUT" "TX_CONVERSION_NON_RATIO_RESULT" stakeholderId commonClassId defaultTestDate 50.0 - , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NON_RATIO" "SEC_CONVERSION_NON_RATIO" 50.0) - ] + exerciseCmd capTableWithCommon CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass preferredClass] edits = [] deletes = [] -testStockConversionSkipsNonComputableRightWhenRatioRightMatchesResult = script do +testStockConversionSelectsMatchingRatioRight = 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 nonMatchingRight = (stockClassConversionRightTo commonClassId) with + ratio = Some (OcfRatio with numerator = 2.0, denominator = 1.0) let preferredClass = (limitedStockClass preferredClassId 100.0) with - conversion_rights = [ppsRight, stockClassConversionRightTo commonClassId] + conversion_rights = [nonMatchingRight, stockClassConversionRightTo commonClassId] (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MIXED" "Conversion Mixed Rights Holder" capTableIssued <- createStockIssuanceWith issuer capTableReady @@ -1485,7 +1477,7 @@ testConvertibleConversionValidatesConvertedUnitBounds = script do edits = [] deletes = [] -testRatioConvertibleConversionEnforcesCeiling = script do +testRatioConvertibleConversionMechanismRejected = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_RATIO" 100.0) @@ -1531,7 +1523,7 @@ testRatioConvertibleConversionEnforcesCeiling = script do boundaryIssuance.security_id (Some 50.0) ["SEC-CONVERTIBLE-RATIO-BOUNDARY-RESULT"] - _ <- submit issuer do + submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateConvertibleIssuance boundaryIssuance @@ -1546,7 +1538,6 @@ testRatioConvertibleConversionEnforcesCeiling = script do ] edits = [] deletes = [] - pure () testIndeterminateConvertibleConversionRequiresMaterializedResult = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml index b735d713..90a0056d 100644 --- a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml @@ -2,6 +2,7 @@ module OpenCapTable.TestStockClassConversionRightReferences where import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary import Fairmint.OpenCapTable.Types.Stock import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) @@ -13,7 +14,7 @@ stockClassConversionRightTo : Text -> OcfStockClassConversionRight stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with ceiling_price_per_share = None conversion_mechanism = OcfConversionMechanismRatioConversion - conversion_price = None + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = ocfTriggerElectiveAtWill converts_to_future_round = None converts_to_stock_class_id = stockClassId @@ -86,10 +87,35 @@ testStockClassConversionRightMissingTargetFails = script do edits = [] deletes = [] -testStockClassConversionRightNestedMissingTargetFails = script do +testStockClassConversionRightPlaceholderTriggerShapeIgnored = script do TestOcp{issuer, cap_table} <- setupTestOcp - submitMustFail issuer do + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass + ((preferredStockClassWithConversionRight "SC_PREFERRED_INVALID_NESTED_TRIGGER" "SC_COMMON_INVALID_NESTED_TRIGGER") with + conversion_rights = + [ (stockClassConversionRightTo "SC_COMMON_INVALID_NESTED_TRIGGER") with + conversion_trigger = + (ocfTriggerElectiveAtWill 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 = None + type_ = "") + ) + ]) + , CT.OcfCreateStockClass (defaultStockClass "SC_COMMON_INVALID_NESTED_TRIGGER") + ] + edits = [] + deletes = [] + pure () + +testStockClassConversionRightPlaceholderTriggerReferenceIgnored = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with creates = [ CT.OcfCreateStockClass (preferredStockClassWithNestedConversionRight "SC_PREFERRED_NESTED_MISSING" "SC_COMMON_OUTER_TARGET" (Some "SC_MISSING_NESTED_TARGET")) @@ -97,6 +123,7 @@ testStockClassConversionRightNestedMissingTargetFails = script do ] edits = [] deletes = [] + pure () testStockClassConversionRightEditMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockConsolidation.daml b/Test/daml/OpenCapTable/TestStockConsolidation.daml index 2f707d06..100842de 100644 --- a/Test/daml/OpenCapTable/TestStockConsolidation.daml +++ b/Test/daml/OpenCapTable/TestStockConsolidation.daml @@ -72,7 +72,7 @@ testStockConsolidation_DuplicateSecurityIdsRejected = script do security_ids = ["SSEC-DUP-CONSOLIDATE", "SSEC-DUP-CONSOLIDATE"] resulting_security_id = "SSEC-DUP-CONSOLIDATED" comments = [] - reason_text = Some "Duplicate source security IDs are invalid"] + reason_text = Some "Duplicate source security IDs are not schema-valid"] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockConversion.daml b/Test/daml/OpenCapTable/TestStockConversion.daml index 7ecb90d7..50ae361b 100644 --- a/Test/daml/OpenCapTable/TestStockConversion.daml +++ b/Test/daml/OpenCapTable/TestStockConversion.daml @@ -19,7 +19,7 @@ stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with ceiling_price_per_share = None conversion_mechanism = OcfConversionMechanismRatioConversion - conversion_price = None + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = ocfTriggerElectiveAtWill converts_to_future_round = None converts_to_stock_class_id = stockClassId @@ -34,8 +34,8 @@ stockClassConversionRightTo stockClassId = type_ = "STOCK_CLASS_CONVERSION_RIGHT" valuation_cap = None -addPreferredStockClass : Party -> ContractId CT.CapTable -> Script (ContractId CT.CapTable) -addPreferredStockClass issuer capTableCid = do +addPreferredStockClassWithRight : Party -> ContractId CT.CapTable -> OcfStockClassConversionRight -> Script (ContractId CT.CapTable) +addPreferredStockClassWithRight issuer capTableCid conversionRight = do result <- submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = @@ -43,12 +43,16 @@ addPreferredStockClass issuer capTableCid = do class_type = OcfStockClassTypePreferred default_id_prefix = "PS-" seniority = 2.0 - conversion_rights = [stockClassConversionRightTo "SC_COMMON"]) + conversion_rights = [conversionRight]) ] edits = [] deletes = [] pure result.updatedCapTableCid +addPreferredStockClass : Party -> ContractId CT.CapTable -> Script (ContractId CT.CapTable) +addPreferredStockClass issuer capTableCid = + addPreferredStockClassWithRight issuer capTableCid (stockClassConversionRightTo "SC_COMMON") + -- Create a stock issuance to convert (follows real workflow) createStockIssuance issuer capTableCid securityId quantity = submit issuer do @@ -102,6 +106,39 @@ testStockConversion_PreferredToCommon = script do deletes = [] pure () +-- StockClassConversionRight has no conversion_trigger property in OCF. The +-- Daml record still carries the field, so conversion calculations must ignore +-- whichever placeholder value an importer supplies. +testStockConversion_IgnoresNonOcfPlaceholderTrigger = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let placeholderTrigger = ocfTriggerElectiveAtWill with + type_ = OcfTriggerTypeTypeElectiveInRange + start_date = None + end_date = None + let conversionRight = (stockClassConversionRightTo "SC_COMMON") with + conversion_trigger = placeholderTrigger + capTableCid <- addPreferredStockClassWithRight issuer capTableCid conversionRight + let securityId = "SSEC-PREF-NON-OCF-TRIGGER" + result <- createStockIssuance issuer capTableCid securityId 100.0 + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion StockConversionOcfData with + id = "TX_STOCK_CONVERSION_NON_OCF_TRIGGER" + date = DT.time (DA.date 2025 Dec 15) 0 0 0 + security_id = securityId + quantity_converted = 100.0 + resulting_security_ids = ["SSEC-COMMON-NON-OCF-TRIGGER"] + comments = [] + balance_security_id = None + , stockSuccessorIssuance "TX_STOCK_CONVERSION_NON_OCF_TRIGGER_RESULT" "SSEC-COMMON-NON-OCF-TRIGGER" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Dec 15) 0 0 0) 100.0 + ] + edits = [] + deletes = [] + pure () + -- Test 2: Partial conversion with balance security testStockConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index 9a0aecea..0dd5e1cc 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -1,6 +1,11 @@ module OpenCapTable.TestStockIssuance where import Fairmint.OpenCapTable.OCF.StockIssuance +import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConsolidation (StockConsolidationOcfData(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers @@ -12,6 +17,11 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption(..), OcfShareNumberRange(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfVesting(..)) +shareRange : Decimal -> Decimal -> OcfShareNumberRange +shareRange start end = OcfShareNumberRange with + starting_share_number = start + ending_share_number = end + -- Test creating a basic stock issuance using helpers testCreateStockIssuance = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -81,3 +91,594 @@ testStockIssuance_OptionalsSome = script do _ <- createStockIssuanceWith issuer referenceResult.updatedCapTableCid issuance pure () + +testStockIssuance_InvalidCostBasisRejectedOnCreate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = (defaultStockIssuance "TX_STOCK_INVALID_COST_BASIS_CREATE" "SEC-STOCK-INVALID-COST-BASIS-CREATE" stakeholderId classId) with + cost_basis = Some (OcfMonetary with amount = 100.0, currency = "usd") + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + +testStockIssuance_InvalidCostBasisRejectedOnEdit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = (defaultStockIssuance "TX_STOCK_INVALID_COST_BASIS_EDIT" "SEC-STOCK-INVALID-COST-BASIS-EDIT" stakeholderId classId) with + cost_basis = Some (OcfMonetary with amount = 100.0, currency = "USD") + capTableIssued <- createStockIssuanceWith issuer capTableReady issuance + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (issuance with + cost_basis = Some (OcfMonetary with amount = -1.0, currency = "USD"))] + deletes = [] + +testStockIssuanceShareNumberRangesCanMatchQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let issuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_OK" "SEC-SHARE-NUMBERS-OK" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 5.0, shareRange 10.0 14.0] + + _ <- createStockIssuanceWith issuer cap_table' issuance + pure () + +testStockIssuanceShareNumberRangesMustMatchQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let issuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_SHORT" "SEC-SHARE-NUMBERS-SHORT" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 9.0] + + submitMustFail issuer do + exerciseCmd cap_table' CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesCannotOverlap = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let issuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_OVERLAP" "SEC-SHARE-NUMBERS-OVERLAP" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 5.0, shareRange 5.0 9.0] + + submitMustFail issuer do + exerciseCmd cap_table' CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesCannotOverlapAcrossIssuances = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_FIRST" "SEC-SHARE-NUMBERS-FIRST" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' firstIssuance + + let secondIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_SECOND" "SEC-SHARE-NUMBERS-SECOND" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 5.0 14.0] + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance secondIssuance] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesCanReuseRetractedRange = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_RETRACTED" "SEC-SHARE-NUMBERS-RETRACTED" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' firstIssuance + + let retraction = StockRetractionOcfData with + id = "TX_STOCK_SHARE_NUMBERS_RETRACT" + date = defaultTestDate + security_id = firstIssuance.security_id + reason_text = "Retracted stock issuance for replacement" + comments = [] + let replacementIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_REPLACEMENT" "SEC-SHARE-NUMBERS-REPLACEMENT" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockRetraction retraction + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesCanReuseFullyCancelledRange = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CANCELLED" "SEC-SHARE-NUMBERS-CANCELLED" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' firstIssuance + + let cancellation = StockCancellationOcfData with + id = "TX_STOCK_SHARE_NUMBERS_CANCEL" + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 10.0 + reason_text = "Cancelled before replacement" + security_id = firstIssuance.security_id + comments = [] + balance_security_id = None + let replacementIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_AFTER_CANCEL" "SEC-SHARE-NUMBERS-AFTER-CANCEL" stakeholderId classId) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesRequireBalanceForPartialCancellation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_PARTIAL_CANCEL_SOURCE" "SEC-SHARE-NUMBERS-PARTIAL-CANCEL-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX_STOCK_SHARE_NUMBERS_PARTIAL_CANCEL" + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 40.0 + reason_text = "Partial cancellation without a traceable remainder" + security_id = sourceIssuance.security_id + comments = [] + balance_security_id = None] + edits = [] + deletes = [] + +testStockIssuanceShareNumberRangesFollowPartialTransferBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_TRANSFER_SOURCE" "SEC-SHARE-NUMBERS-TRANSFER-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_TRANSFER_RECIPIENT" "SEC-SHARE-NUMBERS-TRANSFER-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let balanceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_TRANSFER_BALANCE" "SEC-SHARE-NUMBERS-TRANSFER-BALANCE" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 51.0 100.0] + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_PARTIAL_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = Some balanceIssuance.security_id + consideration_text = None + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesAllowMaterializedTransferResults = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_UNMATERIALIZED_SOURCE" "SEC-SHARE-NUMBERS-UNMATERIALIZED-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_UNMATERIALIZED_TRANSFER" + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 100.0 + security_id = sourceIssuance.security_id + resulting_security_ids = ["SEC-SHARE-NUMBERS-UNMATERIALIZED-RESULT"] + comments = [] + balance_security_id = None + consideration_text = None + let resultIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_UNMATERIALIZED_RESULT" "SEC-SHARE-NUMBERS-UNMATERIALIZED-RESULT" stakeholderId classId) with + date = transfer.date + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesRejectOutOfLineagePartialTransferBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_LINEAGE_SOURCE" "SEC-SHARE-NUMBERS-LINEAGE-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_LINEAGE_RECIPIENT" "SEC-SHARE-NUMBERS-LINEAGE-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let balanceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_LINEAGE_BALANCE" "SEC-SHARE-NUMBERS-LINEAGE-BALANCE" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1_001.0 1_050.0] + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_LINEAGE_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = Some balanceIssuance.security_id + consideration_text = None + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockIssuanceShareNumberRangesRejectOutOfLineagePartialTransferResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_RESULT_LINEAGE_SOURCE" "SEC-SHARE-NUMBERS-RESULT-LINEAGE-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_RESULT_LINEAGE_RECIPIENT" "SEC-SHARE-NUMBERS-RESULT-LINEAGE-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1_001.0 1_050.0] + let balanceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_RESULT_LINEAGE_BALANCE" "SEC-SHARE-NUMBERS-RESULT-LINEAGE-BALANCE" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 51.0 100.0] + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_RESULT_LINEAGE_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = Some balanceIssuance.security_id + consideration_text = None + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockIssuanceShareNumberRangesRejectUnnumberedPartialTransferBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_EMPTY_BALANCE_SOURCE" "SEC-SHARE-NUMBERS-EMPTY-BALANCE-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_EMPTY_BALANCE_RECIPIENT" "SEC-SHARE-NUMBERS-EMPTY-BALANCE-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let balanceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_EMPTY_BALANCE" "SEC-SHARE-NUMBERS-EMPTY-BALANCE" stakeholderId classId) with + date = transferDate + quantity = 50.0 + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_EMPTY_BALANCE_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = Some balanceIssuance.security_id + consideration_text = None + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesRejectUntrackedPartialTransferRemainder = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_UNTRACKED_SOURCE" "SEC-SHARE-NUMBERS-UNTRACKED-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let recipientIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_UNTRACKED_RECIPIENT" "SEC-SHARE-NUMBERS-UNTRACKED-RECIPIENT" stakeholderId classId) with + date = transferDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let transfer = StockTransferOcfData with + id = "TX_STOCK_SHARE_NUMBERS_UNTRACKED_TRANSFER" + date = transferDate + quantity = 50.0 + security_id = sourceIssuance.security_id + resulting_security_ids = [recipientIssuance.security_id] + comments = [] + balance_security_id = None + consideration_text = None + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance recipientIssuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesPreservedAcrossReissuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourceIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_REISSUANCE_SOURCE" "SEC-SHARE-NUMBERS-REISSUANCE-SOURCE" stakeholderId classId) with + quantity = 100.0 + share_numbers_issued = [shareRange 1.0 100.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' sourceIssuance + let reissuanceDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let reissuance = StockReissuanceOcfData with + id = "TX_STOCK_SHARE_NUMBERS_REISSUANCE" + date = reissuanceDate + security_id = sourceIssuance.security_id + resulting_security_ids = ["SEC-SHARE-NUMBERS-REISSUANCE-FIRST", "SEC-SHARE-NUMBERS-REISSUANCE-SECOND"] + comments = [] + reason_text = Some "Replacement certificates" + split_transaction_id = None + let firstResult = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_REISSUANCE_FIRST" "SEC-SHARE-NUMBERS-REISSUANCE-FIRST" stakeholderId classId) with + date = reissuanceDate + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let invalidSecondResult = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_REISSUANCE_SECOND" "SEC-SHARE-NUMBERS-REISSUANCE-SECOND" stakeholderId classId) with + date = reissuanceDate + quantity = 50.0 + share_numbers_issued = [shareRange 1_001.0 1_050.0] + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance reissuance + , CT.OcfCreateStockIssuance firstResult + , CT.OcfCreateStockIssuance invalidSecondResult + ] + edits = [] + deletes = [] + + let validSecondResult = invalidSecondResult with share_numbers_issued = [shareRange 51.0 100.0] + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance reissuance + , CT.OcfCreateStockIssuance firstResult + , CT.OcfCreateStockIssuance validSecondResult + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesPreservedAcrossConsolidation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let firstSource = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CONSOLIDATION_FIRST" "SEC-SHARE-NUMBERS-CONSOLIDATION-FIRST" stakeholderId classId) with + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let secondSource = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CONSOLIDATION_SECOND" "SEC-SHARE-NUMBERS-CONSOLIDATION-SECOND" stakeholderId classId) with + quantity = 50.0 + share_numbers_issued = [shareRange 51.0 100.0] + sources <- submit issuer do + exerciseCmd cap_table' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance firstSource + , CT.OcfCreateStockIssuance secondSource + ] + edits = [] + deletes = [] + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultSecurityId = "SEC-SHARE-NUMBERS-CONSOLIDATED" + let consolidation = StockConsolidationOcfData with + id = "TX_STOCK_SHARE_NUMBERS_CONSOLIDATION" + date = consolidationDate + security_ids = [firstSource.security_id, secondSource.security_id] + resulting_security_id = resultSecurityId + comments = [] + reason_text = Some "Combined certificates" + let resultIssuance ranges = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CONSOLIDATED" resultSecurityId stakeholderId classId) with + date = consolidationDate + quantity = 100.0 + share_numbers_issued = ranges + + submitMustFail issuer do + exerciseCmd sources.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation consolidation + , CT.OcfCreateStockIssuance (resultIssuance [shareRange 1_001.0 1_100.0]) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd sources.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation consolidation + , CT.OcfCreateStockIssuance (resultIssuance [shareRange 1.0 100.0]) + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesAllowMixedConsolidationAsUnnumbered = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let numberedSource = (defaultStockIssuance "TX_STOCK_MIXED_CONSOLIDATION_NUMBERED" "SEC-STOCK-MIXED-CONSOLIDATION-NUMBERED" stakeholderId classId) with + quantity = 50.0 + share_numbers_issued = [shareRange 1.0 50.0] + let unnumberedSource = (defaultStockIssuance "TX_STOCK_MIXED_CONSOLIDATION_UNNUMBERED" "SEC-STOCK-MIXED-CONSOLIDATION-UNNUMBERED" stakeholderId classId) with + quantity = 50.0 + share_numbers_issued = [] + sources <- submit issuer do + exerciseCmd cap_table' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance numberedSource + , CT.OcfCreateStockIssuance unnumberedSource + ] + edits = [] + deletes = [] + + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + resultSecurityId = "SEC-STOCK-MIXED-CONSOLIDATION-RESULT" + consolidation = StockConsolidationOcfData with + id = "TX_STOCK_MIXED_CONSOLIDATION" + date = consolidationDate + security_ids = [numberedSource.security_id, unnumberedSource.security_id] + resulting_security_id = resultSecurityId + comments = [] + reason_text = Some "Combine numbered and unnumbered certificates" + resultIssuance ranges = (defaultStockIssuance "TX_STOCK_MIXED_CONSOLIDATION-RESULT" resultSecurityId stakeholderId classId) with + date = consolidationDate + quantity = 100.0 + share_numbers_issued = ranges + + submitMustFail issuer do + exerciseCmd sources.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation consolidation + , CT.OcfCreateStockIssuance (resultIssuance [shareRange 1.0 100.0]) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd sources.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation consolidation + , CT.OcfCreateStockIssuance (resultIssuance []) + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesCannotOverlapBeforeRetraction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_HISTORICAL_FIRST" "SEC-SHARE-NUMBERS-HISTORICAL-FIRST" stakeholderId classId) with + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + cap_table'' <- createStockIssuanceWith issuer cap_table' firstIssuance + + let secondIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_HISTORICAL_SECOND" "SEC-SHARE-NUMBERS-HISTORICAL-SECOND" stakeholderId classId) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 10.0 + share_numbers_issued = [shareRange 5.0 14.0] + let laterRetraction = StockRetractionOcfData with + id = "TX_STOCK_SHARE_NUMBERS_HISTORICAL_RETRACT" + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + security_id = firstIssuance.security_id + reason_text = "Retracted after the overlapping issuance date" + comments = [] + + submitMustFail issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance secondIssuance + , CT.OcfCreateStockRetraction laterRetraction + ] + edits = [] + deletes = [] + pure () + +testStockIssuanceShareNumberRangesCanRepeatAcrossStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (cap_table', classId, stakeholderId) <- setupForStockIssuance issuer cap_table + (cap_table'', secondClassId) <- setupStockClassWithId issuer cap_table' "SC_SHARE_NUMBER_OTHER" + + let firstIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CLASS_ONE" "SEC-SHARE-NUMBERS-CLASS-ONE" stakeholderId classId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + let secondIssuance = (defaultStockIssuance "TX_STOCK_SHARE_NUMBERS_CLASS_TWO" "SEC-SHARE-NUMBERS-CLASS-TWO" stakeholderId secondClassId) with + quantity = 10.0 + share_numbers_issued = [shareRange 1.0 10.0] + + _ <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance firstIssuance + , CT.OcfCreateStockIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index e4f77872..cb5eccea 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -13,6 +13,7 @@ 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.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..)) import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import OpenCapTable.Setup @@ -87,7 +88,7 @@ stockClassConversionRightTo : Text -> OcfStockClassConversionRight stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with ceiling_price_per_share = None conversion_mechanism = OcfConversionMechanismRatioConversion - conversion_price = None + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = ocfTriggerElectiveAtWill converts_to_future_round = None converts_to_stock_class_id = stockClassId diff --git a/Test/daml/OpenCapTable/TestStockReissuance.daml b/Test/daml/OpenCapTable/TestStockReissuance.daml index 77a23781..84477672 100644 --- a/Test/daml/OpenCapTable/TestStockReissuance.daml +++ b/Test/daml/OpenCapTable/TestStockReissuance.daml @@ -107,3 +107,22 @@ testStockReissuance_MultipleResulting = script do edits = [] deletes = [] pure () + +testStockReissuance_RequiresResultingSecurityOnCapTable = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let securityId = "SSEC-EMPTY-REISSUANCE" + result <- createStockIssuance issuer capTableCid securityId 100.0 + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + id = "TX_STOCK_REISSUANCE_EMPTY_RESULT" + date = DT.time (DA.date 2025 Sep 01) 0 0 0 + security_id = securityId + resulting_security_ids = [] + comments = [] + reason_text = Some "Invalid reissuance without a successor" + split_transaction_id = None] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockTransfer.daml b/Test/daml/OpenCapTable/TestStockTransfer.daml index a5a7b7b2..c05e5a99 100644 --- a/Test/daml/OpenCapTable/TestStockTransfer.daml +++ b/Test/daml/OpenCapTable/TestStockTransfer.daml @@ -65,6 +65,26 @@ testStockTransfer_SplitTransfer = script do deletes = [] pure () +testStockTransfer_DuplicateResultingSecurityIdsRejected = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let securityId = "SSEC-DUP-TRANSFER-001" + result <- createStockIssuance issuer capTableCid securityId 100.0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX_STOCK_TRANSFER_DUP_RESULT" + date = DT.time (DA.date 2025 Aug 05) 0 0 0 + quantity = 50.0 + security_id = securityId + resulting_security_ids = ["SSEC-DUP-RESULT", "SSEC-DUP-RESULT"] + comments = [] + balance_security_id = Some "SSEC-DUP-BALANCE" + consideration_text = None] + edits = [] + deletes = [] + pure () + -- Test 2: Transfer with consideration text and comments (founder transfer) -- Based on prod: Early founder stock transfer with detailed consideration testStockTransfer_FounderTransferWithConsideration = script do diff --git a/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml new file mode 100644 index 00000000..466f0911 --- /dev/null +++ b/Test/daml/OpenCapTable/TestUniqueSecurityArrays.daml @@ -0,0 +1,211 @@ +module OpenCapTable.TestUniqueSecurityArrays where + +import Fairmint.OpenCapTable.OCF.ConvertibleConversion +import Fairmint.OpenCapTable.OCF.ConvertibleTransfer +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer +import Fairmint.OpenCapTable.OCF.StockConsolidation +import Fairmint.OpenCapTable.OCF.StockConversion +import Fairmint.OpenCapTable.OCF.StockReissuance +import Fairmint.OpenCapTable.OCF.StockTransfer +import Fairmint.OpenCapTable.OCF.WarrantExercise +import Fairmint.OpenCapTable.OCF.WarrantTransfer +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import OpenCapTable.TestHelpers (defaultTestDate) +import Daml.Script + +duplicateResultingSecurityIds : [Text] +duplicateResultingSecurityIds = ["SEC-DUP", "SEC-DUP"] + +assertInvalid : Text -> Bool -> Script () +assertInvalid label validationResult = + assertMsg (label <> " should reject invalid security IDs") (not validationResult) + +testUniqueSecurityArrays_TransferValidatorsRejectDuplicateResultingSecurityIds = script do + assertInvalid "stock transfer" $ + validateStockTransferOcfData StockTransferOcfData with + id = "TX_STOCK_TRANSFER_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "SSEC-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + consideration_text = None + + assertInvalid "convertible transfer" $ + validateConvertibleTransferOcfData ConvertibleTransferOcfData with + id = "TX_CONVERTIBLE_TRANSFER_DUP" + amount = OcfMonetary with amount = 1.0, currency = "USD" + date = defaultTestDate + security_id = "CSEC-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + consideration_text = None + + assertInvalid "warrant transfer" $ + validateWarrantTransferOcfData WarrantTransferOcfData with + id = "TX_WARRANT_TRANSFER_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "WSEC-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + consideration_text = None + + assertInvalid "equity compensation transfer" $ + validateEquityCompensationTransferOcfData EquityCompensationTransferOcfData with + id = "TX_EC_TRANSFER_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "ESEC-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + consideration_text = None + + assertInvalid "stock consolidation" $ + validateStockConsolidationOcfData StockConsolidationOcfData with + id = "TX_STOCK_CONSOLIDATION_DUP" + date = defaultTestDate + resulting_security_id = "SSEC-CONSOLIDATED" + comments = [] + security_ids = ["SSEC-1", "SSEC-1"] + reason_text = None + +testUniqueSecurityArrays_NonTransferValidatorsRejectDuplicateResultingSecurityIds = script do + assertInvalid "warrant exercise" $ + validateWarrantExerciseOcfData WarrantExerciseOcfData with + id = "TX_WARRANT_EXERCISE_DUP" + date = defaultTestDate + security_id = "WSEC-EXERCISE" + trigger_id = "TRIGGER-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + quantity = Some 1.0 + + assertInvalid "equity compensation exercise" $ + validateOcfEquityCompensationExerciseData EquityCompensationExerciseOcfData with + id = "TX_EC_EXERCISE_DUP" + date = defaultTestDate + quantity = 1.0 + security_id = "ESEC-EXERCISE" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + + assertInvalid "equity compensation release" $ + validateEquityCompensationReleaseOcfData EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_DUP" + date = defaultTestDate + quantity = 1.0 + release_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = "ESEC-RELEASE" + settlement_date = defaultTestDate + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + consideration_text = None + + assertInvalid "stock reissuance" $ + validateStockReissuanceOcfData StockReissuanceOcfData with + id = "TX_STOCK_REISSUANCE_DUP" + date = defaultTestDate + security_id = "SSEC-REISSUANCE" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + reason_text = None + split_transaction_id = None + + assertInvalid "stock conversion" $ + validateStockConversionOcfData StockConversionOcfData with + id = "TX_STOCK_CONVERSION_DUP" + date = defaultTestDate + quantity_converted = 1.0 + security_id = "SSEC-CONVERSION" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + + assertInvalid "convertible conversion" $ + validateConvertibleConversionOcfData ConvertibleConversionOcfData with + id = "TX_CONVERTIBLE_CONVERSION_DUP" + date = defaultTestDate + reason_text = "Test duplicate successor IDs" + security_id = "CSEC-CONVERSION" + trigger_id = "TRIGGER-1" + comments = [] + resulting_security_ids = duplicateResultingSecurityIds + balance_security_id = None + capitalization_definition = None + quantity_converted = None + +testUniqueSecurityArrays_NonCardinalityResultArraysAllowEmpty = script do + assertMsg "warrant exercise empty result array should be valid" $ + validateWarrantExerciseOcfData WarrantExerciseOcfData with + id = "TX_WARRANT_EXERCISE_EMPTY" + date = defaultTestDate + security_id = "WSEC-EXERCISE" + trigger_id = "TRIGGER-1" + comments = [] + resulting_security_ids = [] + consideration_text = None + quantity = Some 1.0 + + assertMsg "equity compensation exercise empty result array should be valid" $ + validateOcfEquityCompensationExerciseData EquityCompensationExerciseOcfData with + id = "TX_EC_EXERCISE_EMPTY" + date = defaultTestDate + quantity = 1.0 + security_id = "ESEC-EXERCISE" + comments = [] + resulting_security_ids = [] + consideration_text = None + + assertMsg "equity compensation release empty result array should be valid" $ + validateEquityCompensationReleaseOcfData EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_EMPTY" + date = defaultTestDate + quantity = 1.0 + release_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = "ESEC-RELEASE" + settlement_date = defaultTestDate + comments = [] + resulting_security_ids = [] + consideration_text = None + + assertMsg "stock reissuance empty result array should be valid" $ + validateStockReissuanceOcfData StockReissuanceOcfData with + id = "TX_STOCK_REISSUANCE_EMPTY" + date = defaultTestDate + security_id = "SSEC-REISSUANCE" + comments = [] + resulting_security_ids = [] + reason_text = None + split_transaction_id = None + + assertMsg "stock conversion empty result array should be valid" $ + validateStockConversionOcfData StockConversionOcfData with + id = "TX_STOCK_CONVERSION_EMPTY" + date = defaultTestDate + quantity_converted = 1.0 + security_id = "SSEC-CONVERSION" + comments = [] + resulting_security_ids = [] + balance_security_id = None + + assertMsg "convertible conversion empty result array should be valid" $ + validateConvertibleConversionOcfData ConvertibleConversionOcfData with + id = "TX_CONVERTIBLE_CONVERSION_EMPTY" + date = defaultTestDate + reason_text = "Test schema-valid empty successor array" + security_id = "CSEC-CONVERSION" + trigger_id = "TRIGGER-1" + comments = [] + resulting_security_ids = [] + balance_security_id = None + capitalization_definition = None + quantity_converted = None diff --git a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml index 0c7fcd5c..5ba53d7c 100644 --- a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml @@ -3,6 +3,7 @@ module OpenCapTable.TestWarrantConversionRightReferences where import Fairmint.OpenCapTable.OCF.WarrantIssuance import Fairmint.OpenCapTable.Types.Conversion import Fairmint.OpenCapTable.Types.Monetary +import Fairmint.OpenCapTable.Types.Stock import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers @@ -44,17 +45,17 @@ stockClassConversionTriggerTo : Text -> OcfConversionTrigger stockClassConversionTriggerTo stockClassId = OcfConversionTrigger with conversion_right = OcfRightStockClass (OcfStockClassConversionRight with ceiling_price_per_share = None - conversion_mechanism = OcfConversionMechanismCustomConversion - conversion_price = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = warrantConversionTriggerTo None converts_to_future_round = None converts_to_stock_class_id = stockClassId - custom_description = Some "Custom" + custom_description = None discount_rate = None expires_at = None floor_price_per_share = None percent_of_capitalization = None - ratio = 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" @@ -72,17 +73,17 @@ stockClassConversionTriggerWithNestedWarrantTarget : Text -> Optional Text -> Oc stockClassConversionTriggerWithNestedWarrantTarget stockClassId nestedStockClassId = OcfConversionTrigger with conversion_right = OcfRightStockClass (OcfStockClassConversionRight with ceiling_price_per_share = None - conversion_mechanism = OcfConversionMechanismCustomConversion - conversion_price = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = Some (OcfMonetary with amount = 1.0, currency = "USD") conversion_trigger = warrantConversionTriggerTo nestedStockClassId converts_to_future_round = None converts_to_stock_class_id = stockClassId - custom_description = Some "Custom" + custom_description = None discount_rate = None expires_at = None floor_price_per_share = None percent_of_capitalization = None - ratio = 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" @@ -215,11 +216,11 @@ testWarrantStockClassConversionRightMissingTargetFails = script do edits = [] deletes = [] -testWarrantStockClassConversionRightNestedMissingTargetFails = script do +testWarrantStockClassConversionRightPlaceholderTriggerReferenceIgnored = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableWithStakeholder <- addDefaultStakeholder issuer cap_table - submitMustFail issuer do + _ <- submit issuer do exerciseCmd capTableWithStakeholder CT.UpdateCapTable with creates = [ CT.OcfCreateStockClass (defaultStockClass "SC_WARRANT_OUTER_TARGET") @@ -231,6 +232,7 @@ testWarrantStockClassConversionRightNestedMissingTargetFails = script do ] edits = [] deletes = [] + pure () testWarrantConversionRightEditMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestWarrantIssuance.daml b/Test/daml/OpenCapTable/TestWarrantIssuance.daml index 19d9a8a3..ffd74da1 100644 --- a/Test/daml/OpenCapTable/TestWarrantIssuance.daml +++ b/Test/daml/OpenCapTable/TestWarrantIssuance.daml @@ -12,28 +12,31 @@ import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +defaultWarrantIssuance : Text -> Text -> WarrantIssuanceOcfData +defaultWarrantIssuance issuanceId securityId = WarrantIssuanceOcfData with + id = issuanceId + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = securityId + custom_id = "W-" <> issuanceId + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityUnspecified + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + purchase_price = OcfMonetary with amount = 0.0, currency = "USD" + exercise_triggers = [ocfTriggerElectiveAtWill] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = [] + createWarrantIssuance issuer capTableCid = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_ISSUANCE_1" - date = DT.time (DA.date 2024 Jan 01) 0 0 0 - security_id = "WSEC-1" - custom_id = "W-1" - stakeholder_id = "SH-1" - board_approval_date = None - stockholder_approval_date = None - consideration_text = None - security_law_exemptions = [] - quantity = Some 100.0 - quantity_source = Some OcfQuantityUnspecified - exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") - purchase_price = OcfMonetary with amount = 0.0, currency = "USD" - exercise_triggers = [ ocfTriggerElectiveAtWill ] - warrant_expiration_date = None - vesting_terms_id = None - vestings = [] - comments = []] + creates = [CT.OcfCreateWarrantIssuance (defaultWarrantIssuance "TX_WARRANT_ISSUANCE_1" "WSEC-1")] edits = [] deletes = [] @@ -49,6 +52,35 @@ testCreateAndArchiveWarrantIssuanceBuiltIn = script do _ <- createWarrantIssuance issuer capTableCid pure () +testWarrantIssuance_InvalidExercisePriceRejectedOnCreate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let issuance = (defaultWarrantIssuance "TX_WARRANT_INVALID_EXERCISE_PRICE_CREATE" "WSEC-INVALID-EXERCISE-PRICE-CREATE") with + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "usd") + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + +testWarrantIssuance_InvalidExercisePriceRejectedOnEdit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let issuance = defaultWarrantIssuance "TX_WARRANT_INVALID_EXERCISE_PRICE_EDIT" "WSEC-INVALID-EXERCISE-PRICE-EDIT" + created <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd created.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditWarrantIssuance (issuance with + exercise_price = Some (OcfMonetary with amount = -1.0, currency = "USD"))] + deletes = [] + -- Base helper already covers None/empty where applicable -- Optionals Some and arrays non-empty diff --git a/dars/OpenCapTable-v34/0.0.10/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.10/OpenCapTable-v34.dar new file mode 100644 index 00000000..88628765 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.10/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54f7388d9b161a5396acdc0200d6ce80c2f52c8d17e7817e9653f4bc59aa3600 +size 2642264 diff --git a/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar new file mode 100644 index 00000000..29b804d6 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23fc9940acf68156ce2594da14b0783ce38c9195d9aaf15e113fb5124f05d8ee +size 3114372 diff --git a/dars/OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar new file mode 100644 index 00000000..b7bb9e2b --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64b5f71d1108bd5924fe86e0bd62f9bac301a476d4ee53ae7743b103224c44dd +size 2628793 diff --git a/dars/OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar new file mode 100644 index 00000000..6a599098 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67553898061019b37717fc99a3d8d712c2392738e426e1b640d577896c4c1321 +size 2630155 diff --git a/dars/OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar new file mode 100644 index 00000000..9c4b23ad --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b84a021f8862ad8d3f40b5c44d6769f145dd94163226ce43bed5beed23427fdf +size 2633877 diff --git a/dars/dars.lock b/dars/dars.lock index 8351cbed..5e6343c1 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -11,6 +11,20 @@ "mainnet" ] }, + "OpenCapTable-v34/0.0.10/OpenCapTable-v34.dar": { + "sha256": "54f7388d9b161a5396acdc0200d6ce80c2f52c8d17e7817e9653f4bc59aa3600", + "size": 2642264, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T13:01:59.033Z", + "networks": [] + }, + "OpenCapTable-v34/0.0.11/OpenCapTable-v34.dar": { + "sha256": "23fc9940acf68156ce2594da14b0783ce38c9195d9aaf15e113fb5124f05d8ee", + "size": 3114372, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-11T14:46:26Z", + "networks": [] + }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { "sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6", "size": 2446238, @@ -45,6 +59,27 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T03:07:54Z", "networks": [] + }, + "OpenCapTable-v34/0.0.7/OpenCapTable-v34.dar": { + "sha256": "64b5f71d1108bd5924fe86e0bd62f9bac301a476d4ee53ae7743b103224c44dd", + "size": 2628793, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T11:46:17.671Z", + "networks": [] + }, + "OpenCapTable-v34/0.0.8/OpenCapTable-v34.dar": { + "sha256": "67553898061019b37717fc99a3d8d712c2392738e426e1b640d577896c4c1321", + "size": 2630155, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T11:59:30.767Z", + "networks": [] + }, + "OpenCapTable-v34/0.0.9/OpenCapTable-v34.dar": { + "sha256": "b84a021f8862ad8d3f40b5c44d6769f145dd94163226ce43bed5beed23427fdf", + "size": 2633877, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T12:11:45.359Z", + "networks": [] } } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 5476881f..95ae0167 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,8 +18,8 @@ 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(..), OcfConversionTriggerType(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified Fairmint.OpenCapTable.OCF.ConvertibleIssuance as ConvertibleIssuance (OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) @@ -439,20 +439,6 @@ stockClassConversionOutputQuantity maps sourceStockClass conversionRight convers 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 -> @@ -463,24 +449,23 @@ stockConversionResultTerms maps stockClassIndex sourceIssuanceData conversionDat 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) + (\resultTerms conversionRight -> do + -- OCF StockClassConversionRight defines no trigger or expiration; + -- every schema-valid right is eligible for quantity calculation. + 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) [] sourceStockClass.conversion_rights assertMsg - ("Stock conversion has no computable date-eligible conversion right: " <> conversionData.id) + ("Stock conversion has no computable conversion right: " <> conversionData.id) (not (null terms)) pure terms None -> do @@ -504,7 +489,7 @@ stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversion [] -> pure None _ -> do assertMsg - ("Stock conversion output cannot be reconstructed without explicit results because multiple conversion rights are date-eligible: " <> conversionData.id) + ("Stock conversion output cannot be reconstructed without explicit results because multiple conversion rights are computable: " <> conversionData.id) False pure None @@ -984,7 +969,7 @@ validateStockConversionResultIssuances maps conversionData expectedTerms = do 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) + ("Stock conversion results do not match any conversion right for " <> conversionData.id <> ": found class " <> actualStockClassId <> " and quantity " <> show actualQuantity) (any (\expectedTerm -> expectedTerm.stock_class_id == actualStockClassId && @@ -1428,6 +1413,13 @@ stockClassShareEventSortKey event = case event of StockClassShareMultiplier with event_date; event_priority -> (event_date, event_priority, 2) StockClassShareRetraction with event_date; event_priority -> (event_date, event_priority, 3) +stockClassShareEventDate : StockClassShareEvent -> Time +stockClassShareEventDate event = case event of + StockClassShareDelta with event_date -> event_date + StockClassShareTerminalDelta with event_date -> event_date + StockClassShareMultiplier with event_date -> event_date + StockClassShareRetraction with event_date -> event_date + stockClassShareEventOnOrAfter : Time -> StockClassShareEvent -> Bool stockClassShareEventOnOrAfter earliest event = case event of StockClassShareDelta with event_date -> event_date >= earliest @@ -1947,12 +1939,133 @@ stockSecurityQuantityBeforeTerminal : stockSecurityQuantityBeforeTerminal eventIndex maps issuanceData eventDate = stockSecurityQuantityBeforeEvent eventIndex maps issuanceData (eventDate, 2, 1) +validateStockSuccessorShareNumberRanges : + Text -> + StockIssuanceOcfData -> + StockIssuanceOcfData -> + Update () +validateStockSuccessorShareNumberRanges label sourceIssuanceData successorIssuanceData = + case sourceIssuanceData.share_numbers_issued of + [] -> pure () + sourceShareNumberRanges -> + assertMsg + (label <> " share numbers are outside the source issuance range for " <> successorIssuanceData.security_id) + (not (null successorIssuanceData.share_numbers_issued) && + all + (shareNumberRangeCoveredByRanges sourceShareNumberRanges) + successorIssuanceData.share_numbers_issued) + +validateStockResultShareNumberRanges : + CapTableMaps -> + Text -> + StockIssuanceOcfData -> + [Text] -> + Update () +validateStockResultShareNumberRanges maps label sourceIssuanceData resultSecurityIds = + mapA_ + (\resultSecurityId -> + case Map.lookup resultSecurityId maps.stock_issuances_by_security_id of + Some resultIssuanceCid -> do + resultIssuance <- fetch resultIssuanceCid + validateStockSuccessorShareNumberRanges label sourceIssuanceData resultIssuance.issuance_data + -- OCF lifecycle transactions may be imported without materializing + -- their resulting StockIssuance objects. Partial materialization is + -- rejected by the existing result-coverage validation. + None -> pure ()) + resultSecurityIds + +validateStockConsolidationShareNumberRanges : + Text -> + [StockIssuanceOcfData] -> + StockIssuanceOcfData -> + Update () +validateStockConsolidationShareNumberRanges label sourceIssuanceData resultIssuanceData = + let sourceShareNumberRanges = concatMap (\source -> source.share_numbers_issued) sourceIssuanceData + anyNumberedSource = any (\source -> not (null source.share_numbers_issued)) sourceIssuanceData + allSourcesNumbered = all (\source -> not (null source.share_numbers_issued)) sourceIssuanceData + in if not anyNumberedSource + then pure () + else if allSourcesNumbered + then + assertMsg + (label <> " share numbers are outside the consolidated source ranges for " <> resultIssuanceData.security_id) + (not (null resultIssuanceData.share_numbers_issued) && + all + (shareNumberRangeCoveredByRanges sourceShareNumberRanges) + resultIssuanceData.share_numbers_issued) + else + assertMsg + (label <> " cannot assign share numbers because not every consolidated source has share number ranges") + (null resultIssuanceData.share_numbers_issued) + stockReductionSortKey : Time -> Optional Text -> (Time, Int, Int) stockReductionSortKey eventDate balanceSecurityId = case balanceSecurityId of Some _ -> (eventDate, 2, 1) None -> (eventDate, 1, 0) +validateNumberedStockPartialReduction : + StockClassShareEventIndex -> + CapTableMaps -> + Text -> + Text -> + Time -> + Decimal -> + Optional Text -> + Update () +validateNumberedStockPartialReduction eventIndex maps label sourceSecurityId eventDate reductionQuantity balanceSecurityId = + case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + when (not (null sourceIssuance.issuance_data.share_numbers_issued) && isNone balanceSecurityId) do + currentQuantity <- stockSecurityQuantityBeforeEvent + eventIndex + maps + sourceIssuance.issuance_data + (stockReductionSortKey eventDate balanceSecurityId) + assertMsg + (label <> " partially reduces a numbered stock issuance without a balance security") + (reductionQuantity >= currentQuantity) + None -> pure () + +validateNumberedStockPartialReductions : StockClassShareEventIndex -> CapTableMaps -> Update () +validateNumberedStockPartialReductions eventIndex maps = do + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateNumberedStockPartialReduction + eventIndex maps ("Stock cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id cancellation.cancellation_data.date cancellation.cancellation_data.quantity + cancellation.cancellation_data.balance_security_id) + (Map.values maps.stock_cancellations) + + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + validateNumberedStockPartialReduction + eventIndex maps ("Stock repurchase " <> repurchase.repurchase_data.id) + repurchase.repurchase_data.security_id repurchase.repurchase_data.date repurchase.repurchase_data.quantity + repurchase.repurchase_data.balance_security_id) + (Map.values maps.stock_repurchases) + + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateNumberedStockPartialReduction + eventIndex maps ("Stock conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id conversion.conversion_data.date conversion.conversion_data.quantity_converted + conversion.conversion_data.balance_security_id) + (Map.values maps.stock_conversions) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateNumberedStockPartialReduction + eventIndex maps ("Stock transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id) + (Map.values maps.stock_transfers) + stockClassQuantityAtSortKey : CapTableMaps -> Text -> @@ -2080,6 +2193,7 @@ validateStockBalanceSuccessor maps label sourceIssuanceData eventDate expectedQu assertMsg (label <> " balance quantity mismatch for " <> balanceSecurityId <> ": expected " <> show expectedQuantity <> ", found " <> show balanceIssuance.issuance_data.quantity) (balanceIssuance.issuance_data.quantity == expectedQuantity) + validateStockSuccessorShareNumberRanges label sourceIssuanceData balanceIssuance.issuance_data None -> assertMsg (label <> " balance stock issuance not found: " <> balanceSecurityId) False @@ -2153,16 +2267,20 @@ validateStockBalanceSuccessors eventIndex maps = do 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 ()) + case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let label = "Stock transfer " <> transfer.transfer_data.id + validateStockResultShareNumberRanges + maps label sourceIssuance.issuance_data transfer.transfer_data.resulting_security_ids + case transfer.transfer_data.balance_security_id of + Some _ -> + validateStockTerminalBalanceSuccessor + eventIndex maps label + sourceIssuance.issuance_data transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id + None -> pure () + None -> assertMsg ("Stock transfer source issuance not found: " <> transfer.transfer_data.security_id) False) (Map.values maps.stock_transfers) validateStockTransferResultIssuances : CapTableMaps -> Update () @@ -2197,12 +2315,23 @@ validateStockReissuanceResultIssuances eventIndex maps = (\reissuanceCid -> do reissuance <- fetch reissuanceCid let label = "Stock reissuance " <> reissuance.reissuance_data.id + assertMsg + (label <> " has no resulting stock issuance") + (not (null reissuance.reissuance_data.resulting_security_ids)) 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 + case reissuance.reissuance_data.split_transaction_id of + Some _ -> pure () + None -> + validateStockResultShareNumberRanges + maps + label + sourceIssuance.issuance_data + reissuance.reissuance_data.resulting_security_ids let targetSortKey = (reissuance.reissuance_data.date, 3, 0) sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex @@ -2242,8 +2371,8 @@ validateStockConsolidationResultIssuances eventIndex maps = (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 -> + (sourceQuantity, sourceIssuanceData) <- foldlA + (\(total, sourceData) sourceSecurityId -> case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid @@ -2255,12 +2384,16 @@ validateStockConsolidationResultIssuances eventIndex maps = maps sourceIssuance.issuance_data targetSortKey - pure (total + quantity) + pure (total + quantity, sourceIssuance.issuance_data :: sourceData) None -> do assertMsg (label <> " source stock issuance not found: " <> sourceSecurityId) False - pure total) - 0.0 + pure (total, sourceData)) + (0.0, []) consolidation.consolidation_data.security_ids + validateStockConsolidationShareNumberRanges + label + sourceIssuanceData + resultIssuance.issuance_data resultQuantity <- stockIssuanceQuantityAtSortKey maps resultIssuance.issuance_data targetSortKey assertMsg (label <> " resulting quantity mismatch: expected " <> show sourceQuantity <> ", found " <> show resultQuantity) @@ -2627,9 +2760,10 @@ validateAnyConversionRightStockClassReferences label maps conversionRight = case validateOptionalStockClassReference (label <> " convertible conversion target") maps convertibleRight.converts_to_stock_class_id OcfRightWarrant warrantRight -> validateOptionalStockClassReference (label <> " warrant conversion target") maps warrantRight.converts_to_stock_class_id - OcfRightStockClass stockClassRight -> do + -- The DAML stock-class right carries a placeholder trigger that is absent + -- from the OCF schema, so only the actual stock-class target is meaningful. + OcfRightStockClass stockClassRight -> validateStockClassReference (label <> " stock class conversion target") maps stockClassRight.converts_to_stock_class_id - validateAnyConversionRightStockClassReferences (label <> " stock class nested trigger") maps stockClassRight.conversion_trigger.conversion_right validateConvertibleConversionRightReferences : CapTableMaps -> Update () validateConvertibleConversionRightReferences maps = @@ -2696,6 +2830,135 @@ validateStockIssuanceReferences maps = validateOptionalReference "Stock issuance vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) (Map.values maps.stock_issuances) +data StockIssuanceShareNumberLot = StockIssuanceShareNumberLot with + stock_class_id: Text + issuance_id: Text + share_lot: StockClassShareLot + share_number_range: OcfShareNumberRange + deriving (Eq, Show) + +stockIssuanceShareNumberLots : StockClassShareEventIndex -> CapTableMaps -> Update [StockIssuanceShareNumberLot] +stockIssuanceShareNumberLots eventIndex maps = + foldlA + (\lots issuanceCid -> do + issuance <- fetch issuanceCid + let shareLot = stockIssuanceShareLot issuance.issuance_data + case stockClassShareLotCurrentQuantity eventIndex shareLot of + Some _ -> do + let issuanceLots = + map + (\shareNumberRange -> StockIssuanceShareNumberLot with + stock_class_id = issuance.issuance_data.stock_class_id + issuance_id = issuance.issuance_data.id + share_lot = shareLot + share_number_range = shareNumberRange) + issuance.issuance_data.share_numbers_issued + pure (issuanceLots ++ lots) + None -> do + assertMsg ("Stock issuance share number ranges cannot be validated because stock quantity history is invalid for " <> issuance.issuance_data.id) False + pure lots) + [] + (Map.values maps.stock_issuances) + +validateStockIssuanceShareNumberLotDoesNotOverlap : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot -> + Update () +validateStockIssuanceShareNumberLotDoesNotOverlap eventIndex lot other = + when (lot.issuance_id /= other.issuance_id && lot.stock_class_id == other.stock_class_id && shareNumberRangesOverlap lot.share_number_range other.share_number_range) do + overlapsWhileActive <- stockIssuanceShareNumberLotsOverlapWhileActive eventIndex lot other + when overlapsWhileActive do + assertMsg + ("Stock issuance share number ranges overlap for stock class " <> lot.stock_class_id <> ": " <> lot.issuance_id <> " overlaps " <> other.issuance_id) + False + +addStockIssuanceShareNumberLot : StockIssuanceShareNumberLot -> Map Text [StockIssuanceShareNumberLot] -> Map Text [StockIssuanceShareNumberLot] +addStockIssuanceShareNumberLot lot index = + Map.insert lot.stock_class_id (lot :: fromOptional [] (Map.lookup lot.stock_class_id index)) index + +stockIssuanceShareNumberLotIndex : [StockIssuanceShareNumberLot] -> Map Text [StockIssuanceShareNumberLot] +stockIssuanceShareNumberLotIndex lots = + foldl (\index lot -> addStockIssuanceShareNumberLot lot index) Map.empty lots + +stockIssuanceShareNumberLotRelevantDates : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot -> + [Time] +stockIssuanceShareNumberLotRelevantDates eventIndex lot other = + map + stockClassShareEventDate + (stockClassShareLotEvents eventIndex lot.share_lot ++ stockClassShareLotEvents eventIndex other.share_lot) + +stockIssuanceShareNumberLotActiveAt : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + Time -> + Optional Bool +stockIssuanceShareNumberLotActiveAt eventIndex lot asOf = + case stockClassShareLotQuantityAt eventIndex lot.share_lot asOf of + Some quantity -> Some (quantity > 0.0) + None -> None + +stockIssuanceShareNumberLotsOverlapOnDate : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot -> + Time -> + Update Bool +stockIssuanceShareNumberLotsOverlapOnDate eventIndex lot other asOf = + case (stockIssuanceShareNumberLotActiveAt eventIndex lot asOf, stockIssuanceShareNumberLotActiveAt eventIndex other asOf) of + (Some lotActive, Some otherActive) -> pure (lotActive && otherActive) + _ -> do + assertMsg ("Stock issuance share number ranges cannot be validated because stock quantity history is invalid for " <> lot.issuance_id <> " or " <> other.issuance_id) False + pure False + +stockIssuanceShareNumberLotsOverlapWhileActive : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + StockIssuanceShareNumberLot -> + Update Bool +stockIssuanceShareNumberLotsOverlapWhileActive eventIndex lot other = + foldlA + (\overlaps asOf -> + if overlaps + then pure True + else stockIssuanceShareNumberLotsOverlapOnDate eventIndex lot other asOf) + False + (stockIssuanceShareNumberLotRelevantDates eventIndex lot other) + +validateStockIssuanceShareNumberLotAgainstRest : + StockClassShareEventIndex -> + StockIssuanceShareNumberLot -> + [StockIssuanceShareNumberLot] -> + Update () +validateStockIssuanceShareNumberLotAgainstRest eventIndex lot lots = + mapA_ (validateStockIssuanceShareNumberLotDoesNotOverlap eventIndex lot) lots + +validateStockClassShareNumberLotsDoNotOverlap : + StockClassShareEventIndex -> + [StockIssuanceShareNumberLot] -> + Update () +validateStockClassShareNumberLotsDoNotOverlap eventIndex lots = case lots of + lot :: rest -> do + validateStockIssuanceShareNumberLotAgainstRest eventIndex lot rest + validateStockClassShareNumberLotsDoNotOverlap eventIndex rest + _ -> pure () + +validateStockIssuanceShareNumberLotsDoNotOverlap : StockClassShareEventIndex -> [StockIssuanceShareNumberLot] -> Update () +validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots = + mapA_ + (validateStockClassShareNumberLotsDoNotOverlap eventIndex) + (Map.values (stockIssuanceShareNumberLotIndex lots)) + +validateStockIssuanceShareNumberRanges : CapTableMaps -> Update () +validateStockIssuanceShareNumberRanges maps = do + eventIndex <- stockClassShareEventIndex maps + validateNumberedStockPartialReductions eventIndex maps + lots <- stockIssuanceShareNumberLots eventIndex maps + validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots + validateEquityCompensationIssuanceReferences : CapTableMaps -> Update () validateEquityCompensationIssuanceReferences maps = mapA_ @@ -5400,6 +5663,7 @@ template CapTable validateValuationStockClassReferences finalMaps validateStockPlanReferences finalMaps validateStockIssuanceReferences finalMaps + validateStockIssuanceShareNumberRanges finalMaps validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps validateVestingTransactionConditionReferences finalMaps