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

-- | This module provides definitions for balances used as in accounting.
module Haspara.Accounting.Balance where

import qualified Data.Aeson as Aeson
import qualified Data.Foldable as Foldable
import Data.Time (Day)
import GHC.Generics (Generic)
import GHC.TypeLits (KnownNat, Nat)
import Haspara.Accounting.Account (AccountKind (AccountKindAsset))
import Haspara.Accounting.Amount (Amount (Amount), quantityFromAmount, valueFromAmount)
import Haspara.Accounting.Inventory (Inventory, InventoryHistoryItem, updateInventoryVV)
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
  { forall (precision :: Nat). Balance precision -> Side
balanceSide :: !Side
  , forall (precision :: Nat). Balance precision -> Quantity precision
balanceValue :: !(Quantity precision)
  , forall (precision :: Nat).
Balance precision -> Inventory 8 12 precision
balanceInventory :: !(Inventory 8 12 precision)
  }
  deriving (Balance precision -> Balance precision -> Bool
(Balance precision -> Balance precision -> Bool)
-> (Balance precision -> Balance precision -> Bool)
-> Eq (Balance precision)
forall (precision :: Nat).
Balance precision -> Balance precision -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: forall (precision :: Nat).
Balance precision -> Balance precision -> Bool
== :: Balance precision -> Balance precision -> Bool
$c/= :: forall (precision :: Nat).
Balance precision -> Balance precision -> Bool
/= :: Balance precision -> Balance precision -> Bool
Eq, (forall x. Balance precision -> Rep (Balance precision) x)
-> (forall x. Rep (Balance precision) x -> Balance precision)
-> Generic (Balance precision)
forall (precision :: Nat) x.
Rep (Balance precision) x -> Balance precision
forall (precision :: Nat) x.
Balance precision -> Rep (Balance precision) x
forall x. Rep (Balance precision) x -> Balance precision
forall x. Balance precision -> Rep (Balance precision) x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall (precision :: Nat) x.
Balance precision -> Rep (Balance precision) x
from :: forall x. Balance precision -> Rep (Balance precision) x
$cto :: forall (precision :: Nat) x.
Rep (Balance precision) x -> Balance precision
to :: forall x. Rep (Balance precision) x -> Balance precision
Generic, Int -> Balance precision -> ShowS
[Balance precision] -> ShowS
Balance precision -> String
(Int -> Balance precision -> ShowS)
-> (Balance precision -> String)
-> ([Balance precision] -> ShowS)
-> Show (Balance precision)
forall (precision :: Nat).
KnownNat precision =>
Int -> Balance precision -> ShowS
forall (precision :: Nat).
KnownNat precision =>
[Balance precision] -> ShowS
forall (precision :: Nat).
KnownNat precision =>
Balance precision -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: forall (precision :: Nat).
KnownNat precision =>
Int -> Balance precision -> ShowS
showsPrec :: Int -> Balance precision -> ShowS
$cshow :: forall (precision :: Nat).
KnownNat precision =>
Balance precision -> String
show :: Balance precision -> String
$cshowList :: forall (precision :: Nat).
KnownNat precision =>
[Balance precision] -> ShowS
showList :: [Balance precision] -> ShowS
Show)


-- | 'Aeson.FromJSON' instance for 'Balance'.
--
-- For normal balances:
--
-- >>> :set -XDataKinds
-- >>> :set -XOverloadedStrings
-- >>> Aeson.eitherDecode "{\"side\": \"db\", \"value\": 42, \"inventory\": {\"current\": [], \"history\": [], \"total\": 0.0}}" :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideDebit, balanceValue = 42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}})
-- >>> Aeson.eitherDecode "{\"side\": \"cr\", \"value\": 42, \"inventory\": {\"current\": [], \"history\": [], \"total\": 0.0}}" :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideCredit, balanceValue = 42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}})
--
-- For negative balances:
--
-- >>> Aeson.eitherDecode "{\"side\": \"db\", \"value\": -42, \"inventory\": {\"current\": [], \"history\": [], \"total\": 0.0}}" :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideDebit, balanceValue = -42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}})
-- >>> Aeson.eitherDecode "{\"side\": \"cr\", \"value\": -42, \"inventory\": {\"current\": [], \"history\": [], \"total\": 0.0}}" :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideCredit, balanceValue = -42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}})
instance KnownNat precision => Aeson.FromJSON (Balance precision) where
  parseJSON :: Value -> Parser (Balance precision)
parseJSON = Options -> Value -> Parser (Balance precision)
forall a.
(Generic a, GFromJSON Zero (Rep a)) =>
Options -> Value -> Parser a
Aeson.genericParseJSON (Options -> Value -> Parser (Balance precision))
-> Options -> Value -> Parser (Balance precision)
forall a b. (a -> b) -> a -> b
$ String -> Options
commonAesonOptions String
"balance"


-- | 'Aeson.ToJSON' instance for 'Balance'.
--
-- For normal balances:
--
-- >>> :set -XDataKinds
-- >>> import Data.Default (def)
-- >>> import Haspara.Accounting.Side
-- >>> import Haspara.Quantity
-- >>> Aeson.encode (Balance SideDebit (mkQuantity 42 :: Quantity 2) def)
-- "{\"side\":\"db\",\"value\":42.0,\"inventory\":{\"total\":0.0,\"current\":[],\"history\":[]}}"
-- >>> Aeson.encode (Balance SideCredit (mkQuantity 42 :: Quantity 2) def)
-- "{\"side\":\"cr\",\"value\":42.0,\"inventory\":{\"total\":0.0,\"current\":[],\"history\":[]}}"
-- >>> Aeson.eitherDecode (Aeson.encode (Balance SideDebit (mkQuantity 42 :: Quantity 2) def)) :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideDebit, balanceValue = 42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}})
-- >>> Aeson.eitherDecode (Aeson.encode (Balance SideCredit (mkQuantity 42 :: Quantity 2) def)) :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideCredit, balanceValue = 42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}})
--
-- For negative balances:
--
-- >>> Aeson.encode (Balance SideDebit (mkQuantity (-42) :: Quantity 2) def)
-- "{\"side\":\"db\",\"value\":-42.0,\"inventory\":{\"total\":0.0,\"current\":[],\"history\":[]}}"
-- >>> Aeson.encode (Balance SideCredit (mkQuantity (-42) :: Quantity 2) def)
-- "{\"side\":\"cr\",\"value\":-42.0,\"inventory\":{\"total\":0.0,\"current\":[],\"history\":[]}}"
-- >>> Aeson.eitherDecode (Aeson.encode (Balance SideDebit (mkQuantity (-42) :: Quantity 2) def)) :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideDebit, balanceValue = -42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}})
-- >>> Aeson.eitherDecode (Aeson.encode (Balance SideCredit (mkQuantity (-42) :: Quantity 2) def)) :: Either String (Balance 2)
-- Right (Balance {balanceSide = SideCredit, balanceValue = -42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}})
instance KnownNat precision => Aeson.ToJSON (Balance precision) where
  toJSON :: Balance precision -> Value
toJSON = Options -> Balance precision -> Value
forall a.
(Generic a, GToJSON' Value Zero (Rep a)) =>
Options -> a -> Value
Aeson.genericToJSON (Options -> Balance precision -> Value)
-> Options -> Balance precision -> Value
forall a b. (a -> b) -> a -> b
$ String -> Options
commonAesonOptions String
"balance"
  toEncoding :: Balance precision -> Encoding
toEncoding = Options -> Balance precision -> Encoding
forall a.
(Generic a, GToJSON' Encoding Zero (Rep a)) =>
Options -> a -> Encoding
Aeson.genericToEncoding (Options -> Balance precision -> Encoding)
-> Options -> Balance precision -> Encoding
forall a b. (a -> b) -> a -> b
$ String -> Options
commonAesonOptions String
"balance"


-- | Returns the debit quantity, if any.
balanceDebit
  :: KnownNat precision
  => Balance precision
  -> Maybe (Quantity precision)
balanceDebit :: forall (precision :: Nat).
KnownNat precision =>
Balance precision -> Maybe (Quantity precision)
balanceDebit (Balance Side
SideDebit Quantity precision
v Inventory 8 12 precision
_) = Quantity precision -> Maybe (Quantity precision)
forall a. a -> Maybe a
Just Quantity precision
v
balanceDebit Balance precision
_ = Maybe (Quantity precision)
forall a. Maybe a
Nothing


-- | Returns the credit quantity, if any.
balanceCredit
  :: KnownNat precision
  => Balance precision
  -> Maybe (Quantity precision)
balanceCredit :: forall (precision :: Nat).
KnownNat precision =>
Balance precision -> Maybe (Quantity precision)
balanceCredit (Balance Side
SideCredit Quantity precision
v Inventory 8 12 precision
_) = Quantity precision -> Maybe (Quantity precision)
forall a. a -> Maybe a
Just Quantity precision
v
balanceCredit Balance precision
_ = Maybe (Quantity precision)
forall a. Maybe a
Nothing


-- | Updates the balance with the given amount.
--
-- >>> :set -XDataKinds
-- >>> import Data.Default (def)
-- >>> import Haspara.Accounting.Amount
-- >>> import Haspara.Accounting.Side
-- >>> import Refined.Unsafe
-- >>> let balance = Balance SideDebit 42 def :: Balance 2
-- >>> balance
-- Balance {balanceSide = SideDebit, balanceValue = 42.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}}
-- >>> 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, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}}
-- >>> updateBalance balance amountCredit
-- Balance {balanceSide = SideDebit, balanceValue = 32.00, balanceInventory = MkInventory {inventoryTotal = 0.000000000000, inventoryCurrent = fromList [], inventoryHistory = fromList []}}
updateBalance
  :: KnownNat precision
  => Balance precision
  -> Amount precision
  -> Balance precision
updateBalance :: forall (precision :: Nat).
KnownNat precision =>
Balance precision -> Amount precision -> Balance precision
updateBalance (Balance Side
bSide Quantity precision
bVal Inventory 8 12 precision
inventory) (Amount Side
aSide UnsignedQuantity precision
aVal) =
  Side
-> Quantity precision
-> Inventory 8 12 precision
-> Balance precision
forall (precision :: Nat).
Side
-> Quantity precision
-> Inventory 8 12 precision
-> Balance precision
Balance Side
bSide (Quantity precision
bVal Quantity precision -> Quantity precision -> Quantity precision
forall a. Num a => a -> a -> a
+ (UnsignedQuantity precision -> Quantity precision
forall {k} (p :: k) x. Refined p x -> x
unrefine UnsignedQuantity precision
aVal Quantity precision -> Quantity precision -> Quantity precision
forall a. Num a => a -> a -> a
* (if Side
bSide Side -> Side -> Bool
forall a. Eq a => a -> a -> Bool
== Side
aSide then Quantity precision
1 else (-Quantity precision
1)))) Inventory 8 12 precision
inventory


-- | Updates the balance with additional inventory event.
updateBalanceWithInventory
  :: KnownNat precision
  => Day
  -> Balance precision
  -> Amount precision
  -> Quantity 12
  -> ([InventoryHistoryItem 8 12 precision], Balance precision)
updateBalanceWithInventory :: forall (precision :: Nat).
KnownNat precision =>
Day
-> Balance precision
-> Amount precision
-> Quantity 12
-> ([InventoryHistoryItem 8 12 precision], Balance precision)
updateBalanceWithInventory Day
date Balance precision
balance Amount precision
amount Quantity 12
quantity =
  let nb :: Balance precision
nb = Balance precision -> Amount precision -> Balance precision
forall (precision :: Nat).
KnownNat precision =>
Balance precision -> Amount precision -> Balance precision
updateBalance Balance precision
balance Amount precision
amount
      vv :: Quantity precision
vv = Quantity precision -> Quantity precision
forall a. Num a => a -> a
abs (AccountKind -> Amount precision -> Quantity precision
forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Amount precision -> Quantity precision
valueFromAmount AccountKind
AccountKindAsset Amount precision
amount)
      (Seq (InventoryHistoryItem 8 12 precision)
is, Inventory 8 12 precision
ni) = Day
-> Quantity precision
-> Quantity 12
-> Inventory 8 12 precision
-> (Seq (InventoryHistoryItem 8 12 precision),
    Inventory 8 12 precision)
forall (pprec :: Nat) (sprec :: Nat) (vprec :: Nat).
(KnownNat pprec, KnownNat sprec, KnownNat vprec) =>
Day
-> Quantity vprec
-> Quantity sprec
-> Inventory pprec sprec vprec
-> (Seq (InventoryHistoryItem pprec sprec vprec),
    Inventory pprec sprec vprec)
updateInventoryVV Day
date Quantity precision
vv Quantity 12
quantity (Balance precision -> Inventory 8 12 precision
forall (precision :: Nat).
Balance precision -> Inventory 8 12 precision
balanceInventory Balance precision
nb)
   in (Seq (InventoryHistoryItem 8 12 precision)
-> [InventoryHistoryItem 8 12 precision]
forall a. Seq a -> [a]
forall (t :: * -> *) a. Foldable t => t a -> [a]
Foldable.toList Seq (InventoryHistoryItem 8 12 precision)
is, Balance precision
nb {balanceInventory = ni})


-- | Converts the balance to amount.
--
-- >>> :set -XDataKinds
-- >>> import Data.Default (def)
-- >>> import Haspara.Accounting.Side
-- >>> amountFromBalance (Balance SideDebit 42 def :: Balance 2)
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
-- >>> amountFromBalance (Balance SideDebit (-42) def :: Balance 2)
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
-- >>> amountFromBalance (Balance SideCredit 42 def :: Balance 2)
-- Amount {amountSide = SideCredit, amountValue = Refined 42.00}
-- >>> amountFromBalance (Balance SideCredit (-42) def :: Balance 2)
-- Amount {amountSide = SideDebit, amountValue = Refined 42.00}
amountFromBalance
  :: KnownNat precision
  => Balance precision
  -> Amount precision
amountFromBalance :: forall (precision :: Nat).
KnownNat precision =>
Balance precision -> Amount precision
amountFromBalance (Balance Side
side Quantity precision
value Inventory 8 12 precision
_) =
  Side -> UnsignedQuantity precision -> Amount precision
forall (precision :: Nat).
Side -> UnsignedQuantity precision -> Amount precision
Amount (if Quantity precision
value Quantity precision -> Quantity precision -> Bool
forall a. Ord a => a -> a -> Bool
< Quantity precision
0 then Side -> Side
otherSide Side
side else Side
side) (Quantity precision -> UnsignedQuantity precision
forall (s :: Nat). KnownNat s => Quantity s -> UnsignedQuantity s
absQuantity Quantity precision
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 :: forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Balance precision -> Quantity precision
quantityFromBalance AccountKind
k = AccountKind -> Amount precision -> Quantity precision
forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Amount precision -> Quantity precision
quantityFromAmount AccountKind
k (Amount precision -> Quantity precision)
-> (Balance precision -> Amount precision)
-> Balance precision
-> Quantity precision
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Balance precision -> Amount precision
forall (precision :: Nat).
KnownNat precision =>
Balance precision -> Amount precision
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 :: forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Balance precision -> Quantity precision
valueFromBalance AccountKind
k = AccountKind -> Amount precision -> Quantity precision
forall (precision :: Nat).
KnownNat precision =>
AccountKind -> Amount precision -> Quantity precision
valueFromAmount AccountKind
k (Amount precision -> Quantity precision)
-> (Balance precision -> Amount precision)
-> Balance precision
-> Quantity precision
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Balance precision -> Amount precision
forall (precision :: Nat).
KnownNat precision =>
Balance precision -> Amount precision
amountFromBalance