1 /*	HomeBank -- Free, easy, personal accounting for everyone.
2  *	Copyright (C) 1995-2021 Maxime DOYEN
3  *
4  *	This file is part of HomeBank.
5  *
6  *	HomeBank is free software; you can redistribute it and/or modify
7  *	it under the terms of the GNU General Public License as published by
8  *	the Free Software Foundation; either version 2 of the License, or
9  *	(at your option) any later version.
10  *
11  *	HomeBank is distributed in the hope that it will be useful,
12  *	but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
14  *	GNU General Public License for more details.
15  *
16  *	You should have received a copy of the GNU General Public License
17  *	along with this program.	If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "homebank.h"
21 
22 #include "hb-import.h"
23 
24 #ifndef NOOFX
25 #include <libofx/libofx.h>
26 #endif
27 
28 
29 /****************************************************************************/
30 /* Debug macros                                                             */
31 /****************************************************************************/
32 #define MYDEBUG 0
33 
34 #if MYDEBUG
35 #define DB(x) (x);
36 #else
37 #define DB(x);
38 #endif
39 
40 /* our global datas */
41 extern struct HomeBank *GLOBALS;
42 extern struct Preferences *PREFS;
43 
44 
45 
46 #ifndef NOOFX
47 
48 /* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
49 
50 /**
51  * ofx_proc_account_cb:
52  *
53  * The ofx_proc_account_cb event is always generated first, to allow the application to create accounts
54  * or ask the user to match an existing account before the ofx_proc_statement and ofx_proc_transaction
55  * event are received. An OfxAccountData is passed to this event.
56  *
57  */
58 static LibofxProcStatementCallback
ofx_proc_account_cb(const struct OfxAccountData data,ImportContext * ctx)59 ofx_proc_account_cb(const struct OfxAccountData data, ImportContext *ctx)
60 {
61 GenAcc *genacc;
62 Account *dst_acc;
63 
64 	DB( g_print("** ofx_proc_account_cb()\n") );
65 
66 	if(data.account_id_valid==true)
67 	{
68 		DB( g_print("  account_id: %s\n", data.account_id) );
69 		DB( g_print("  account_name: %s\n", data.account_name) );
70 	}
71 
72 	//if(data.account_number_valid==true)
73 	//{
74 		DB( g_print("  account_number: %s\n", data.account_number) );
75 	//}
76 
77 
78 	if(data.account_type_valid==true)
79 	{
80 		DB( g_print("  account_type: %d\n", data.account_type) );
81 		/*
82 		enum:
83 		OFX_CHECKING 	A standard checking account
84 		OFX_SAVINGS 	A standard savings account
85 		OFX_MONEYMRKT 	A money market account
86 		OFX_CREDITLINE 	A line of credit
87 		OFX_CMA 	Cash Management Account
88 		OFX_CREDITCARD 	A credit card account
89 		OFX_INVESTMENT 	An investment account
90 		*/
91 	}
92 
93 	if(data.currency_valid==true)
94 	{
95 		DB( g_print("  currency: %s\n", data.currency) );
96 	}
97 
98 	//todo: normally should check for validity here
99 	// in every case we create an account here
100 	DB( g_print(" -> create generic account: '%s':'%s'\n", data.account_id, data.account_name) );
101 	genacc = hb_import_gen_acc_get_next (ctx, FILETYPE_OFX, (gchar *)data.account_name, (gchar *)data.account_id);
102 	ctx->curr_acc_isnew = TRUE;
103 
104 	dst_acc = hb_import_acc_find_existing((gchar *)data.account_name, (gchar *)data.account_id );
105 	if( dst_acc != NULL )
106 	{
107 		genacc->kacc = dst_acc->key;
108 		ctx->curr_acc_isnew = FALSE;
109 		if(dst_acc->type == ACC_TYPE_CREDITCARD)
110 			genacc->is_ccard = TRUE;
111 	}
112 
113 	ctx->curr_acc = genacc;
114 
115 	DB( fputs("\n",stdout) );
116 	return 0;
117 }
118 
119 
120 /**
121  * ofx_proc_statement_cb:
122  *
123  * The ofx_proc_statement_cb event is sent after all ofx_proc_transaction events have been sent.
124  * An OfxStatementData is passed to this event.
125  *
126  */
127 static LibofxProcStatementCallback
ofx_proc_statement_cb(const struct OfxStatementData data,ImportContext * ctx)128 ofx_proc_statement_cb(const struct OfxStatementData data, ImportContext *ctx)
129 {
130 	DB( g_print("** ofx_proc_statement_cb()\n") );
131 
132 #if MYDEBUG == 1
133 	if(data.ledger_balance_date_valid==true)
134 	{
135 	struct tm temp_tm;
136 
137 		temp_tm = *localtime(&(data.ledger_balance_date));
138 		g_print("ledger_balance_date : %d%s%d%s%d%s", temp_tm.tm_mday, "/", temp_tm.tm_mon+1, "/", temp_tm.tm_year+1900, "\n");
139 	}
140 #endif
141 
142 	if(data.ledger_balance_valid==true)
143 	{
144 		if( ctx->curr_acc != NULL && ctx->curr_acc_isnew == TRUE )
145 		{
146 			ctx->curr_acc->initial = data.ledger_balance;
147 		}
148 		DB( g_print("ledger_balance: $%.2f%s",data.ledger_balance,"\n") );
149 	}
150 
151 	return 0;
152 }
153 
154 /**
155  * ofx_proc_statement_cb:
156  *
157  * An ofx_proc_transaction_cb event is generated for every transaction in the ofx response,
158  * after ofx_proc_statement (and possibly ofx_proc_security is generated.
159  * An OfxTransactionData structure is passed to this event.
160  *
161  */
162 static LibofxProcStatementCallback
ofx_proc_transaction_cb(const struct OfxTransactionData data,ImportContext * ctx)163 ofx_proc_transaction_cb(const struct OfxTransactionData data, ImportContext *ctx)
164 {
165 struct tm *temp_tm;
166 GDate date;
167 GenTxn *gentxn;
168 
169 	DB( g_print("** ofx_proc_transaction_cb()\n") );
170 
171 	gentxn = da_gen_txn_malloc();
172 
173 // date
174 	gentxn->julian = 0;
175 	if(data.date_posted_valid && (data.date_posted != 0))
176 	{
177 		temp_tm = localtime(&data.date_posted);
178 		if( temp_tm != 0)
179 		{
180 			g_date_set_dmy(&date, temp_tm->tm_mday, temp_tm->tm_mon+1, temp_tm->tm_year+1900);
181 			gentxn->julian = g_date_get_julian(&date);
182 		}
183 	}
184 	else if (data.date_initiated_valid && (data.date_initiated != 0))
185 	{
186 		temp_tm = localtime(&data.date_initiated);
187 		if( temp_tm != 0)
188 		{
189 			g_date_set_dmy(&date, temp_tm->tm_mday, temp_tm->tm_mon+1, temp_tm->tm_year+1900);
190 			gentxn->julian = g_date_get_julian(&date);
191 		}
192 	}
193 
194 // amount
195 	if(data.amount_valid==true)
196 	{
197 		gentxn->amount = data.amount;
198 	}
199 
200 // 5.5.1 add fitid
201 	if(data.fi_id_valid==true)
202 	{
203 		gentxn->fitid = g_strdup(data.fi_id);
204 	}
205 
206 // check number :: The check number is most likely an integer and can probably be converted properly with atoi().
207 	//However the spec allows for up to 12 digits, so it is not garanteed to work
208 	if(data.check_number_valid==true)
209 	{
210 		gentxn->rawinfo = g_strdup(data.check_number);
211 	}
212 	//todo: reference_number ?Might present in addition to or instead of a check_number. Not necessarily a number
213 
214 // ofx:name = Can be the name of the payee or the description of the transaction
215 	if(data.name_valid==true)
216 	{
217 		gentxn->rawpayee = g_strdup(data.name);
218 	}
219 
220 //memo ( new for v4.2) #319202 Extra information not included in name
221 
222 	DB( g_print(" -> memo is='%d'\n", data.memo_valid) );
223 
224 
225 	if(data.memo_valid==true)
226 	{
227 		gentxn->rawmemo = g_strdup(data.memo);
228 	}
229 
230 // payment
231 	if(data.transactiontype_valid==true)
232 	{
233 		switch(data.transactiontype)
234 		{
235 			//#740373
236 			case OFX_CREDIT:
237 				if(gentxn->amount < 0)
238 					gentxn->amount *= -1;
239 				break;
240 			case OFX_DEBIT:
241 				if(gentxn->amount > 0)
242 					gentxn->amount *= -1;
243 				break;
244 			case OFX_INT:
245 					gentxn->paymode = PAYMODE_XFER;
246 				break;
247 			case OFX_DIV:
248 					gentxn->paymode = PAYMODE_XFER;
249 				break;
250 			case OFX_FEE:
251 					gentxn->paymode = PAYMODE_FEE;
252 				break;
253 			case OFX_SRVCHG:
254 					gentxn->paymode = PAYMODE_XFER;
255 				break;
256 			case OFX_DEP:
257 					gentxn->paymode = PAYMODE_DEPOSIT;
258 				break;
259 			case OFX_ATM:
260 					gentxn->paymode = PAYMODE_CASH;
261 				break;
262 			case OFX_POS:
263 				if(ctx->curr_acc && ctx->curr_acc->is_ccard == TRUE)
264 					gentxn->paymode = PAYMODE_CCARD;
265 				else
266 					gentxn->paymode = PAYMODE_DCARD;
267 				break;
268 			case OFX_XFER:
269 					gentxn->paymode = PAYMODE_XFER;
270 				break;
271 			case OFX_CHECK:
272 					gentxn->paymode = PAYMODE_CHECK;
273 				break;
274 			case OFX_PAYMENT:
275 					gentxn->paymode = PAYMODE_EPAYMENT;
276 				break;
277 			case OFX_CASH:
278 					gentxn->paymode = PAYMODE_CASH;
279 				break;
280 			case OFX_DIRECTDEP:
281 					gentxn->paymode = PAYMODE_DEPOSIT;
282 				break;
283 			case OFX_DIRECTDEBIT:
284 					//1854953: directdebit not adding in 4.6
285 					//gentxn->paymode = PAYMODE_XFER;
286 					gentxn->paymode = PAYMODE_DIRECTDEBIT;
287 				break;
288 			case OFX_REPEATPMT:
289 					gentxn->paymode = PAYMODE_REPEATPMT;
290 				break;
291 			case OFX_OTHER:
292 
293 				break;
294 			default :
295 
296 				break;
297 		}
298 	}
299 
300 	if( ctx->curr_acc )
301 	{
302 		gentxn->account = g_strdup(ctx->curr_acc->name);
303 
304 		#if MYDEBUG == 1
305 		if(gentxn->rawinfo)
306 			g_print(" len info %d %ld\n", (int)strlen(gentxn->rawinfo) , g_utf8_strlen(gentxn->rawinfo, -1));
307 		if(gentxn->rawmemo)
308 			g_print(" len memo %d %ld\n", (int)strlen(gentxn->rawmemo) , g_utf8_strlen(gentxn->rawmemo, -1));
309 		if(gentxn->rawpayee)
310 			g_print(" len name %d %ld\n", (int)strlen(gentxn->rawpayee), g_utf8_strlen(gentxn->rawpayee, -1));
311 		#endif
312 
313 		//#1842935 workaround for libofx truncate bug that can leave invalid UTF-8 string
314 		//NAME = A-32 (96 allowed)
315 		#if( (GLIB_MAJOR_VERSION == 2) && (GLIB_MINOR_VERSION >= 52) )
316 		if( gentxn->rawpayee && g_utf8_strlen(gentxn->rawpayee, -1) > 32 )
317 		{
318 		gchar *oldtxt = gentxn->rawpayee;
319 			DB( g_print(" ensure UTF-8 for truncated NAME='%s'\n", oldtxt) );
320 			gentxn->rawpayee = g_utf8_make_valid(oldtxt, -1);
321 			g_free(oldtxt);
322 		}
323 		#endif
324 		//TODO: maybe MEMO = A-255
325 
326 		/* ensure utf-8 here, has under windows, libofx not always return utf-8 as it should */
327 		#ifndef G_OS_UNIX
328 		DB( g_print(" ensure UTF-8\n") );
329 
330 		gentxn->rawinfo  = homebank_utf8_ensure(gentxn->rawinfo);
331 		gentxn->rawmemo  = homebank_utf8_ensure(gentxn->rawmemo);
332 		gentxn->rawpayee = homebank_utf8_ensure(gentxn->rawpayee);
333 		#endif
334 
335 		da_gen_txn_append(ctx, gentxn);
336 
337 		DB( g_print(" insert gentxn: acc=%s\n", gentxn->account) );
338 
339 		if( ctx->curr_acc_isnew == TRUE )
340 		{
341 			DB( g_print(" sub amount from initial\n") );
342 			ctx->curr_acc->initial -= data.amount;
343 		}
344 	}
345 	else
346 	{
347 		da_gen_txn_free(gentxn);
348 		DB( g_print(" no account, insert txn skipped\n") );
349 	}
350 
351 	return 0;
352 }
353 
354 
355 static LibofxProcStatusCallback
ofx_proc_status_cb(const struct OfxStatusData data,ImportContext * ctx)356 ofx_proc_status_cb(const struct OfxStatusData data, ImportContext *ctx)
357 {
358 	DB( g_print("** ofx_proc_status_cb()\n") );
359 
360    if(data.ofx_element_name_valid==true){
361      DB( g_print("    Ofx entity this status is relevent to: '%s'\n", data.ofx_element_name) );
362    }
363    if(data.severity_valid==true){
364      DB( g_print("    Severity: ") );
365      switch(data.severity){
366      case INFO : DB( g_print("INFO\n") );
367        break;
368      case WARN : DB( g_print("WARN\n") );
369        break;
370      case ERROR : DB( g_print("ERROR\n") );
371        break;
372      default: DB( g_print("WRITEME: Unknown status severity!\n") );
373      }
374    }
375    if(data.code_valid==true){
376      DB( g_print("    Code: %d, name: %s\n    Description: %s\n", data.code, data.name, data.description) );
377    }
378    if(data.server_message_valid==true){
379      DB( g_print("    Server Message: %s\n", data.server_message) );
380    }
381    DB( g_print("\n") );
382 
383 	return 0;
384 }
385 
386 
homebank_ofx_import(ImportContext * ictx,GenFile * genfile)387 GList *homebank_ofx_import(ImportContext *ictx, GenFile *genfile)
388 {
389 /*extern int ofx_PARSER_msg;
390 extern int ofx_DEBUG_msg;
391 extern int ofx_WARNING_msg;
392 extern int ofx_ERROR_msg;
393 extern int ofx_INFO_msg;
394 extern int ofx_STATUS_msg;*/
395 
396 	DB( g_print("\n[import] ofx import (libofx=%s) \n", LIBOFX_VERSION_RELEASE_STRING) );
397 
398 	/*ofx_PARSER_msg	= false;
399 	ofx_DEBUG_msg	= false;
400 	ofx_WARNING_msg = false;
401 	ofx_ERROR_msg	= false;
402 	ofx_INFO_msg	= false;
403 	ofx_STATUS_msg	= false;*/
404 
405 	LibofxContextPtr libofx_context = libofx_get_new_context();
406 
407 	ofx_set_status_cb     (libofx_context, (LibofxProcStatusCallback)     ofx_proc_status_cb     , ictx);
408 	ofx_set_statement_cb  (libofx_context, (LibofxProcStatementCallback)  ofx_proc_statement_cb  , ictx);
409 	ofx_set_account_cb    (libofx_context, (LibofxProcAccountCallback)    ofx_proc_account_cb    , ictx);
410 	ofx_set_transaction_cb(libofx_context, (LibofxProcTransactionCallback)ofx_proc_transaction_cb, ictx);
411 
412 #ifdef G_OS_WIN32
413 	//#932959: windows don't like utf8 path, so convert
414 	gchar *filepath = g_win32_locale_filename_from_utf8(genfile->filepath);
415 	libofx_proc_file(libofx_context, filepath, AUTODETECT);
416 	g_free(filepath);
417 #else
418 	libofx_proc_file(libofx_context, genfile->filepath, AUTODETECT);
419 #endif
420 
421 	libofx_free_context(libofx_context);
422 
423 	DB( g_print("ofx nb txn=%d\n", g_list_length(ictx->gen_lst_txn) ));
424 
425 	return ictx->gen_lst_txn;
426 }
427 
428 #endif
429