packages feed

haspara 0.0.0.0 → 0.0.0.1

raw patch · 10 files changed

+468/−6 lines, 10 filesdep +deriving-aesondep ~refineddep ~unordered-containersPVP: minor bump suggested

API additions: PVP suggests at least a minor version bump

Dependencies added: deriving-aeson

Dependency ranges changed: refined, unordered-containers

API changes (from Hackage documentation)

+ Haspara.Accounting: Account :: !AccountKind -> !o -> Account o
+ Haspara.Accounting: AccountKindAsset :: AccountKind
+ Haspara.Accounting: AccountKindEquity :: AccountKind
+ Haspara.Accounting: AccountKindExpense :: AccountKind
+ Haspara.Accounting: AccountKindLiability :: AccountKind
+ Haspara.Accounting: AccountKindRevenue :: AccountKind
+ Haspara.Accounting: EntryCredit :: Date -> o -> UnsignedQuantity s -> Entry o (s :: Nat)
+ Haspara.Accounting: EntryDebit :: Date -> o -> UnsignedQuantity s -> Entry o (s :: Nat)
+ Haspara.Accounting: EventDecrement :: Date -> o -> UnsignedQuantity s -> Event o (s :: Nat)
+ Haspara.Accounting: EventIncrement :: Date -> o -> UnsignedQuantity s -> Event o (s :: Nat)
+ Haspara.Accounting: Ledger :: !Account a -> !Quantity s -> !Quantity s -> ![LedgerItem o s] -> Ledger a o (s :: Nat)
+ Haspara.Accounting: LedgerItem :: !Entry o s -> !Quantity s -> LedgerItem o (s :: Nat)
+ Haspara.Accounting: Posting :: NonEmpty (Event o s, Account a) -> Posting a o (s :: Nat)
+ Haspara.Accounting: [accountKind] :: Account o -> !AccountKind
+ Haspara.Accounting: [accountObject] :: Account o -> !o
+ Haspara.Accounting: [ledgerAccount] :: Ledger a o (s :: Nat) -> !Account a
+ Haspara.Accounting: [ledgerClosing] :: Ledger a o (s :: Nat) -> !Quantity s
+ Haspara.Accounting: [ledgerItemBalance] :: LedgerItem o (s :: Nat) -> !Quantity s
+ Haspara.Accounting: [ledgerItemEntry] :: LedgerItem o (s :: Nat) -> !Entry o s
+ Haspara.Accounting: [ledgerOpening] :: Ledger a o (s :: Nat) -> !Quantity s
+ Haspara.Accounting: [ledgerRunning] :: Ledger a o (s :: Nat) -> ![LedgerItem o s]
+ Haspara.Accounting: accountKindText :: AccountKind -> Text
+ Haspara.Accounting: addEntry :: KnownNat s => Ledger a o s -> Entry o s -> Ledger a o s
+ Haspara.Accounting: buildEntry :: KnownNat s => Event o s -> AccountKind -> Entry o s
+ Haspara.Accounting: data Account o
+ Haspara.Accounting: data AccountKind
+ Haspara.Accounting: data Entry o (s :: Nat)
+ Haspara.Accounting: data Event o (s :: Nat)
+ Haspara.Accounting: data Ledger a o (s :: Nat)
+ Haspara.Accounting: data LedgerItem o (s :: Nat)
+ Haspara.Accounting: entryCredit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
+ Haspara.Accounting: entryDate :: KnownNat s => Entry o s -> Date
+ Haspara.Accounting: entryDebit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
+ Haspara.Accounting: entryObject :: KnownNat s => Entry o s -> o
+ Haspara.Accounting: entryQuantity :: KnownNat s => Entry o s -> Quantity s
+ Haspara.Accounting: eventDate :: KnownNat s => Event o s -> Date
+ Haspara.Accounting: eventObject :: KnownNat s => Event o s -> o
+ Haspara.Accounting: mkEvent :: (MonadError String m, KnownNat s) => Date -> o -> Quantity s -> m (Event o s)
+ Haspara.Accounting: mkLedger :: KnownNat s => Account a -> Quantity s -> [Entry o s] -> Ledger a o s
+ Haspara.Accounting: negateEvent :: KnownNat s => Event o s -> Event o s
+ Haspara.Accounting: newtype Posting a o (s :: Nat)
+ Haspara.Accounting: post :: KnownNat s => Posting a o s -> [(Account a, Entry o s)]
+ Haspara.Accounting: postingEvents :: KnownNat s => Posting a o s -> [o]
+ Haspara.Accounting: type UnsignedQuantity s = Refined NonNegative (Quantity s)
+ Haspara.Accounting.Account: Account :: !AccountKind -> !o -> Account o
+ Haspara.Accounting.Account: [accountKind] :: Account o -> !AccountKind
+ Haspara.Accounting.Account: [accountObject] :: Account o -> !o
+ Haspara.Accounting.Account: data Account o
+ Haspara.Accounting.Account: instance Data.Aeson.Types.FromJSON.FromJSON o => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance Data.Aeson.Types.ToJSON.ToJSON o => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance Data.Hashable.Class.Hashable o => Data.Hashable.Class.Hashable (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance GHC.Classes.Ord o => GHC.Classes.Ord (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance GHC.Generics.Generic (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.Account: instance GHC.Show.Show o => GHC.Show.Show (Haspara.Accounting.Account.Account o)
+ Haspara.Accounting.AccountKind: AccountKindAsset :: AccountKind
+ Haspara.Accounting.AccountKind: AccountKindEquity :: AccountKind
+ Haspara.Accounting.AccountKind: AccountKindExpense :: AccountKind
+ Haspara.Accounting.AccountKind: AccountKindLiability :: AccountKind
+ Haspara.Accounting.AccountKind: AccountKindRevenue :: AccountKind
+ Haspara.Accounting.AccountKind: accountKindText :: AccountKind -> Text
+ Haspara.Accounting.AccountKind: data AccountKind
+ Haspara.Accounting.AccountKind: instance Data.Aeson.Types.FromJSON.FromJSON Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance Data.Aeson.Types.ToJSON.ToJSON Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance Data.Hashable.Class.Hashable Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Classes.Eq Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Classes.Ord Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Enum.Enum Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Generics.Generic Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.AccountKind: instance GHC.Show.Show Haspara.Accounting.AccountKind.AccountKind
+ Haspara.Accounting.Entry: EntryCredit :: Date -> o -> UnsignedQuantity s -> Entry o (s :: Nat)
+ Haspara.Accounting.Entry: EntryDebit :: Date -> o -> UnsignedQuantity s -> Entry o (s :: Nat)
+ Haspara.Accounting.Entry: buildEntry :: KnownNat s => Event o s -> AccountKind -> Entry o s
+ Haspara.Accounting.Entry: data Entry o (s :: Nat)
+ Haspara.Accounting.Entry: entryCredit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
+ Haspara.Accounting.Entry: entryDate :: KnownNat s => Entry o s -> Date
+ Haspara.Accounting.Entry: entryDebit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)
+ Haspara.Accounting.Entry: entryObject :: KnownNat s => Entry o s -> o
+ Haspara.Accounting.Entry: entryQuantity :: KnownNat s => Entry o s -> Quantity s
+ Haspara.Accounting.Entry: instance (Data.Aeson.Types.FromJSON.FromJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Entry: instance (Data.Aeson.Types.ToJSON.ToJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Entry: instance (GHC.Show.Show o, GHC.TypeNats.KnownNat s) => GHC.Show.Show (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Entry: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Entry: instance GHC.Classes.Ord o => GHC.Classes.Ord (Haspara.Accounting.Entry.Entry o s)
+ Haspara.Accounting.Event: EventDecrement :: Date -> o -> UnsignedQuantity s -> Event o (s :: Nat)
+ Haspara.Accounting.Event: EventIncrement :: Date -> o -> UnsignedQuantity s -> Event o (s :: Nat)
+ Haspara.Accounting.Event: data Event o (s :: Nat)
+ Haspara.Accounting.Event: eventDate :: KnownNat s => Event o s -> Date
+ Haspara.Accounting.Event: eventObject :: KnownNat s => Event o s -> o
+ Haspara.Accounting.Event: instance (Data.Aeson.Types.FromJSON.FromJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: instance (Data.Aeson.Types.ToJSON.ToJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: instance (GHC.Show.Show o, GHC.TypeNats.KnownNat s) => GHC.Show.Show (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: instance GHC.Classes.Ord o => GHC.Classes.Ord (Haspara.Accounting.Event.Event o s)
+ Haspara.Accounting.Event: mkEvent :: (MonadError String m, KnownNat s) => Date -> o -> Quantity s -> m (Event o s)
+ Haspara.Accounting.Event: negateEvent :: KnownNat s => Event o s -> Event o s
+ Haspara.Accounting.Ledger: Ledger :: !Account a -> !Quantity s -> !Quantity s -> ![LedgerItem o s] -> Ledger a o (s :: Nat)
+ Haspara.Accounting.Ledger: LedgerItem :: !Entry o s -> !Quantity s -> LedgerItem o (s :: Nat)
+ Haspara.Accounting.Ledger: [ledgerAccount] :: Ledger a o (s :: Nat) -> !Account a
+ Haspara.Accounting.Ledger: [ledgerClosing] :: Ledger a o (s :: Nat) -> !Quantity s
+ Haspara.Accounting.Ledger: [ledgerItemBalance] :: LedgerItem o (s :: Nat) -> !Quantity s
+ Haspara.Accounting.Ledger: [ledgerItemEntry] :: LedgerItem o (s :: Nat) -> !Entry o s
+ Haspara.Accounting.Ledger: [ledgerOpening] :: Ledger a o (s :: Nat) -> !Quantity s
+ Haspara.Accounting.Ledger: [ledgerRunning] :: Ledger a o (s :: Nat) -> ![LedgerItem o s]
+ Haspara.Accounting.Ledger: addEntry :: KnownNat s => Ledger a o s -> Entry o s -> Ledger a o s
+ Haspara.Accounting.Ledger: data Ledger a o (s :: Nat)
+ Haspara.Accounting.Ledger: data LedgerItem o (s :: Nat)
+ Haspara.Accounting.Ledger: instance (Data.Aeson.Types.FromJSON.FromJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance (Data.Aeson.Types.ToJSON.ToJSON o, Data.Aeson.Types.ToJSON.ToJSON a, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance (GHC.Classes.Eq a, GHC.Classes.Eq o) => GHC.Classes.Eq (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance (GHC.Classes.Ord a, GHC.Classes.Ord o) => GHC.Classes.Ord (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance (GHC.Show.Show o, GHC.TypeNats.KnownNat s) => GHC.Show.Show (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance (GHC.TypeNats.KnownNat s, Data.Aeson.Types.FromJSON.FromJSON a, Data.Aeson.Types.FromJSON.FromJSON o) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance (GHC.TypeNats.KnownNat s, Data.Aeson.Types.ToJSON.ToJSON o) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance (GHC.TypeNats.KnownNat s, GHC.Show.Show a, GHC.Show.Show o) => GHC.Show.Show (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance GHC.Classes.Ord o => GHC.Classes.Ord (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: instance GHC.Generics.Generic (Haspara.Accounting.Ledger.Ledger a o s)
+ Haspara.Accounting.Ledger: instance GHC.Generics.Generic (Haspara.Accounting.Ledger.LedgerItem o s)
+ Haspara.Accounting.Ledger: mkLedger :: KnownNat s => Account a -> Quantity s -> [Entry o s] -> Ledger a o s
+ Haspara.Accounting.Posting: Posting :: NonEmpty (Event o s, Account a) -> Posting a o (s :: Nat)
+ Haspara.Accounting.Posting: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSON o, GHC.TypeNats.KnownNat s) => Data.Aeson.Types.ToJSON.ToJSON (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance (GHC.Classes.Eq o, GHC.Classes.Eq a) => GHC.Classes.Eq (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance (GHC.Classes.Ord o, GHC.Classes.Ord a) => GHC.Classes.Ord (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance (GHC.TypeNats.KnownNat s, Data.Aeson.Types.FromJSON.FromJSON o, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance (GHC.TypeNats.KnownNat s, GHC.Show.Show o, GHC.Show.Show a) => GHC.Show.Show (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: instance GHC.Generics.Generic (Haspara.Accounting.Posting.Posting a o s)
+ Haspara.Accounting.Posting: newtype Posting a o (s :: Nat)
+ Haspara.Accounting.Posting: post :: KnownNat s => Posting a o s -> [(Account a, Entry o s)]
+ Haspara.Accounting.Posting: postingEvents :: KnownNat s => Posting a o s -> [o]
+ Haspara.Accounting.Types: type UnsignedQuantity s = Refined NonNegative (Quantity s)

Files

CHANGELOG.md view
@@ -2,6 +2,20 @@ ## [Unreleased]  +<a name="0.0.0.1"></a>+## [0.0.0.1] - 2022-03-01+### Chore+- bump development version to 0.0.0.1+- **deps:** upgrade to lts-18.27++### Feat+- add rudimentary accounting functionality++### Pull Requests+- Merge pull request [#4](https://github.com/telostat/haspara/issues/4) from telostat/upgrade-stack-lts+- Merge pull request [#3](https://github.com/telostat/haspara/issues/3) from telostat/rudimentary-accounting++ <a name="0.0.0.0"></a> ## 0.0.0.0 - 2022-03-01 ### Chore@@ -13,6 +27,7 @@ - init codebase - **deps:** upgrade to lts-18.17 - **docs:** update README+- **release:** 0.0.0.0  ### Feat - add Aeson instances to Money data type@@ -22,4 +37,5 @@ - Merge pull request [#1](https://github.com/telostat/haspara/issues/1) from telostat/init  -[Unreleased]: https://github.com/telostat/haspara/compare/0.0.0.0...HEAD+[Unreleased]: https://github.com/telostat/haspara/compare/0.0.0.1...HEAD+[0.0.0.1]: https://github.com/telostat/haspara/compare/0.0.0.0...0.0.0.1
haspara.cabal view
@@ -1,6 +1,6 @@ cabal-version:      1.12 name:               haspara-version:            0.0.0.0+version:            0.0.0.1 license:            MIT license-file:       LICENSE copyright:          Copyright (c) 2021-2022 Telostat Pte Ltd@@ -27,6 +27,14 @@ library     exposed-modules:         Haspara+        Haspara.Accounting+        Haspara.Accounting.Account+        Haspara.Accounting.AccountKind+        Haspara.Accounting.Entry+        Haspara.Accounting.Event+        Haspara.Accounting.Ledger+        Haspara.Accounting.Posting+        Haspara.Accounting.Types         Haspara.Currency         Haspara.Date         Haspara.FXQuote@@ -50,16 +58,17 @@     build-depends:         aeson >=1.5.6.0 && <1.6,         base >=4.11 && <5,+        deriving-aeson >=0.2.8 && <0.3,         hashable >=1.3.0.0 && <1.4,         megaparsec >=9.0.1 && <9.1,         mtl >=2.2.2 && <2.3,-        refined >=0.6.2 && <0.7,+        refined >=0.6.3 && <0.7,         safe-decimal >=0.2.1.0 && <0.3,         scientific >=0.3.7.0 && <0.4,         template-haskell >=2.16.0.0 && <2.17,         text >=1.2.4.1 && <1.3,         time >=1.9.3 && <1.10,-        unordered-containers >=0.2.15.0 && <0.3+        unordered-containers >=0.2.16.0 && <0.3  test-suite haspara-doctest     type:             exitcode-stdio-1.0@@ -71,15 +80,16 @@     build-depends:         aeson >=1.5.6.0 && <1.6,         base >=4.11 && <5,+        deriving-aeson >=0.2.8 && <0.3,         doctest ==0.17.*,         hashable >=1.3.0.0 && <1.4,         haspara -any,         megaparsec >=9.0.1 && <9.1,         mtl >=2.2.2 && <2.3,-        refined >=0.6.2 && <0.7,+        refined >=0.6.3 && <0.7,         safe-decimal >=0.2.1.0 && <0.3,         scientific >=0.3.7.0 && <0.4,         template-haskell >=2.16.0.0 && <2.17,         text >=1.2.4.1 && <1.3,         time >=1.9.3 && <1.10,-        unordered-containers >=0.2.15.0 && <0.3+        unordered-containers >=0.2.16.0 && <0.3
+ src/Haspara/Accounting.hs view
@@ -0,0 +1,42 @@+module Haspara.Accounting+  ( Account(..)+  , AccountKind(..)+  , accountKindText+  , Entry(..)+  , buildEntry+  , Event(..)+  , eventDate+  , eventObject+  , negateEvent+  , mkEvent+  , Posting(..)+  , postingEvents+  , post+  , UnsignedQuantity+  , Ledger(..)+  , LedgerItem(..)+  , mkLedger+  , addEntry+  , entryDate+  , entryObject+  , entryQuantity+  , entryDebit+  , entryCredit+  ) where+++import Haspara.Accounting.Account     (Account(..))+import Haspara.Accounting.AccountKind (AccountKind(..), accountKindText)+import Haspara.Accounting.Entry+       ( Entry(..)+       , buildEntry+       , entryCredit+       , entryDate+       , entryDebit+       , entryObject+       , entryQuantity+       )+import Haspara.Accounting.Event       (Event(..), eventDate, eventObject, mkEvent, negateEvent)+import Haspara.Accounting.Ledger      (Ledger(..), LedgerItem(..), addEntry, mkLedger)+import Haspara.Accounting.Posting     (Posting(..), post, postingEvents)+import Haspara.Accounting.Types       (UnsignedQuantity)
+ src/Haspara/Accounting/Account.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE DataKinds     #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia   #-}++module Haspara.Accounting.Account where++import Data.Hashable                  (Hashable)+import Deriving.Aeson                 (CustomJSON(CustomJSON), FromJSON, Generic, ToJSON)+import Deriving.Aeson.Stock           (PrefixedSnake)+import Haspara.Accounting.AccountKind (AccountKind)+++-- | Account model.+--+-- >>> import Haspara.Accounting.AccountKind (AccountKind(..))+-- >>> import qualified Data.Aeson as Aeson+-- >>> let acc = Account AccountKindAsset (1 ::Int)+-- >>> Aeson.encode acc+-- "{\"kind\":\"ASSET\",\"object\":1}"+-- >>> Aeson.decode (Aeson.encode acc) :: Maybe (Account Int)+-- Just (Account {accountKind = AccountKindAsset, accountObject = 1})+-- >>> Aeson.decode (Aeson.encode acc) == Just acc+-- True+data Account o = Account+  { accountKind   :: !AccountKind+  , accountObject :: !o+  } deriving (Eq, Generic, Ord, Show)+  deriving (FromJSON, ToJSON)+  via PrefixedSnake "account" (Account o)+++instance Hashable o => Hashable (Account o)
+ src/Haspara/Accounting/AccountKind.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE DeriveGeneric #-}++module Haspara.Accounting.AccountKind where++import qualified Data.Aeson    as Aeson+import qualified Data.Char     as C+import           Data.Hashable (Hashable)+import qualified Data.Text     as T+import           GHC.Generics  (Generic)+++data AccountKind =+    AccountKindAsset+  | AccountKindLiability+  | AccountKindEquity+  | AccountKindRevenue+  | AccountKindExpense+  deriving (Enum, Eq, Generic, Ord, Show)+++instance Hashable AccountKind+++-- | 'Aeson.FromJSON' instance for 'AccountKind'.+--+-- >>> Aeson.decode "\"Asset\"" :: Maybe AccountKind+-- Just AccountKindAsset+-- >>> Aeson.decode "\"aSSET\"" :: Maybe AccountKind+-- Just AccountKindAsset+-- >>> Aeson.decode "\"ASSET\"" :: Maybe AccountKind+-- Just AccountKindAsset+-- >>> Aeson.decode "\"LIABILITY\"" :: Maybe AccountKind+-- Just AccountKindLiability+-- >>> Aeson.decode "\"EQUITY\"" :: Maybe AccountKind+-- Just AccountKindEquity+-- >>> Aeson.decode "\"REVENUE\"" :: Maybe AccountKind+-- Just AccountKindRevenue+-- >>> Aeson.decode "\"EXPENSE\"" :: Maybe AccountKind+-- Just AccountKindExpense+instance Aeson.FromJSON AccountKind where+  parseJSON = Aeson.withText "AccountKind" $ \t -> case T.map C.toUpper t of+    "ASSET"     -> pure AccountKindAsset+    "LIABILITY" -> pure AccountKindLiability+    "EQUITY"    -> pure AccountKindEquity+    "REVENUE"   -> pure AccountKindRevenue+    "EXPENSE"   -> pure AccountKindExpense+    _           -> fail $ "Unknown account kind: " <> show t+++-- | 'Aeson.ToJSON' instance for 'AccountKind'.+--+-- >>> Aeson.encode AccountKindAsset+-- "\"ASSET\""+-- >>> Aeson.encode AccountKindLiability+-- "\"LIABILITY\""+-- >>> Aeson.encode AccountKindEquity+-- "\"EQUITY\""+-- >>> Aeson.encode AccountKindRevenue+-- "\"REVENUE\""+-- >>> Aeson.encode AccountKindExpense+-- "\"EXPENSE\""+instance Aeson.ToJSON AccountKind where+  toJSON AccountKindAsset     = Aeson.String "ASSET"+  toJSON AccountKindLiability = Aeson.String "LIABILITY"+  toJSON AccountKindEquity    = Aeson.String "EQUITY"+  toJSON AccountKindRevenue   = Aeson.String "REVENUE"+  toJSON AccountKindExpense   = Aeson.String "EXPENSE"+++accountKindText :: AccountKind -> T.Text+accountKindText AccountKindAsset     = "Asset"+accountKindText AccountKindLiability = "Liability"+accountKindText AccountKindEquity    = "Equity"+accountKindText AccountKindRevenue   = "Revenue"+accountKindText AccountKindExpense   = "Expense"
+ src/Haspara/Accounting/Entry.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE DataKinds      #-}+{-# LANGUAGE KindSignatures #-}++module Haspara.Accounting.Entry where++import           Data.Aeson                     ((.:), (.=))+import qualified Data.Aeson                     as Aeson+import qualified Data.Char                      as C+import qualified Data.Text                      as T+import           GHC.TypeLits                   (KnownNat, Nat)+import qualified Haspara                        as H+import           Haspara.Accounting.AccountKind (AccountKind(..))+import           Haspara.Accounting.Event       (Event(..))+import           Haspara.Accounting.Types       (UnsignedQuantity)+import           Refined                        (unrefine)+++-- | 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,\"obj\":1,\"date\":\"2021-01-01\",\"type\":\"DEBIT\"}"+-- >>> 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 H.Date o (UnsignedQuantity s)+  | EntryCredit H.Date 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]+++entryDate :: KnownNat s => Entry o s -> H.Date+entryDate (EntryDebit d _ _)  = d+entryDate (EntryCredit d _ _) = d+++entryQuantity :: KnownNat s => Entry o s -> H.Quantity s+entryQuantity (EntryDebit _ _ q)  = unrefine q+entryQuantity (EntryCredit _ _ q) = -(unrefine q)+++entryObject :: KnownNat s => Entry o s -> o+entryObject (EntryDebit _ o _)  = o+entryObject (EntryCredit _ o _) = o+++entryDebit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)+entryDebit (EntryDebit _ _ x) = Just x+entryDebit EntryCredit {}     = Nothing+++entryCredit :: KnownNat s => Entry o s -> Maybe (UnsignedQuantity s)+entryCredit EntryDebit {}       = Nothing+entryCredit (EntryCredit _ _ x) = Just x+++-- |+--+-- +-----------------------+----------+----------++-- | 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
+ src/Haspara/Accounting/Event.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE DataKinds        #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE KindSignatures   #-}++module Haspara.Accounting.Event where++import           Control.Monad.Except     (MonadError(throwError))+import           Data.Aeson               ((.:), (.=))+import qualified Data.Aeson               as Aeson+import qualified Data.Char                as C+import qualified Data.Text                as T+import           GHC.TypeLits             (KnownNat, Nat)+import qualified Haspara                  as H+import           Haspara.Accounting.Types (UnsignedQuantity)+import           Refined                  (refine)+++-- | Encoding of an increment/decrement event.+--+-- >>> :set -XDataKinds+-- >>> import Refined+-- >>> let date = read "2021-01-01"+-- >>> let oid = 1 :: Int+-- >>> let qty = $$(refineTH 42) :: UnsignedQuantity 2+-- >>> let event = EventDecrement date oid qty+-- >>> let json = Aeson.encode event+-- >>> json+-- "{\"qty\":42.0,\"obj\":1,\"date\":\"2021-01-01\",\"type\":\"DECREMENT\"}"+-- >>> Aeson.decode json :: Maybe (Event Int 2)+-- Just (EventDecrement 2021-01-01 1 (Refined 42.00))+-- >>> Aeson.decode json == Just event+-- True+data Event o (s :: Nat) =+    EventDecrement H.Date o (UnsignedQuantity s)+  | EventIncrement H.Date o (UnsignedQuantity s)+  deriving (Eq, Ord, Show)+++instance (Aeson.FromJSON o, KnownNat s) => Aeson.FromJSON (Event o s) where+  parseJSON = Aeson.withObject "Event" $ \o -> do+    dorc <- o .: "type"+    cons <- case T.map C.toUpper dorc of+      "DECREMENT" -> pure EventDecrement+      "INCREMENT" -> pure EventIncrement+      x           -> fail ("Unknown event 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 (Event o s) where+  toJSON x = case x of+    EventDecrement d o q -> Aeson.object ["type" .= ("DECREMENT" :: T.Text), "date" .= d, "obj" .= o, "qty" .= q]+    EventIncrement d o q -> Aeson.object ["type" .= ("INCREMENT" :: T.Text), "date" .= d, "obj" .= o, "qty" .= q]+++eventDate :: (KnownNat s) => Event o s -> H.Date+eventDate (EventDecrement d _ _) = d+eventDate (EventIncrement d _ _) = d+++eventObject :: (KnownNat s) => Event o s -> o+eventObject (EventDecrement _ o _) = o+eventObject (EventIncrement _ o _) = o+++negateEvent :: (KnownNat s) => Event o s -> Event o s+negateEvent (EventDecrement d o x) = EventIncrement d o x+negateEvent (EventIncrement d o x) = EventDecrement d o x+++mkEvent :: (MonadError String m, KnownNat s) => H.Date -> o -> H.Quantity s -> m (Event o s)+mkEvent d o x+  | x < 0     = either (throwError . show) pure $ EventDecrement d o <$> refine (abs x)+  | otherwise = either (throwError . show) pure $ EventIncrement d o <$> refine (abs x)
+ src/Haspara/Accounting/Ledger.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DataKinds        #-}+{-# LANGUAGE DeriveGeneric    #-}+{-# LANGUAGE DerivingVia      #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE KindSignatures   #-}++module Haspara.Accounting.Ledger where++import Deriving.Aeson             (CustomJSON(CustomJSON), FromJSON, Generic, ToJSON)+import Deriving.Aeson.Stock       (PrefixedSnake)+import GHC.TypeLits               (KnownNat, Nat)+import Haspara                    (Quantity)+import Haspara.Accounting.Account (Account)+import Haspara.Accounting.Entry   (Entry(..), entryQuantity)+++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)+++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)+++mkLedger :: KnownNat s => Account a -> Quantity s -> [Entry o s] -> Ledger a o s+mkLedger a o = foldl addEntry (Ledger a o o [])+++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
+ src/Haspara/Accounting/Posting.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE DataKinds       #-}+{-# LANGUAGE DeriveGeneric   #-}+{-# LANGUAGE DerivingVia     #-}+{-# LANGUAGE KindSignatures  #-}+{-# LANGUAGE TemplateHaskell #-}++module Haspara.Accounting.Posting where++import qualified Data.List.NonEmpty         as NE+import           Deriving.Aeson             (CustomJSON(CustomJSON), FromJSON, Generic, ToJSON)+import           Deriving.Aeson.Stock       (Vanilla)+import           GHC.TypeLits               (KnownNat, Nat)+import           Haspara.Accounting.Account (Account(accountKind))+import           Haspara.Accounting.Entry   (Entry, buildEntry)+import           Haspara.Accounting.Event   (Event, eventObject)+++-- | 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,\"obj\":1,\"date\":\"2021-01-01\",\"type\":\"DECREMENT\"},{\"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)+++postingEvents :: (KnownNat s) => Posting a o s -> [o]+postingEvents (Posting es)  = eventObject . fst <$> NE.toList es+++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
+ src/Haspara/Accounting/Types.hs view
@@ -0,0 +1,7 @@+module Haspara.Accounting.Types where++import Haspara.Quantity (Quantity)+import Refined          (NonNegative, Refined)+++type UnsignedQuantity s = Refined NonNegative (Quantity s)