haspara-0.0.0.2: src/Haspara/Accounting/Ledger.hs
-- | This module provides definitions for postings, ledgers and ledger entries.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
module Haspara.Accounting.Ledger where
import Data.Aeson ((.:), (.=))
import qualified Data.Aeson as Aeson
import qualified Data.Char as C
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import Data.Time (Day)
import Deriving.Aeson (CustomJSON(CustomJSON), FromJSON, Generic, ToJSON)
import Deriving.Aeson.Stock (PrefixedSnake, Vanilla)
import GHC.TypeLits (KnownNat, Nat)
import Haspara.Accounting.Account (Account(accountKind), AccountKind(..))
import Haspara.Accounting.Event (Event(..), eventObject)
import Haspara.Quantity (Quantity, UnsignedQuantity)
import Refined (unrefine)
-- | Type encoding of a ledger.
data Ledger a o (s :: Nat) = Ledger
{ ledgerAccount :: !(Account a)
, ledgerOpening :: !(Quantity s)
, ledgerClosing :: !(Quantity s)
, ledgerRunning :: ![LedgerItem o s]
} deriving (Eq, Generic, Ord, Show)
deriving (FromJSON, ToJSON) via PrefixedSnake "ledger" (Ledger a o s)
-- | Type encoding of a ledger item.
data LedgerItem o (s :: Nat) = LedgerItem
{ ledgerItemEntry :: !(Entry o s)
, ledgerItemBalance :: !(Quantity s)
} deriving (Eq, Generic, Ord, Show)
deriving (FromJSON, ToJSON)
via PrefixedSnake "ledgerItem" (LedgerItem o s)
-- | Creates a ledger from a given list of 'Entry' values.
mkLedger :: KnownNat s => Account a -> Quantity s -> [Entry o s] -> Ledger a o s
mkLedger a o = foldl addEntry (Ledger a o o [])
-- | Adds a new entry to a ledger.
addEntry :: KnownNat s => Ledger a o s -> Entry o s -> Ledger a o s
addEntry l@(Ledger _ _ c r) e = l { ledgerClosing = balance, ledgerRunning = r <> [item]}
where
balance = c + entryQuantity e
item = LedgerItem e balance
-- | Type encoding for a posting.
--
-- >>> :set -XDataKinds
-- >>> import Haspara.Accounting
-- >>> import Refined
-- >>> import qualified Data.Aeson as Aeson
-- >>> import qualified Data.List.NonEmpty as NE
-- >>> let date = read "2021-01-01"
-- >>> let oid = 1 :: Int
-- >>> let qty = $$(refineTH 42) :: UnsignedQuantity 2
-- >>> let event = EventDecrement date oid qty
-- >>> let account = Account AccountKindAsset ("Cash" :: String, 1 ::Int)
-- >>> let posting = Posting . NE.fromList $ [(event, account)]
-- >>> let json = Aeson.encode posting
-- >>> json
-- "[[{\"qty\":42.0,\"type\":\"DECREMENT\",\"obj\":1,\"date\":\"2021-01-01\"},{\"kind\":\"ASSET\",\"object\":[\"Cash\",1]}]]"
-- >>> Aeson.decode json :: Maybe (Posting (String, Int) Int 2)
-- Just (Posting ((EventDecrement 2021-01-01 1 (Refined 42.00),Account {accountKind = AccountKindAsset, accountObject = ("Cash",1)}) :| []))
-- >>> Aeson.decode json == Just posting
-- True
newtype Posting a o (s :: Nat) = Posting (NE.NonEmpty (Event o s, Account a))
deriving (Eq, Generic, Ord, Show)
deriving (FromJSON, ToJSON)
via Vanilla (Posting a o s)
-- | Returns the list of posting event sources.
postingEvents :: (KnownNat s) => Posting a o s -> [o]
postingEvents (Posting es) = eventObject . fst <$> NE.toList es
-- | Posts an event.
post :: (KnownNat s) => Posting a o s -> [(Account a, Entry o s)]
post (Posting xs) = go (NE.toList xs)
where
go [] = []
go ((ev, ac) : ys) = (ac, buildEntry ev (accountKind ac)) : go ys
-- | Encoding of a posting entry.
--
-- >>> :set -XDataKinds
-- >>> import Refined
-- >>> let date = read "2021-01-01"
-- >>> let oid = 1 :: Int
-- >>> let qty = $$(refineTH 42) :: UnsignedQuantity 2
-- >>> let entry = EntryDebit date oid qty
-- >>> let json = Aeson.encode entry
-- >>> json
-- "{\"qty\":42.0,\"type\":\"DEBIT\",\"obj\":1,\"date\":\"2021-01-01\"}"
-- >>> Aeson.decode json :: Maybe (Entry Int 2)
-- Just (EntryDebit 2021-01-01 1 (Refined 42.00))
-- >>> Aeson.decode json == Just entry
-- True
data Entry o (s :: Nat) =
EntryDebit Day o (UnsignedQuantity s)
| EntryCredit Day o (UnsignedQuantity s)
deriving (Eq, Ord, Show)
instance (Aeson.FromJSON o, KnownNat s) => Aeson.FromJSON (Entry o s) where
parseJSON = Aeson.withObject "Entry" $ \o -> do
dorc <- o .: "type"
cons <- case T.map C.toUpper dorc of
"DEBIT" -> pure EntryDebit
"CREDIT" -> pure EntryCredit
x -> fail ("Unknown entry type: " <> T.unpack x)
date <- o .: "date"
obj <- o .: "obj"
qty <- o .: "qty"
pure (cons date obj qty)
instance (Aeson.ToJSON o, KnownNat s) => Aeson.ToJSON (Entry o s) where
toJSON x = case x of
EntryDebit d o q -> Aeson.object ["type" .= ("DEBIT" :: T.Text), "date" .= d, "obj" .= o, "qty" .= q]
EntryCredit d o q -> Aeson.object ["type" .= ("CREDIT" :: T.Text), "date" .= d, "obj" .= o, "qty" .= q]
-- | Returns the date of the posting entry.
entryDate :: KnownNat s => Entry o s -> Day
entryDate (EntryDebit d _ _) = d
entryDate (EntryCredit d _ _) = d
-- | Returns the quantity of the posting entry.
entryQuantity :: KnownNat s => Entry o s -> Quantity s
entryQuantity (EntryDebit _ _ q) = unrefine q
entryQuantity (EntryCredit _ _ q) = -(unrefine q)
-- | Returns the source object of the posting entry.
entryObject :: KnownNat s => Entry o s -> o
entryObject (EntryDebit _ o _) = o
entryObject (EntryCredit _ o _) = o
-- | Returns the debit quantity of the posting entry.
entryDebit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
entryDebit (EntryDebit _ _ x) = Just x
entryDebit EntryCredit {} = Nothing
-- | Returns the credit quantity of the posting entry.
entryCredit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
entryCredit EntryDebit {} = Nothing
entryCredit (EntryCredit _ _ x) = Just x
-- | Consumes an event and a type of account, and produces a posting entry.
--
-- Note the following map as a guide:
--
-- +-----------------------+----------+----------+
-- | Kind of account | Debit | Credit |
-- +-----------------------+----------+----------+
-- | Asset | Increase | Decrease |
-- +-----------------------+----------+----------+
-- | Liability | Decrease | Increase |
-- +-----------------------+----------+----------+
-- | Equity/Capital | Decrease | Increase |
-- +-----------------------+----------+----------+
-- | Income/Revenue | Decrease | Increase |
-- +-----------------------+----------+----------+
-- | Expense/Cost/Dividend | Increase | Decrease |
-- +-----------------------+----------+----------+
--
buildEntry :: (KnownNat s) => Event o s -> AccountKind -> Entry o s
buildEntry (EventDecrement d o x) AccountKindAsset = EntryCredit d o x
buildEntry (EventIncrement d o x) AccountKindAsset = EntryDebit d o x
buildEntry (EventDecrement d o x) AccountKindLiability = EntryDebit d o x
buildEntry (EventIncrement d o x) AccountKindLiability = EntryCredit d o x
buildEntry (EventDecrement d o x) AccountKindEquity = EntryDebit d o x
buildEntry (EventIncrement d o x) AccountKindEquity = EntryCredit d o x
buildEntry (EventDecrement d o x) AccountKindRevenue = EntryDebit d o x
buildEntry (EventIncrement d o x) AccountKindRevenue = EntryCredit d o x
buildEntry (EventDecrement d o x) AccountKindExpense = EntryCredit d o x
buildEntry (EventIncrement d o x) AccountKindExpense = EntryDebit d o x