packages feed

haspara-0.0.0.4: src/Haspara/Accounting/Balance.hs

-- | This module provides definitions for balances used as in accounting.

{-# LANGUAGE DataKinds #-}

module Haspara.Accounting.Balance where

import qualified Data.Aeson                 as Aeson
import           GHC.Generics               (Generic)
import           GHC.TypeLits               (KnownNat, Nat)
import           Haspara.Accounting.Account (AccountKind)
import           Haspara.Accounting.Amount  (Amount(Amount), quantityFromAmount, valueFromAmount)
import           Haspara.Accounting.Side    (Side(..), otherSide)
import           Haspara.Internal.Aeson     (commonAesonOptions)
import           Haspara.Quantity           (Quantity, absQuantity)
import           Refined                    (unrefine)


-- | Data definition for balances.
--
-- This definition is similar to 'Haspara.Accounting.Amount.Amount', however,
-- the value is allowed to be negative to reflect "Negative Balance" phenomenon.
--
-- See https://www.accountingtools.com/articles/what-is-a-negative-balance.html
data Balance (precision :: Nat) = Balance
  { balanceSide  :: !Side
  , balanceValue :: !(Quantity precision)
  }
  deriving (Eq, Generic, Show)


-- | 'Aeson.FromJSON' instance for 'Balance'.
--
-- For normal balances:
--
-- >>> Aeson.eitherDecode "{\"side\": \"db\", \"value\": 42}" :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideDebit, balanceValue = 42.00})
-- >>> Aeson.eitherDecode "{\"side\": \"cr\", \"value\": 42}" :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideCredit, balanceValue = 42.00})
--
-- For negative balances:
--
-- >>> Aeson.eitherDecode "{\"side\": \"db\", \"value\": -42}" :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideDebit, balanceValue = -42.00})
-- >>> Aeson.eitherDecode "{\"side\": \"cr\", \"value\": -42}" :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideCredit, balanceValue = -42.00})
instance KnownNat precision => Aeson.FromJSON (Balance precision) where
  parseJSON = Aeson.genericParseJSON $ commonAesonOptions "balance"


-- | 'Aeson.ToJSON' instance for 'Balance'.
--
-- For normal balances:
--
-- >>> import Haspara.Accounting.Side
-- >>> import Haspara.Quantity
-- >>> Aeson.encode (Balance SideDebit (mkQuantity 42 :: Quantity 2))
-- "{\"side\":\"db\",\"value\":42.0}"
-- >>> Aeson.encode (Balance SideCredit (mkQuantity 42 :: Quantity 2))
-- "{\"side\":\"cr\",\"value\":42.0}"
-- >>> Aeson.eitherDecode (Aeson.encode (Balance SideDebit (mkQuantity 42 :: Quantity 2))) :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideDebit, balanceValue = 42.00})
-- >>> Aeson.eitherDecode (Aeson.encode (Balance SideCredit (mkQuantity 42 :: Quantity 2))) :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideCredit, balanceValue = 42.00})
--
-- For negative balances:
--
-- >>> Aeson.encode (Balance SideDebit (mkQuantity (-42) :: Quantity 2))
-- "{\"side\":\"db\",\"value\":-42.0}"
-- >>> Aeson.encode (Balance SideCredit (mkQuantity (-42) :: Quantity 2))
-- "{\"side\":\"cr\",\"value\":-42.0}"
-- >>> Aeson.eitherDecode (Aeson.encode (Balance SideDebit (mkQuantity (-42) :: Quantity 2))) :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideDebit, balanceValue = -42.00})
-- >>> Aeson.eitherDecode (Aeson.encode (Balance SideCredit (mkQuantity (-42) :: Quantity 2))) :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideCredit, balanceValue = -42.00})
instance KnownNat precision => Aeson.ToJSON (Balance precision) where
  toJSON = Aeson.genericToJSON $ commonAesonOptions "balance"


-- | Returns the debit quantity, if any.
balanceDebit
  :: KnownNat precision
  => Balance precision
  -> Maybe (Quantity precision)
balanceDebit (Balance SideDebit v) = Just v
balanceDebit _                     = Nothing


-- | Returns the credit quantity, if any.
balanceCredit
  :: KnownNat precision
  => Balance precision
  -> Maybe (Quantity precision)
balanceCredit (Balance SideCredit v) = Just v
balanceCredit _                      = Nothing


-- | Updates the balance with the given amount.
--
-- >>> import Haspara.Accounting.Amount
-- >>> import Haspara.Accounting.Side
-- >>> import Refined.Unsafe
-- >>> let balance = Balance SideDebit 42 :: Balance 2
-- >>> balance
-- Balance {balanceSide = SideDebit, balanceValue = 42.00}
-- >>> let amountDebit = Amount SideDebit (unsafeRefine 10) :: Amount 2
-- >>> amountDebit
-- Amount {amountSide = SideDebit, amountValue = Refined 10.00}
-- >>> let amountCredit = Amount SideCredit (unsafeRefine 10) :: Amount 2
-- >>> amountCredit
-- Amount {amountSide = SideCredit, amountValue = Refined 10.00}
-- >>> updateBalance balance amountDebit
-- Balance {balanceSide = SideDebit, balanceValue = 52.00}
-- >>> updateBalance balance amountCredit
-- Balance {balanceSide = SideDebit, balanceValue = 32.00}
updateBalance
  :: KnownNat precision
  => Balance precision
  -> Amount precision
  -> Balance precision
updateBalance (Balance bSide bVal) (Amount aSide aVal) =
  Balance bSide (bVal + (unrefine aVal * (if bSide == aSide then 1 else (-1))))


-- | Converts the balance to amount.
--
-- >>> import Haspara.Accounting.Side
-- >>> amountFromBalance (Balance SideDebit 42 :: Balance 2)
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
-- >>> amountFromBalance (Balance SideDebit (-42) :: Balance 2)
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
-- >>> amountFromBalance (Balance SideCredit 42 :: Balance 2)
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
-- >>> amountFromBalance (Balance SideCredit (-42) :: Balance 2)
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
amountFromBalance
  :: KnownNat precision
  => Balance precision
  -> Amount precision
amountFromBalance (Balance side value) =
  Amount (if value < 0 then otherSide side else side) (absQuantity value)


-- | Returns the quantity of the balance given the account kind.
--
-- See 'quantityFromAmount' for the meaning of quantity.
quantityFromBalance
  :: KnownNat precision
  => AccountKind
  -> Balance precision
  -> Quantity precision
quantityFromBalance k = quantityFromAmount k . amountFromBalance


-- | Returns the value of the balance given the account kind.
--
-- See 'valueFromAmount' for the meaning of quantity.
valueFromBalance
  :: KnownNat precision
  => AccountKind
  -> Balance precision
  -> Quantity precision
valueFromBalance k = valueFromAmount k . amountFromBalance