/* * Copyright (c) 2025 Aldrik Ramaekers * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #define CPPHTTPLIB_OPENSSL_SUPPORT #include "httplib.h" #include "logger.hpp" #include "strops.hpp" #include "memops.hpp" #include "locales.hpp" #include "importer.hpp" #include "file_templates.hpp" #include "administration_reader.hpp" extern importer::ai_provider_impl _chatgpt_api_provider; extern importer::ai_provider_impl _deepseek_api_provider; importer::ai_provider_impl importer::get_ai_provider_implementation(ai_provider provider) { switch(provider) { case AI_PROVIDER_OPENAI: return _chatgpt_api_provider; //case AI_PROVIDER_DEEPSEEK: return _deepseek_api_provider; default: assert(0); break; } return importer::ai_provider_impl {0}; } static void _batch_query_response_handler(invoice* buffer, char* json) { int alloc_size = 1000; char* rb = (char*)memops::alloc(alloc_size); strops::get_json_value(json, "query_id", rb, alloc_size); if (strops::equals(rb, "-1")) return; // ignore else if (strops::equals(rb, "1")) { strops::get_json_value(json, "sequential_number", rb, alloc_size); strops::copy(buffer->sequential_number, rb, sizeof(buffer->sequential_number)); } else if (strops::equals(rb, "2")) { strops::get_json_value(json, "issued_at", rb, alloc_size); buffer->issued_at = strtoll(rb, NULL, 10); } else if (strops::equals(rb, "3")) { strops::get_json_value(json, "expires_at", rb, alloc_size); buffer->expires_at = strtoll(rb, NULL, 10); } else if (strops::equals(rb, "4")) { strops::get_json_value(json, "currency_code", rb, alloc_size); administration::invoice_set_currency(buffer, rb); } else if (strops::equals(rb, "5")) { strops::get_json_value(json, "address1", rb, alloc_size); strops::copy(buffer->supplier.address.address1, rb, sizeof(buffer->supplier.address.address1)); strops::get_json_value(json, "address2", rb, alloc_size); strops::copy(buffer->supplier.address.address2, rb, sizeof(buffer->supplier.address.address2)); strops::get_json_value(json, "city", rb, alloc_size); strops::copy(buffer->supplier.address.city, rb, sizeof(buffer->supplier.address.city)); strops::get_json_value(json, "postal", rb, alloc_size); strops::copy(buffer->supplier.address.postal, rb, sizeof(buffer->supplier.address.postal)); strops::get_json_value(json, "region", rb, alloc_size); strops::copy(buffer->supplier.address.region, rb, sizeof(buffer->supplier.address.region)); strops::get_json_value(json, "country_code", rb, alloc_size); strops::copy(buffer->supplier.address.country_code, rb, sizeof(buffer->supplier.address.country_code)); strops::get_json_value(json, "is_business", rb, alloc_size); buffer->supplier.type = (strops::equals(rb, "true")) ? contact_type::CONTACT_BUSINESS : contact_type::CONTACT_CONSUMER; strops::get_json_value(json, "name", rb, alloc_size); strops::copy(buffer->supplier.name, rb, sizeof(buffer->supplier.name)); strops::get_json_value(json, "taxid", rb, alloc_size); strops::copy(buffer->supplier.taxid, rb, sizeof(buffer->supplier.taxid)); strops::get_json_value(json, "businessid", rb, alloc_size); strops::copy(buffer->supplier.businessid, rb, sizeof(buffer->supplier.businessid)); strops::get_json_value(json, "email", rb, alloc_size); strops::copy(buffer->supplier.email, rb, sizeof(buffer->supplier.email)); strops::get_json_value(json, "phone_number", rb, alloc_size); strops::copy(buffer->supplier.phone_number, rb, sizeof(buffer->supplier.phone_number)); strops::get_json_value(json, "bank_account", rb, alloc_size); strops::copy(buffer->supplier.bank_account, rb, sizeof(buffer->supplier.bank_account)); } else if (strops::equals(rb, "6")) { strops::get_json_value(json, "address1", rb, alloc_size); strops::copy(buffer->customer.address.address1, rb, sizeof(buffer->customer.address.address1)); strops::get_json_value(json, "address2", rb, alloc_size); strops::copy(buffer->customer.address.address2, rb, sizeof(buffer->customer.address.address2)); strops::get_json_value(json, "city", rb, alloc_size); strops::copy(buffer->customer.address.city, rb, sizeof(buffer->customer.address.city)); strops::get_json_value(json, "postal", rb, alloc_size); strops::copy(buffer->customer.address.postal, rb, sizeof(buffer->customer.address.postal)); strops::get_json_value(json, "region", rb, alloc_size); strops::copy(buffer->customer.address.region, rb, sizeof(buffer->customer.address.region)); strops::get_json_value(json, "country_code", rb, alloc_size); strops::copy(buffer->customer.address.country_code, rb, sizeof(buffer->customer.address.country_code)); strops::get_json_value(json, "is_business", rb, alloc_size); buffer->customer.type = (strops::equals(rb, "true")) ? contact_type::CONTACT_BUSINESS : contact_type::CONTACT_CONSUMER; strops::get_json_value(json, "name", rb, alloc_size); strops::copy(buffer->customer.name, rb, sizeof(buffer->customer.name)); strops::get_json_value(json, "taxid", rb, alloc_size); strops::copy(buffer->customer.taxid, rb, sizeof(buffer->customer.taxid)); strops::get_json_value(json, "businessid", rb, alloc_size); strops::copy(buffer->customer.businessid, rb, sizeof(buffer->customer.businessid)); strops::get_json_value(json, "email", rb, alloc_size); strops::copy(buffer->customer.email, rb, sizeof(buffer->customer.email)); strops::get_json_value(json, "phone_number", rb, alloc_size); strops::copy(buffer->customer.phone_number, rb, sizeof(buffer->customer.phone_number)); strops::get_json_value(json, "bank_account", rb, alloc_size); strops::copy(buffer->customer.bank_account, rb, sizeof(buffer->customer.bank_account)); } else if (strops::equals(rb, "7")) { strops::get_json_value(json, "address1", rb, alloc_size); strops::copy(buffer->addressee.address.address1, rb, sizeof(buffer->addressee.address.address1)); strops::get_json_value(json, "address2", rb, alloc_size); strops::copy(buffer->addressee.address.address2, rb, sizeof(buffer->addressee.address.address2)); strops::get_json_value(json, "city", rb, alloc_size); strops::copy(buffer->addressee.address.city, rb, sizeof(buffer->addressee.address.city)); strops::get_json_value(json, "postal", rb, alloc_size); strops::copy(buffer->addressee.address.postal, rb, sizeof(buffer->addressee.address.postal)); strops::get_json_value(json, "region", rb, alloc_size); strops::copy(buffer->addressee.address.region, rb, sizeof(buffer->addressee.address.region)); strops::get_json_value(json, "country_code", rb, alloc_size); strops::copy(buffer->addressee.address.country_code, rb, sizeof(buffer->addressee.address.country_code)); strops::get_json_value(json, "name", rb, alloc_size); strops::copy(buffer->addressee.name, rb, sizeof(buffer->addressee.name)); } else if (strops::equals(rb, "8")) { strops::get_json_value(json, "item_count", rb, alloc_size); u32 item_count = strtol(rb, NULL, 10); for (u32 i = 0; i < item_count; i++) { billing_item item = administration::billing_item_create_empty(); strops::get_json_value(json, "amount", rb, alloc_size, i); item.amount = strtof(rb, NULL); strops::get_json_value(json, "amount_is_percentage", rb, alloc_size, i); item.amount_is_percentage = strops::equals(rb, "true"); strops::get_json_value(json, "description", rb, alloc_size, i); strops::copy(item.description, rb, sizeof(item.description)); strops::get_json_value(json, "price_per_item", rb, alloc_size, i); item.net_per_item = strtof(rb, NULL); strops::get_json_value(json, "discount", rb, alloc_size, i); item.discount = strtof(rb, NULL); strops::get_json_value(json, "discount_is_percentage", rb, alloc_size, i); item.discount_is_percentage = strops::equals(rb, "true"); administration::billing_item_add_to_invoice(buffer, item); } } memops::unalloc(rb); return; } static int _ai_document_to_invoice_t(void *arg) { importer::invoice_request* request = (importer::invoice_request*)arg; char* file_path = request->file_path; importer::ai_provider_impl impl = importer::get_ai_provider_implementation(administration::get_ai_service().provider); request->status = importer::status::IMPORT_UPLOADING_FILE; char file_id[100]; if (!impl.upload_file(file_path, file_id, 100)) { request->status = importer::status::IMPORT_DONE; request->error = I_ERR_FAILED_UPLOAD; return 0; } request->status = importer::status::IMPORT_WAITING_FOR_RESPONSE; char* queries[] = { "What is the invoice number/ID? Return json containing sequential_number (string), query_id = 1 (string)", "When was this invoice issued? Return json containing issued_at (time_t value), query_id = 2 (string). If not found, issued_at = 0", "When does this invoice expire? Return json containing expires_at (time_t value), query_id = 3 (string). If not found, expires_at = 0", "What currency is this invoice issued in? Look for a currency symbol in one of the billed items. Return json containing currency_code (3 letter code string), query_id = 4 (string)", "Who sent the invoice? This information might be under the section 'From, 'Supplier', 'Seller', 'Sold by' or something similar. Return json containing query_id = 5 (string), " " address1 (string, address line 1), address2 (string, address line 2), " " city (string), postal (string), region (string), country_code (string, 2 letter code), is_business (string, 'true' or 'false'), " " name (string), taxid (string, tax identifier number), businessid (string, business identifier number), email (string), " " phone_number (string), bank_account (string)", "Who received the invoice? This information might be under the section 'Customer', 'Ordered by', 'Billing address' or something similar. Return json containing query_id = 6 (string), " " address1 (string, address line 1), address2 (string, address line 2), " " city (string), postal (string), region (string), country_code (string, 2 letter code), is_business (string, 'true' or 'false'), " " name (string), taxid (string, tax identifier number), businessid (string, business identifier number), email (string), " " phone_number (string), bank_account (string)", "Who received the product? This information might be under the section 'Delivered to', 'shipping address' or something similar. Return json containing query_id = 7 (string), " " address1 (string, address line 1), address2 (string, address line 2), " " city (string), postal (string), region (string), country_code (string, 2 letter code), name (string). " " If the delivery address is not provided, return query_id = -1 (string)", "Give me a list of billed items in json format. Return query_id = 8 (string), item_count (string), items (array). For each item, get: " " amount (string, number of items in billing line, default to 1), amount_is_percentage (string, 'true' or 'false'), description (string), price_per_item (string, price with 2 decimals, no currency symbol), " " discount (string, price with 2 decimals, no currency symbol), discount_is_percentage (string, 'true' or 'false')" }; invoice inv = administration::invoice_create_empty(); if (!impl.batch_query_with_file(queries, sizeof(queries) / sizeof(char*), file_id, &inv, _batch_query_response_handler)) { request->status = importer::status::IMPORT_DONE; request->error = I_ERR_FAILED_QUERY; return 0; } inv.status = invoice_status::INVOICE_RECEIVED; // Set customer or supplier depending on incomming or outgoing. contact my_info = administration::company_info_get(); //memops::copy(&inv.customer, &my_info, sizeof(contact)); strops::copy(inv.customer.id, MY_COMPANY_ID, MAX_LEN_ID); // Project and cost centers cannot be interpreted from file so are set to 0. strops::copy(inv.project_id, "", MAX_LEN_ID); strops::copy(inv.cost_center_id, "", MAX_LEN_ID); // Set document references and save copy to disk. strops::copy(inv.document.original_path, file_path, MAX_LEN_PATH); strops::copy(inv.document.copy_path, "", MAX_LEN_PATH); request->status = importer::status::IMPORT_DONE; request->result = administration::invoice_create_copy(&inv); return 0; } importer::invoice_request* importer::ai_document_to_invoice(char* file_path) { importer::invoice_request* result = (importer::invoice_request*)memops::alloc(sizeof(importer::invoice_request)); result->started_at = time(NULL); result->error = I_ERR_SUCCESS; result->status = importer::status::IMPORT_STARTING; strops::copy(result->file_path, file_path, MAX_LEN_PATH); thrd_t thr; if (thrd_create(&thr, _ai_document_to_invoice_t, result) != thrd_success) { return 0; } return result; } static int _ai_get_available_models_t(void* arg) { importer::model_list_request* request = (importer::model_list_request*)arg; importer::ai_provider_impl impl = importer::get_ai_provider_implementation(request->service); if (!impl.get_available_models) { request->status = importer::status::IMPORT_DONE; request->error = I_ERR_UNIMPLEMENTED; return 0; } request->status = importer::status::IMPORT_WAITING_FOR_RESPONSE; if (!impl.get_available_models(request)) { request->status = importer::status::IMPORT_DONE; request->error = I_ERR_FAILED_QUERY; return 0; } request->status = importer::status::IMPORT_DONE; return 0; } importer::model_list_request* importer::ai_get_available_models(ai_provider service) { importer::model_list_request* result = (importer::model_list_request*)memops::alloc(sizeof(importer::model_list_request)); result->started_at = time(NULL); result->error = I_ERR_SUCCESS; result->status = importer::status::IMPORT_STARTING; result->result_count = 0; result->service = service; memops::zero(result->result, sizeof(result->result)); thrd_t thr; if (thrd_create(&thr, _ai_get_available_models_t, result) != thrd_success) { result->status = importer::status::IMPORT_DONE; result->error = I_ERR_FAILED_QUERY; return 0; } return result; } const char* importer::status_to_string(importer::status status) { switch(status) { case importer::status::IMPORT_STARTING: return locale::get("import.status.starting"); case importer::status::IMPORT_UPLOADING_FILE: return locale::get("import.status.uploading_file"); case importer::status::IMPORT_QUERYING: return locale::get("import.status.querying"); case importer::status::IMPORT_WAITING_FOR_RESPONSE: return locale::get("import.status.waiting_for_result"); case importer::status::IMPORT_DONE: return locale::get("import.status.done"); } return ""; } const char* importer::error_to_string(i_err error) { switch(error) { case I_ERR_FAILED_UPLOAD: return locale::get("import.error.upload"); case I_ERR_FAILED_QUERY: return locale::get("import.error.query"); case I_ERR_FAILED_IMPORT: return locale::get("import.error.import"); } return ""; }