diff options
Diffstat (limited to 'lib/ingcsv.ml')
-rw-r--r-- | lib/ingcsv.ml | 71 |
1 files changed, 63 insertions, 8 deletions
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 | |||
68 | end | 68 | end |
69 | 69 | ||
70 | module Primitive_tx = struct | 70 | module 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 | ||
200 | let assert_value_date (ptx : Primitive_tx.t) d = | 213 | let 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 | ||
386 | let specifics_from_prim_exn (ams_tz : Time_ns.Zone.t) : | 399 | let 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 | |||
548 | let 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 | |||
557 | let 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 | |||
562 | type csv_err = Parse_err of parse_err | Exn of exn | ||
563 | |||
564 | module 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 | ||
577 | end | ||
578 | |||
579 | let 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) | ||