{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE KindSignatures #-}

-- | This module provides definitions for amounts used as in accounting.
--
-- For balance definition that allows "Negative Balance" phenomenon, see
-- 'Haspara.Accounting.Balance'.
module Haspara.Accounting.Amount where

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


-- | Data definition for amounts.
data Amount (precision :: Nat) = Amount
  { forall (precision :: Nat). Amount precision -> Side
amountSide :: !Side
  , forall (precision :: Nat).
Amount precision -> UnsignedQuantity precision
amountValue :: !(UnsignedQuantity precision)
  }
  deriving (Amount precision -> Amount precision -> Bool
forall (precision :: Nat).
Amount precision -> Amount precision -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: Amount precision -> Amount precision -> Bool
$c/= :: forall (precision :: Nat).
Amount precision -> Amount precision -> Bool
== :: Amount precision -> Amount precision -> Bool
$c== :: forall (precision :: Nat).
Amount precision -> Amount precision -> Bool
Eq, forall (precision :: Nat) x.
Rep (Amount precision) x -> Amount precision
forall (precision :: Nat) x.
Amount precision -> Rep (Amount precision) x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cto :: forall (precision :: Nat) x.
Rep (Amount precision) x -> Amount precision
$cfrom :: forall (precision :: Nat) x.
Amount precision -> Rep (Amount precision) x
Generic, Amount precision -> Amount precision -> Bool
Amount precision -> Amount precision -> Ordering
forall (precision :: Nat). Eq (Amount precision)
forall (precision :: Nat).
Amount precision -> Amount precision -> Bool
forall (precision :: Nat).
Amount precision -> Amount precision -> Ordering
forall (precision :: Nat).
Amount precision -> Amount precision -> Amount precision
forall a.
Eq a
-> (a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
min :: Amount precision -> Amount precision -> Amount precision
$cmin :: forall (precision :: Nat).
Amount precision -> Amount precision -> Amount precision
max :: Amount precision -> Amount precision -> Amount precision
$cmax :: forall (precision :: Nat).
Amount precision -> Amount precision -> Amount precision
>= :: Amount precision -> Amount precision -> Bool
$c>= :: forall (precision :: Nat).
Amount precision -> Amount precision -> Bool
> :: Amount precision -> Amount precision -> Bool
$c> :: forall (precision :: Nat).
Amount precision -> Amount precision -> Bool
<= :: Amount precision -> Amount precision -> Bool
$c<= :: forall (precision :: Nat).
Amount precision -> Amount precision -> Bool
< :: Amount precision -> Amount precision -> Bool
$c< :: forall (precision :: Nat).
Amount precision -> Amount precision -> Bool
compare :: Amount precision -> Amount precision -> Ordering
$ccompare :: forall (precision :: Nat).
Amount precision -> Amount precision -> Ordering
Ord, Int -> Amount precision -> ShowS
forall (precision :: Nat).
KnownNat precision =>
Int -> Amount precision -> ShowS
forall (precision :: Nat).
KnownNat precision =>
[Amount precision] -> ShowS
forall (precision :: Nat).
KnownNat precision =>
Amount precision -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [Amount precision] -> ShowS
$cshowList :: forall (precision :: Nat).
KnownNat precision =>
[Amount precision] -> ShowS
show :: Amount precision -> String
$cshow :: forall (precision :: Nat).
KnownNat precision =>
Amount precision -> String
showsPrec :: Int -> Amount precision -> ShowS
$cshowsPrec :: forall (precision :: Nat).
KnownNat precision =>
Int -> Amount precision -> ShowS
Show)


-- | 'Aeson.FromJSON' instance for 'Amount'.
--
-- >>> :set -XDataKinds
-- >>> :set -XOverloadedStrings
-- >>> Aeson.eitherDecode "{\"side\": \"db\", \"value\": 42}" :: Either String (Amount 2)
-- Right (Amount {amountSide = SideDebit, amountValue = Refined 42.00})
-- >>> Aeson.eitherDecode "{\"side\": \"cr\", \"value\": 42}" :: Either String (Amount 2)
-- Right (Amount {amountSide = SideCredit, amountValue = Refined 42.00})
instance KnownNat precision => Aeson.FromJSON (Amount precision) where
  parseJSON :: Value -> Parser (Amount precision)
parseJSON = forall a.
(Generic a, GFromJSON Zero (Rep a)) =>
Options -> Value -> Parser a
Aeson.genericParseJSON forall a b. (a -> b) -> a -> b
$ String -> Options
commonAesonOptions String
"amount"


-- | 'Aeson.ToJSON' instance for 'Amount'.
--
-- >>> :set -XDataKinds
-- >>> import Haspara.Accounting.Side
-- >>> import Haspara.Quantity
-- >>> import Refined.Unsafe
-- >>> Aeson.encode (Amount SideDebit (unsafeRefine (mkQuantity 42 :: Quantity 2)))
-- "{\"side\":\"db\",\"value\":42.0}"
-- >>> Aeson.encode (Amount SideCredit (unsafeRefine (mkQuantity 42 :: Quantity 2)))
-- "{\"side\":\"cr\",\"value\":42.0}"
-- >>> Aeson.eitherDecode (Aeson.encode (Amount SideDebit (unsafeRefine (mkQuantity 42 :: Quantity 2)))) :: Either String (Amount 2)
-- Right (Amount {amountSide = SideDebit, amountValue = Refined 42.00})
-- >>> Aeson.eitherDecode (Aeson.encode (Amount SideCredit (unsafeRefine (mkQuantity 42 :: Quantity 2)))) :: Either String (Amount 2)
-- Right (Amount {amountSide = SideCredit, amountValue = Refined 42.00})
instance KnownNat precision => Aeson.ToJSON (Amount precision) where
  toJSON :: Amount precision -> Value
toJSON = forall a.
(Generic a, GToJSON' Value Zero (Rep a)) =>
Options -> a -> Value
Aeson.genericToJSON forall a b. (a -> b) -> a -> b
$ String -> Options
commonAesonOptions String
"amount"
  toEncoding :: Amount precision -> Encoding
toEncoding = forall a.
(Generic a, GToJSON' Encoding Zero (Rep a)) =>
Options -> a -> Encoding
Aeson.genericToEncoding forall a b. (a -> b) -> a -> b
$ String -> Options
commonAesonOptions String
"amount"


-- | Returns the debit value of the 'Amount', if any.
amountDebit :: KnownNat precision => Amount precision -> Maybe (UnsignedQuantity precision)
amountDebit :: forall (precision :: Nat).
KnownNat precision =>
Amount precision -> Maybe (UnsignedQuantity precision)
amountDebit (Amount Side
SideDebit UnsignedQuantity precision
value) = forall a. a -> Maybe a
Just UnsignedQuantity precision
value
amountDebit Amount precision
_ = forall a. Maybe a
Nothing


-- | Returns the credit value of the 'Amount', if any.
amountCredit :: KnownNat precision => Amount precision -> Maybe (UnsignedQuantity precision)
amountCredit :: forall (precision :: Nat).
KnownNat precision =>
Amount precision -> Maybe (UnsignedQuantity precision)
amountCredit (Amount Side
SideCredit UnsignedQuantity precision
value) = forall a. a -> Maybe a
Just UnsignedQuantity precision
value
amountCredit Amount precision
_ = forall a. Maybe a
Nothing


-- | Builds the 'Amount' for the given /value/ for the given 'AccountKind'.
--
-- The /value/ concept here refers to the value of a particular economic event
-- as in the contribution of that event to the net-worth of the entity.
--
-- This definition of the value is different than what we refer to in
-- 'amountFromQuantity'. In 'amountFromQuantity' the /quantity/ is simply
-- reflecting the increment or decrement in a particular account of a particular
-- 'AccountKind'.
--
-- For example, consider getting a loan: There are two immediate events due to
-- this exchange:
--
-- 1. Inflow of cash of some quantity to an 'AccountKindAsset' account.
-- 2. Inflow of loan contract with some notional value of the same quantity to a
--    'AccountKindLiability acount.
--
-- Let's say, the notional is USD 1,000. Therefore:
--
-- 1. Inflow of USD 1,000 to the cash account.
-- 2. Inflow of a Loan Contract of USD 1,000 to the liability account.
--
-- Conventionally, the latter is reflected as follow:
--
-- >>> :set -XDataKinds
-- >>> import Haspara.Quantity
-- >>> amountFromQuantity AccountKindLiability (mkQuantity 1000 :: Quantity 2)
-- Amount {amountSide = SideCredit, amountValue = Refined 1000.00}
--
-- However, if the call-site is referring to values as in the net effect of the
-- event to the net-worth of the entity, then:
--
-- >>> amountFromValue AccountKindLiability (mkQuantity (-1000) :: Quantity 2)
-- Amount {amountSide = SideCredit, amountValue = Refined 1000.00}
--
-- For reference, given:
--
-- >>> let valPos = mkQuantity 42 :: Quantity 2
-- >>> let valNeg = mkQuantity (-42) :: Quantity 2
--
-- ..., let's consider following events:
--
-- We have an inflow and outflow of some assets, respectively:
--
-- >>> amountFromValue AccountKindAsset valPos
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
-- >>> amountFromValue AccountKindAsset valNeg
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
--
-- We have some decrease and increase in our liabilities, respectively:
--
-- >>> amountFromValue AccountKindLiability valPos
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
-- >>> amountFromValue AccountKindLiability valNeg
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
--
-- We have some increase and decrease in our equity, respectively:
--
-- >>> amountFromValue AccountKindEquity valPos
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
-- >>> amountFromValue AccountKindEquity valNeg
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
--
-- We have some profit and loss in our PnL, respectively:
--
-- >>> amountFromValue AccountKindRevenue valPos
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
-- >>> amountFromValue AccountKindRevenue valNeg
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
--
-- We have some decrease and increase in our expenses, respectively:
--
-- >>> amountFromValue AccountKindExpense valPos
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
-- >>> amountFromValue AccountKindExpense valNeg
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
amountFromValue
  :: KnownNat precision
  => AccountKind
  -> Quantity precision
  -> Amount precision
amountFromValue :: forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Quantity precision -> Amount precision
amountFromValue AccountKind
k Quantity precision
q = case AccountKind
k of
  AccountKind
AccountKindAsset -> Amount {amountSide :: Side
amountSide = if Quantity precision
q forall a. Ord a => a -> a -> Bool
>= Quantity precision
0 then Side
SideDebit else Side
SideCredit, amountValue :: UnsignedQuantity precision
amountValue = forall (s :: Nat). KnownNat s => Quantity s -> UnsignedQuantity s
absQuantity Quantity precision
q}
  AccountKind
AccountKindLiability -> Amount {amountSide :: Side
amountSide = if Quantity precision
q forall a. Ord a => a -> a -> Bool
>= Quantity precision
0 then Side
SideDebit else Side
SideCredit, amountValue :: UnsignedQuantity precision
amountValue = forall (s :: Nat). KnownNat s => Quantity s -> UnsignedQuantity s
absQuantity Quantity precision
q}
  AccountKind
AccountKindEquity -> Amount {amountSide :: Side
amountSide = if Quantity precision
q forall a. Ord a => a -> a -> Bool
>= Quantity precision
0 then Side
SideCredit else Side
SideDebit, amountValue :: UnsignedQuantity precision
amountValue = forall (s :: Nat). KnownNat s => Quantity s -> UnsignedQuantity s
absQuantity Quantity precision
q}
  AccountKind
AccountKindRevenue -> Amount {amountSide :: Side
amountSide = if Quantity precision
q forall a. Ord a => a -> a -> Bool
>= Quantity precision
0 then Side
SideCredit else Side
SideDebit, amountValue :: UnsignedQuantity precision
amountValue = forall (s :: Nat). KnownNat s => Quantity s -> UnsignedQuantity s
absQuantity Quantity precision
q}
  AccountKind
AccountKindExpense -> Amount {amountSide :: Side
amountSide = if Quantity precision
q forall a. Ord a => a -> a -> Bool
>= Quantity precision
0 then Side
SideCredit else Side
SideDebit, amountValue :: UnsignedQuantity precision
amountValue = forall (s :: Nat). KnownNat s => Quantity s -> UnsignedQuantity s
absQuantity Quantity precision
q}


-- | Returns the value for the given 'Amount' for the given 'AccountKind'.
--
-- This is dual to 'amountFromValue'.
--
-- For values of positive and negative net-effect on the net-worth of the
-- entity, respectively:
--
-- >>> :set -XDataKinds
-- >>> import Haspara.Quantity
-- >>> let valPos = mkQuantity 42 :: Quantity 2
-- >>> let valNeg = mkQuantity (-42) :: Quantity 2
--
-- ..., for a @check@ function that checks if the roundtrip to a value is
-- successful for a given 'AccountKind':
--
-- >>> let check = \k v -> v == valueFromAmount k (amountFromValue k v)
--
-- ..., and for the list of 'AccountKind's.
--
-- >>> let kinds = [minBound .. maxBound] :: [AccountKind]
-- >>> kinds
-- [AccountKindAsset,AccountKindLiability,AccountKindEquity,AccountKindRevenue,AccountKindExpense]
--
-- All checks should pass:
--
-- >>> all (\k -> check k valPos && check k valNeg) kinds
-- True
valueFromAmount
  :: KnownNat precision
  => AccountKind
  -> Amount precision
  -> Quantity precision
valueFromAmount :: forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Amount precision -> Quantity precision
valueFromAmount AccountKind
k (Amount Side
s UnsignedQuantity precision
v) = case (AccountKind
k, Side
s, forall p x. Refined p x -> x
unrefine UnsignedQuantity precision
v) of
  (AccountKind
AccountKindAsset, Side
SideDebit, Quantity precision
q) -> Quantity precision
q
  (AccountKind
AccountKindAsset, Side
SideCredit, Quantity precision
q) -> -Quantity precision
q
  (AccountKind
AccountKindLiability, Side
SideDebit, Quantity precision
q) -> Quantity precision
q
  (AccountKind
AccountKindLiability, Side
SideCredit, Quantity precision
q) -> -Quantity precision
q
  (AccountKind
AccountKindEquity, Side
SideDebit, Quantity precision
q) -> -Quantity precision
q
  (AccountKind
AccountKindEquity, Side
SideCredit, Quantity precision
q) -> Quantity precision
q
  (AccountKind
AccountKindRevenue, Side
SideDebit, Quantity precision
q) -> -Quantity precision
q
  (AccountKind
AccountKindRevenue, Side
SideCredit, Quantity precision
q) -> Quantity precision
q
  (AccountKind
AccountKindExpense, Side
SideDebit, Quantity precision
q) -> -Quantity precision
q
  (AccountKind
AccountKindExpense, Side
SideCredit, Quantity precision
q) -> Quantity precision
q


-- | Builds the 'Amount' value for the given account kind and quantity.
--
-- The concept of /quantity/ here refers to the conventional concept of what it
-- means for an 'Haspara.Accounting.Account.Account' of a given 'AccountKind'.
--
-- For example, a loan of USD 1,000 has an increase in our liabilities.
-- Therefore, the quantity is expected to be positive:
--
-- >>> :set -XDataKinds
-- >>> import Haspara.Quantity
-- >>> amountFromQuantity AccountKindLiability (mkQuantity 1000 :: Quantity 2)
-- Amount {amountSide = SideCredit, amountValue = Refined 1000.00}
--
-- Note 'amountFromValue' function if you are rather working with values that
-- are conceptually different than the /quantity/ here whereby a /value/ refers
-- to the value of a particular economic event as in the contribution of that
-- event to the net-worth of the entity. Therefore, above example would be
-- reflected as follows to get the same 'Amount' value:
--
-- >>> amountFromValue AccountKindLiability (mkQuantity (-1000) :: Quantity 2)
-- Amount {amountSide = SideCredit, amountValue = Refined 1000.00}
--
-- Check 'amountFromValue' documentation for further information.
amountFromQuantity
  :: KnownNat precision
  => AccountKind
  -> Quantity precision
  -> Amount precision
amountFromQuantity :: forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Quantity precision -> Amount precision
amountFromQuantity AccountKind
k Quantity precision
q =
  Amount
    { amountSide :: Side
amountSide = forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Quantity precision -> Side
sideByAccountKind AccountKind
k Quantity precision
q
    , amountValue :: UnsignedQuantity precision
amountValue = forall (s :: Nat). KnownNat s => Quantity s -> UnsignedQuantity s
absQuantity Quantity precision
q
    }


-- | Returns the quantity for the given amount.
--
-- This is dual to 'amountFromQuantity'.
quantityFromAmount
  :: KnownNat precision
  => AccountKind
  -> Amount precision
  -> Quantity precision
quantityFromAmount :: forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Amount precision -> Quantity precision
quantityFromAmount AccountKind
k (Amount Side
side UnsignedQuantity precision
absValue) =
  let value :: Quantity precision
value = forall p x. Refined p x -> x
unrefine UnsignedQuantity precision
absValue
   in case (AccountKind
k, Side
side) of
        (AccountKind
AccountKindAsset, Side
SideDebit) -> Quantity precision
value
        (AccountKind
AccountKindAsset, Side
SideCredit) -> -Quantity precision
value
        (AccountKind
AccountKindLiability, Side
SideDebit) -> -Quantity precision
value
        (AccountKind
AccountKindLiability, Side
SideCredit) -> Quantity precision
value
        (AccountKind
AccountKindEquity, Side
SideDebit) -> -Quantity precision
value
        (AccountKind
AccountKindEquity, Side
SideCredit) -> Quantity precision
value
        (AccountKind
AccountKindRevenue, Side
SideDebit) -> -Quantity precision
value
        (AccountKind
AccountKindRevenue, Side
SideCredit) -> Quantity precision
value
        (AccountKind
AccountKindExpense, Side
SideDebit) -> Quantity precision
value
        (AccountKind
AccountKindExpense, Side
SideCredit) -> -Quantity precision
value