summaryrefslogtreecommitdiffstats
path: root/lib/account/account.ml
diff options
context:
space:
mode:
Diffstat (limited to 'lib/account/account.ml')
-rw-r--r--lib/account/account.ml121
1 files changed, 121 insertions, 0 deletions
diff --git a/lib/account/account.ml b/lib/account/account.ml
new file mode 100644
index 0000000..0ef3d28
--- /dev/null
+++ b/lib/account/account.ml
@@ -0,0 +1,121 @@
1open Prelude
2
3(* TODO: Decide on public interface. Probably should not include
4 functions such as [unsafe_update_bal], but having [Balanced_batch]
5 under [Account] also feels a bit awkward. *)
6
7module Path = struct
8 type t = string list [@@deriving compare, sexp]
9end
10
11(** Ensures that only accounts with valid type hierarchies can be constructed.
12*)
13module Kernel : sig
14 type extra = { description : string }
15
16 type t = private { type_ : Type.t; extra : extra; core : core }
17
18 and core =
19 (* Balance in some commodity *)
20 | Leaf of Money.Commodity_id.t * Money.Diff.t
21 | Subtree of t String.Map.t
22
23 val make : Type.t -> extra -> core -> t option
24end = struct
25 type extra = { description : string }
26
27 type t = { type_ : Type.t; extra : extra; core : core }
28
29 and core =
30 (* Balance in some commodity *)
31 | Leaf of Money.Commodity_id.t * Money.Diff.t
32 | Subtree of t String.Map.t
33
34 let make type_ extra : core -> t option = function
35 | Leaf (comm, bal) -> Some { type_; extra; core = Leaf (comm, bal) }
36 | Subtree children ->
37 if
38 Map.for_all children ~f:(fun subacc ->
39 Type.is_super subacc.type_ ~super:type_ ~strict:false)
40 then Some { type_; extra; core = Subtree children }
41 else None
42end
43
44type t = Kernel.t
45
46type update_bal_error =
47 | Empty_path
48 | Unmatching_commodity_id of { in_account : Money.Commodity_id.t }
49 | Not_a_leaf_account
50 | Not_a_subtree_account
51 | Not_found (* TODO: report at which level *)
52
53(* We do not necessarily expect [aid] to be a valid path, as we
54 always do for paths in the type hierarchy. The difference is that
55 the type hierarchy is fixed, while the account hierarchy can
56 change over the course of a year. *)
57let rec unsafe_update_bal_aux (aid : Path.t) (dc : Money.Debit_credit.t)
58 (by_amount : Money.Amount.t) (in_comm : Money.Commodity_id.t) (w : t) :
59 (t, update_bal_error) result =
60 match (aid, w.core) with
61 | [], Leaf (comm, bal) ->
62 if [%equal: Money.Commodity_id.t] in_comm comm then (* slay! *)
63 let core' =
64 Kernel.Leaf
65 ( comm,
66 Money.Diff.(bal + of_amount by_amount dc (Type.polarity w.type_))
67 )
68 in
69 Ok (Option.value_exn (Kernel.make w.type_ w.extra core'))
70 else (* bruh *)
71 Error (Unmatching_commodity_id { in_account = comm })
72 | [], Subtree _ -> Error Not_a_leaf_account
73 | _ :: _, Leaf _ -> Error Not_a_subtree_account
74 | aid0 :: aid', Subtree subaccs -> (
75 match Map.find subaccs aid0 with
76 | None -> Error Not_found
77 | Some subacc ->
78 let open Result.Let_syntax in
79 let%bind subacc' =
80 (* TODO: when reporting at which level Not_found fails,
81 we want to make sure that we extend the information
82 in the error with the current aid0 (so we recover a
83 full path to where the account is missing *)
84 unsafe_update_bal_aux aid' dc by_amount in_comm subacc
85 in
86 let core' =
87 Kernel.Subtree (Map.set subaccs ~key:aid0 ~data:subacc')
88 in
89 Ok (Option.value_exn (Kernel.make w.type_ w.extra core')))
90
91(* Unfortunate but true, there has to be some kind of a root account
92 with no type :) *)
93type root = t String.Map.t
94
95let unsafe_update_bal (aid : Path.t) (dc : Money.Debit_credit.t)
96 (by_amount : Money.Amount.t) (in_comm : Money.Commodity_id.t) (w : root) :
97 (root, update_bal_error) result =
98 match aid with
99 | [] -> Error Empty_path
100 | aid0 :: aid' -> (
101 match Map.find w aid0 with
102 | None -> Error Not_found
103 | Some subacc ->
104 let open Result.Let_syntax in
105 (* TODO: when reporting at which level Not_found fails,
106 we want to make sure that we extend the information
107 in the error with the current aid0 (so we recover a
108 full path to where the account is missing *)
109 let%bind subacc' =
110 unsafe_update_bal_aux aid' dc by_amount in_comm subacc
111 in
112 Ok (Map.set w ~key:aid0 ~data:subacc'))
113
114module Balanced_batch_acc_paths = Balanced_batch.Make (Path)
115
116let apply_balanced_batch (b : Balanced_batch_acc_paths.t) (w : root) =
117 Map.fold_result (Balanced_batch_acc_paths.entries b) ~init:w
118 ~f:(fun ~key:aid ~(data : Balanced_batch_acc_paths.entry) w ->
119 let open Result.Let_syntax in
120 let%bind w = unsafe_update_bal aid data.dc data.amount data.commodity w in
121 Ok w)