packages feed

haspara-0.0.0.8: src/Haspara/Accounting/Side.hs

{-# LANGUAGE OverloadedStrings #-}

-- | This module provides definitions for and functions to work with
-- Debit/Credit dichotomy which is essential to double-entry bookkeeping.
--
-- In our concept, we refer to this dichotomy as "Side" (materialized via 'Side'
-- sum-type) which is either "Debit" (materialized via 'SideDebit' nullary data
-- constructor) or "Dredit" (materialized via 'SideCredit' nullary data
-- constructor).
--
-- This module provides 'Aeson.FromJSON' and 'Aeson.ToJSON' instances for 'Side'
-- as well. Following accounting conventions, we chose the JSON value for
-- "Debit" as @"db"@, and for "Credit" as @"cr"@.
module Haspara.Accounting.Side where

import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Encoding as Aeson.Encoding
import qualified Data.Text as T
import GHC.TypeLits (KnownNat)
import Haspara.Accounting.Account (AccountKind (..))
import Haspara.Quantity (Quantity)


-- | Data definition for encoding the debit/credit indicator.
data Side = SideDebit | SideCredit
  deriving (Eq, Ord, Show)


-- | 'Aeson.FromJSON' instance for 'Side'.
--
-- >>> :set -XOverloadedStrings
-- >>> Aeson.eitherDecode "\"db\"" :: Either String Side
-- Right SideDebit
-- >>> Aeson.eitherDecode "\"cr\"" :: Either String Side
-- Right SideCredit
-- >>> Aeson.eitherDecode "\"hebele\"" :: Either String Side
-- Left "Error in $: Unkown side indicator: \"hebele\". Expecting one of \"db\" or \"cr\""
instance Aeson.FromJSON Side where
  parseJSON = Aeson.withText "Side" $ \t -> case t of
    "db" -> pure SideDebit
    "cr" -> pure SideCredit
    _ -> fail $ "Unkown side indicator: \"" <> T.unpack t <> "\". Expecting one of \"db\" or \"cr\""


-- | 'Aeson.ToJSON' instance for 'Side'.
--
-- >>> Aeson.encode SideDebit
-- "\"db\""
-- >>> Aeson.encode SideCredit
-- "\"cr\""
-- >>> Aeson.decode (Aeson.encode SideDebit) == Just SideDebit
-- True
-- >>> Aeson.decode (Aeson.encode SideCredit) == Just SideCredit
-- True
instance Aeson.ToJSON Side where
  toJSON SideDebit = Aeson.String "db"
  toJSON SideCredit = Aeson.String "cr"


  toEncoding SideDebit = Aeson.Encoding.text "db"
  toEncoding SideCredit = Aeson.Encoding.text "cr"


-- | Gives the other side.
--
-- >>> otherSide SideDebit
-- SideCredit
-- >>> otherSide SideCredit
-- SideDebit
otherSide :: Side -> Side
otherSide SideDebit = SideCredit
otherSide SideCredit = SideDebit


-- | Computes the 'Side' by the given 'AccountKind' and the sign of the given
-- 'Quantity'.
--
-- The sign of the 'Quantity' is indeed a proxy for whether the event of the
-- 'Quantity' is an increment (@+1@) or decrement (@-1@) event.
--
-- @0@ quantities are considered to originate from an increment event. So far,
-- this seems to be a safe assumption that gives us totality in the context of
-- this function.
--
-- Note the following mapping 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 |
-- +-----------------------+----------+----------+
--
-- >>> :set -XDataKinds
-- >>> import Haspara.Quantity
-- >>> let decrement = mkQuantity (-0.42) :: Quantity 2
-- >>> let nocrement = mkQuantity 0 :: Quantity 2
-- >>> let increment = mkQuantity 0.42 :: Quantity 2
-- >>> fmap (sideByAccountKind AccountKindAsset) [decrement, nocrement, increment]
-- [SideCredit,SideDebit,SideDebit]
-- >>> fmap (sideByAccountKind AccountKindLiability) [decrement, nocrement, increment]
-- [SideDebit,SideCredit,SideCredit]
-- >>> fmap (sideByAccountKind AccountKindEquity) [decrement, nocrement, increment]
-- [SideDebit,SideCredit,SideCredit]
-- >>> fmap (sideByAccountKind AccountKindRevenue) [decrement, nocrement, increment]
-- [SideDebit,SideCredit,SideCredit]
-- >>> fmap (sideByAccountKind AccountKindExpense) [decrement, nocrement, increment]
-- [SideCredit,SideDebit,SideDebit]
sideByAccountKind
  :: KnownNat precision
  => AccountKind
  -> Quantity precision
  -> Side
sideByAccountKind k q = case (k, signum q >= 0) of
  (AccountKindAsset, False) -> SideCredit
  (AccountKindAsset, True) -> SideDebit
  (AccountKindLiability, False) -> SideDebit
  (AccountKindLiability, True) -> SideCredit
  (AccountKindEquity, False) -> SideDebit
  (AccountKindEquity, True) -> SideCredit
  (AccountKindRevenue, False) -> SideDebit
  (AccountKindRevenue, True) -> SideCredit
  (AccountKindExpense, False) -> SideCredit
  (AccountKindExpense, True) -> SideDebit


-- | Returns the "normal" side for a given 'AccountKind'.
--
-- Note the following mapping as a guide:
--
-- +-----------------+----------------+------------------+
-- | Kind of Account | Normal Balance | Negative Balance |
-- +-----------------+----------------+------------------+
-- | Asset           | Debit          | Credit           |
-- +-----------------+----------------+------------------+
-- | Liability       | Credit         | Debit            |
-- +-----------------+----------------+------------------+
-- | Equity          | Credit         | Debit            |
-- +-----------------+----------------+------------------+
-- | Revenue         | Credit         | Debit            |
-- +-----------------+----------------+------------------+
-- | Expense         | Debit          | Credit           |
-- +-----------------+----------------+------------------+
normalSideByAccountKind :: AccountKind -> Side
normalSideByAccountKind AccountKindAsset = SideDebit
normalSideByAccountKind AccountKindLiability = SideCredit
normalSideByAccountKind AccountKindEquity = SideCredit
normalSideByAccountKind AccountKindRevenue = SideCredit
normalSideByAccountKind AccountKindExpense = SideDebit