summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRutger Broekhoff2025-08-25 23:39:51 +0200
committerRutger Broekhoff2025-08-25 23:39:51 +0200
commitb8fbaa53b912347b3b50cac3e913a142db460b0a (patch)
tree4563ebc8b04a4ad841ad2103d8ddc0698e844a45
parent3f5221c2da2a19cf5de05284821e9b854d31b7fb (diff)
downloadrdcapsis-b8fbaa53b912347b3b50cac3e913a142db460b0a.tar.gz
rdcapsis-b8fbaa53b912347b3b50cac3e913a142db460b0a.zip
Conversion
-rw-r--r--lib/convert.ml164
-rw-r--r--lib/ingcsv.ml71
-rw-r--r--lib/ledger.ml32
3 files changed, 245 insertions, 22 deletions
diff --git a/lib/convert.ml b/lib/convert.ml
new file mode 100644
index 0000000..cbdb90f
--- /dev/null
+++ b/lib/convert.ml
@@ -0,0 +1,164 @@
1open Core
2open Ledger
3
4let virt_checking_acc = [ "Unfiled"; "Checking" ]
5let virt_savings_acc = [ "Unfiled"; "Savings" ]
6let virt_counterparty = [ "Unfiled"; "Counterparty" ]
7
8(* TODO: clean up *)
9type convert_err = Nonpositive_amount | Other of Tx.error
10
11let cents n = Amount (Money.of_z n)
12
13let tx_from_current_acc euc_id (Ingcsv.Tx (base, spec)) =
14 if Z.(lt base.amount ~$0) then Error Nonpositive_amount
15 else
16 Result.map_error ~f:(fun e -> Other e)
17 @@
18 match spec with
19 | Payment_terminal_payment details ->
20 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
21 ~credit:
22 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
23 ~debit:
24 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
25 ~labels:
26 Labels.(
27 empty
28 |> add (Iban_label Account_tag) base.account
29 |> add (String_label Counterparty_name_tag)
30 details.counterparty_name
31 |> add (String_label Card_seq_no_tag) details.card_sequence_no
32 |> add (String_label Terminal_tag) details.terminal
33 |> add (String_label Transaction_tag) details.transaction
34 |> add Timestamp_label details.timestamp
35 |>
36 if details.google_pay then add (Unit_label Google_pay_tag) ()
37 else Fn.id)
38 | Payment_terminal_cashback details ->
39 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
40 ~debit:
41 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
42 ~credit:
43 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
44 ~labels:
45 Labels.(
46 empty
47 |> add (Iban_label Account_tag) base.account
48 |> add (String_label Counterparty_name_tag)
49 details.counterparty_name
50 |> add (String_label Card_seq_no_tag) details.card_sequence_no
51 |> add (String_label Terminal_tag) details.terminal
52 |> add (String_label Transaction_tag) details.transaction
53 |> add Timestamp_label details.timestamp)
54 | Online_banking_credit details ->
55 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
56 ~debit:
57 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
58 ~credit:
59 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
60 ~labels:
61 Labels.(
62 empty
63 |> add (Iban_label Account_tag) base.account
64 |> add (String_label Counterparty_name_tag)
65 details.counterparty_name
66 |> add (Iban_label Counterparty_iban_tag)
67 details.counterparty_iban
68 |> add (String_label Desc_tag) details.description
69 |> add Timestamp_label details.timestamp)
70 | Online_banking_debit details ->
71 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
72 ~debit:
73 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
74 ~credit:
75 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
76 ~labels:
77 Labels.(
78 empty
79 |> add (Iban_label Account_tag) base.account
80 |> add (String_label Counterparty_name_tag)
81 details.counterparty_name
82 |> add (Iban_label Counterparty_iban_tag)
83 details.counterparty_iban
84 |> add (String_label Desc_tag) details.description)
85 | Recurrent_direct_debit details ->
86 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
87 ~debit:
88 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
89 ~credit:
90 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
91 ~labels:
92 Labels.(
93 empty
94 |> add (Iban_label Account_tag) base.account
95 |> add (Iban_label Counterparty_iban_tag)
96 details.counterparty_iban
97 |> add (String_label Counterparty_name_tag)
98 details.counterparty_name
99 |> add (String_label Desc_tag) details.description
100 |> add (String_label Reference_tag) details.reference
101 |> add (String_label Mandate_id_tag) details.mandate_id
102 |> add (String_label Creditor_id_tag) details.creditor_id
103 |>
104 match details.other_party with
105 | None -> Fn.id
106 | Some other_party ->
107 add (String_label Other_party_tag) other_party)
108 | Rounding_savings_deposit details ->
109 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
110 ~debit:
111 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
112 ~credit:
113 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
114 ~labels:
115 Labels.(
116 empty
117 |> add (Unit_label Auto_round_savings_tag) ()
118 |> add (String_label Savings_account_tag) details.savings_account)
119 | Deposit details ->
120 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
121 ~debit:
122 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
123 ~credit:
124 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
125 ~labels:
126 Labels.(
127 empty
128 |> add (Iban_label Counterparty_iban_tag)
129 details.counterparty_iban
130 |> add (String_label Counterparty_name_tag)
131 details.counterparty_name
132 |> add (String_label Desc_tag) details.description
133 |> add (String_label Reference_tag) details.reference)
134 | Ideal_debit details ->
135 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
136 ~debit:
137 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
138 ~credit:
139 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
140 ~labels:
141 Labels.(
142 empty
143 |> add (Iban_label Counterparty_iban_tag)
144 details.counterparty_iban
145 |> add (String_label Counterparty_name_tag)
146 details.counterparty_name
147 |> add (String_label Desc_tag) details.description
148 |> add (String_label Reference_tag) details.reference
149 |> add Timestamp_label details.timestamp)
150 | Batch_payment details ->
151 Tx.make ~cleared:(Some base.date) ~commodity_id:euc_id
152 ~debit:
153 (Account_id_map.singleton virt_counterparty @@ cents base.amount)
154 ~credit:
155 (Account_id_map.singleton virt_checking_acc @@ cents base.amount)
156 ~labels:
157 Labels.(
158 empty
159 |> add (Iban_label Counterparty_iban_tag)
160 details.counterparty_iban
161 |> add (String_label Counterparty_name_tag)
162 details.counterparty_name
163 |> add (String_label Desc_tag) details.description
164 |> add (String_label Reference_tag) details.reference)
diff --git a/lib/ingcsv.ml b/lib/ingcsv.ml
index 203a353..f3536bf 100644
--- a/lib/ingcsv.ml
+++ b/lib/ingcsv.ml
@@ -68,6 +68,8 @@ module Transaction_type = struct
68end 68end
69 69
70module Primitive_tx = struct 70module Primitive_tx = struct
71 exception Inconsistent_transaction_code
72
71 type t = { 73 type t = {
72 date : Date.t; 74 date : Date.t;
73 description : string; 75 description : string;
@@ -85,6 +87,21 @@ module Primitive_tx = struct
85 let opt_field (f : string -> 'a) (v : string) : 'a option = 87 let opt_field (f : string -> 'a) (v : string) : 'a option =
86 if String.is_empty (String.strip v) then None else Some (f v) 88 if String.is_empty (String.strip v) then None else Some (f v)
87 89
90 let headers =
91 [
92 "Date";
93 "Name / Description";
94 "Account";
95 "Counterparty";
96 "Code";
97 "Debit/credit";
98 "Amount (EUR)";
99 "Transaction type";
100 "Notifications";
101 "Resulting balance";
102 "Tag";
103 ]
104
88 let parse : t Delimited.Read.t = 105 let parse : t Delimited.Read.t =
89 let open Delimited.Read.Let_syntax in 106 let open Delimited.Read.Let_syntax in
90 let%map_open date = at_header "Date" ~f:Date.of_string 107 let%map_open date = at_header "Date" ~f:Date.of_string
@@ -99,12 +116,7 @@ module Primitive_tx = struct
99 and resulting_balance = at_header "Resulting balance" ~f:Cents.of_string 116 and resulting_balance = at_header "Resulting balance" ~f:Cents.of_string
100 and tag = at_header "Tag" ~f:Fn.id in 117 and tag = at_header "Tag" ~f:Fn.id in
101 if not ([%equal: Transaction_type.t] code type_) then 118 if not ([%equal: Transaction_type.t] code type_) then
102 Printf.failwithf 119 raise Inconsistent_transaction_code;
103 "Primitive_tx.parse: parsed transaction code (%S) and type (%S) do not \
104 match"
105 (Transaction_type.to_string code)
106 (Transaction_type.to_string type_)
107 ();
108 { 120 {
109 date; 121 date;
110 description; 122 description;
@@ -123,7 +135,7 @@ type tx_base = {
123 date : Date.t; 135 date : Date.t;
124 account : Iban.t; 136 account : Iban.t;
125 amount : Cents.t; 137 amount : Cents.t;
126 res_bal : Cents.t; 138 resulting_balance : Cents.t;
127 tag : string; 139 tag : string;
128} 140}
129 141
@@ -196,6 +208,7 @@ type parse_err =
196 | Inconsistent_value_date 208 | Inconsistent_value_date
197 | Inconsistent_counterparty_name 209 | Inconsistent_counterparty_name
198 | Inconsistent_counterparty_iban 210 | Inconsistent_counterparty_iban
211 | Inconsistent_transaction_code
199 212
200let assert_value_date (ptx : Primitive_tx.t) d = 213let assert_value_date (ptx : Primitive_tx.t) d =
201 if Date.(d = ptx.date) then Ok () else Error Inconsistent_value_date 214 if Date.(d = ptx.date) then Ok () else Error Inconsistent_value_date
@@ -383,7 +396,7 @@ let parse_batch_payment_credit_notifs notifs =
383 (name, desc, iban, ref_, val_date) 396 (name, desc, iban, ref_, val_date)
384 | _ | (exception _) -> Error No_notifs_match 397 | _ | (exception _) -> Error No_notifs_match
385 398
386let specifics_from_prim_exn (ams_tz : Time_ns.Zone.t) : 399let tx_specifics_from_prim (ams_tz : Time_ns.Zone.t) :
387 Primitive_tx.t -> (tx_specifics, parse_err) result = function 400 Primitive_tx.t -> (tx_specifics, parse_err) result = function
388 | { type_ = Payment_terminal; debit_credit = Debit; _ } as ptx -> 401 | { type_ = Payment_terminal; debit_credit = Debit; _ } as ptx ->
389 let%bind 402 let%bind
@@ -531,3 +544,45 @@ let specifics_from_prim_exn (ams_tz : Time_ns.Zone.t) :
531 reference = ref_; 544 reference = ref_;
532 } 545 }
533 | _ -> Error Unknown_type_combination 546 | _ -> Error Unknown_type_combination
547
548let tx_base_from_prim (ptx : Primitive_tx.t) : tx_base =
549 {
550 date = ptx.date;
551 account = ptx.account;
552 amount = ptx.amount;
553 resulting_balance = ptx.resulting_balance;
554 tag = ptx.tag;
555 }
556
557let tx_from_prim ptx ~ams_tz : (tx, parse_err) result =
558 let base = tx_base_from_prim ptx in
559 let%map specifics = tx_specifics_from_prim ams_tz ptx in
560 Tx (base, specifics)
561
562type csv_err = Parse_err of parse_err | Exn of exn
563
564module List = struct
565 include List
566
567 let map_result ~(f : 'a -> ('b, 'c) result) : 'a list -> ('b list, 'c) result
568 =
569 let open Result.Let_syntax in
570 let rec go = function
571 | [] -> return []
572 | x :: xs ->
573 let%map x' = f x and xs' = go xs in
574 x' :: xs'
575 in
576 go
577end
578
579let read_channel (c : In_channel.t) ~ams_tz : (tx list, csv_err) result =
580 let%bind ptxs =
581 try
582 Ok
583 (Delimited.Read.read_lines ~header:(`Require Primitive_tx.headers)
584 Primitive_tx.parse c)
585 with e -> Error (Exn e)
586 in
587 List.map_result ptxs ~f:(tx_from_prim ~ams_tz)
588 |> Result.map_error ~f:(fun e -> Parse_err e)
diff --git a/lib/ledger.ml b/lib/ledger.ml
index 1d9a63c..3b52bcc 100644
--- a/lib/ledger.ml
+++ b/lib/ledger.ml
@@ -16,7 +16,7 @@ type tx_type =
16 16
17type iban_tag = Account_tag | Counterparty_iban_tag [@@deriving compare] 17type iban_tag = Account_tag | Counterparty_iban_tag [@@deriving compare]
18 18
19type unit_tag = Filed_tag | GooglePay_tag | AutoRoundSavings_tag 19type unit_tag = Filed_tag | Google_pay_tag | Auto_round_savings_tag
20[@@deriving compare] 20[@@deriving compare]
21 21
22type string_tag = 22type string_tag =
@@ -81,22 +81,26 @@ end = struct
81end 81end
82 82
83type scalar = Amount of Money.t | Rate of Z.t [@@deriving equal, compare] 83type scalar = Amount of Money.t | Rate of Z.t [@@deriving equal, compare]
84type account_id = string list
85type commodity_id = string (* TODO: consider making this UUID *) 84type commodity_id = string (* TODO: consider making this UUID *)
86 85
86module Account_id = struct
87 type t = string list [@@deriving sexp, compare]
88end
89
87type account = { 90type account = {
88 id : account_id; 91 id : Account_id.t;
89 description : string list; 92 description : string list;
90 commodity_id : commodity_id; 93 commodity_id : commodity_id;
91 balance : Money.t; 94 balance : Money.t;
92} 95}
93 96
94type bal_assert = { account : account_id; amount : Money.t; labels : Labels.t } 97type bal_assert = {
98 account : Account_id.t;
99 amount : Money.t;
100 labels : Labels.t;
101}
95 102
96module Account_id_key = struct 103module Account_id_map = Map.Make (Account_id)
97 type t = account_id
98 type comparator_witness
99end
100 104
101module Tx : sig 105module Tx : sig
102 type t 106 type t
@@ -105,23 +109,23 @@ module Tx : sig
105 val make : 109 val make :
106 cleared:Date.t option -> 110 cleared:Date.t option ->
107 commodity_id:commodity_id -> 111 commodity_id:commodity_id ->
108 debit:scalar Map.M(Account_id_key).t -> 112 debit:scalar Account_id_map.t ->
109 credit:scalar Map.M(Account_id_key).t -> 113 credit:scalar Account_id_map.t ->
110 labels:Labels.t -> 114 labels:Labels.t ->
111 (t, error) result 115 (t, error) result
112 116
113 val cleared : t -> Date.t option 117 val cleared : t -> Date.t option
114 val commodity_id : t -> commodity_id 118 val commodity_id : t -> commodity_id
115 val debit : t -> scalar Map.M(Account_id_key).t 119 val debit : t -> scalar Account_id_map.t
116 val credit : t -> scalar Map.M(Account_id_key).t 120 val credit : t -> scalar Account_id_map.t
117 val labels : t -> Labels.t 121 val labels : t -> Labels.t
118end = struct 122end = struct
119 (* We hide this because we only want to allow constructing balanced transactions *) 123 (* We hide this because we only want to allow constructing balanced transactions *)
120 type t = { 124 type t = {
121 cleared : Date.t option; 125 cleared : Date.t option;
122 commodity_id : commodity_id; 126 commodity_id : commodity_id;
123 debit : scalar Map.M(Account_id_key).t; 127 debit : scalar Account_id_map.t;
124 credit : scalar Map.M(Account_id_key).t; 128 credit : scalar Account_id_map.t;
125 labels : Labels.t; 129 labels : Labels.t;
126 } 130 }
127 [@@deriving fields] 131 [@@deriving fields]