packages feed

exchangealgebra-0.6.0.0: src/ExchangeAlgebra/Accounting/Transaction.hs

{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wincomplete-patterns -Werror=incomplete-patterns #-}

-- | Exact transaction coordinates and posting entries in the Accounting layer.
-- These types use algebra values and Journal notes; admission and equivalence
-- share them without a dependency on input validation. Read the identifiers
-- before 'Entry' and 'isBlankKey'.
--
-- The following names identify transactions and their supporting records.
--
-- +-----------------------------+---------------------------------------------+
-- | Task                        | Type or function                            |
-- +=============================+=============================================+
-- | Identify a transaction      | t'EntityId', t'PeriodId', t'TxId', t'TxKey' |
-- +-----------------------------+---------------------------------------------+
-- | Keep original postings      | 'Entry'                                     |
-- +-----------------------------+---------------------------------------------+
-- | Check blank key coordinates | 'isBlankKey'                                |
-- +-----------------------------+---------------------------------------------+
--
-- > import qualified ExchangeAlgebra.Accounting.Transaction as Transaction
--
-- t'TxKey' supplies the note coordinates used by Definitions 10-12.
-- Constructing an identifier or an 'Entry' does not establish admission.
module ExchangeAlgebra.Accounting.Transaction
    ( EntityId(..)
    , PeriodId(..)
    , TxId(..)
    , TxKey(..)
    , Entry
    , isBlankKey
    ) where

import Data.Hashable (Hashable(..))
import Data.Text (Text)
import qualified Data.Text as Text

import ExchangeAlgebra.Accounting.Account (AccountTitles)
import ExchangeAlgebra.Algebra.Core (Alg)
import ExchangeAlgebra.Algebra.Base.Representation (HatBase)
import ExchangeAlgebra.Algebra.Value (MoneyDecimal)
import ExchangeAlgebra.Journal.Core (Note(..))

-- | Exact entity identity; admission rejects blank identifiers.
newtype EntityId = EntityId Text deriving (Eq, Ord, Show)

-- | Exact reporting-period identity; no implicit current period is assumed.
-- 'Ord' is the text order, not a calendar order: "2026-9" sorts after
-- "2026-10". Use it only for map keys; operations that need the order of
-- periods take the earlier and later period as explicit arguments.
newtype PeriodId = PeriodId Text deriving (Eq, Ord, Show)

-- | Transaction identity within one entity and period.
newtype TxId = TxId Text deriving (Eq, Ord, Show)

-- | Hash each identifier by its exact text, as t'TxKey' hashes its coordinates.
instance Hashable EntityId where
    hashWithSalt salt (EntityId text) = hashWithSalt salt text

instance Hashable PeriodId where
    hashWithSalt salt (PeriodId text) = hashWithSalt salt text

instance Hashable TxId where
    hashWithSalt salt (TxId text) = hashWithSalt salt text

-- | Exact transaction coordinate. Equality never performs wildcard matching.
data TxKey = TxKey EntityId PeriodId TxId -- ^ Entity, period, and transaction identity.
    deriving (Eq, Ord, Show)

-- | Hash the three exact text coordinates in their declared order.
instance Hashable TxKey where
    hashWithSalt salt (TxKey (EntityId entity) (PeriodId period) (TxId transaction)) =
        hashWithSalt salt (entity, period, transaction)

-- | Journal notes preserve transaction boundaries. The blank sentinel is
-- rejected by admission and is used only by the existing Journal interface.
instance Note TxKey where
    plank = TxKey (EntityId "") (PeriodId "") (TxId "")

-- | Concrete entry representation, retaining every original posting.
type Entry = Alg MoneyDecimal (HatBase AccountTitles)

-- | Reject empty or whitespace-only entity, period, or transaction identities.
isBlankKey :: TxKey -> Bool
isBlankKey (TxKey (EntityId entity) (PeriodId period) (TxId transaction)) =
    any (Text.null . Text.strip) [entity, period, transaction]