package com.alexanderlogistics.zoho; import java.io.IOException; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.imixs.workflow.FileData; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.exceptions.PluginException; import com.alexanderlogistics.InvoiceUtil; import com.alexanderlogistics.zoho.dto.ZohoBill; import com.alexanderlogistics.zoho.dto.ZohoBillCreateResponse; import com.alexanderlogistics.zoho.dto.ZohoBillItem; import com.alexanderlogistics.zoho.dto.ZohoContact; import com.alexanderlogistics.zoho.dto.ZohoContactCreateResponse; import com.alexanderlogistics.zoho.dto.ZohoContactFindResponse; import com.alexanderlogistics.zoho.dto.ZohoCreditnote; import com.alexanderlogistics.zoho.dto.ZohoCreditnoteCreateResponse; import com.alexanderlogistics.zoho.dto.ZohoInvoice; import com.alexanderlogistics.zoho.dto.ZohoInvoiceCreateResponse; import com.alexanderlogistics.zoho.dto.ZohoInvoiceItem; import com.alexanderlogistics.zoho.dto.ZohoTax; import com.alexanderlogistics.zoho.dto.ZohoTaxListResponse; import com.alexanderlogistics.zoho.dto.ZohoVendorCredit; import com.alexanderlogistics.zoho.dto.ZohoVendorCreditCreateResponse; import jakarta.annotation.PostConstruct; import jakarta.annotation.security.DeclareRoles; import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RunAs; import jakarta.ejb.Singleton; import jakarta.inject.Inject; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbConfig; /** * Der ZohoAPIService stellt methoden für den Zugriff auf die Zoho API bereit. */ @DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS", "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS", "org.imixs.ACCESSLEVEL.MANAGERACCESS" }) @RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS", "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS", "org.imixs.ACCESSLEVEL.MANAGERACCESS" }) @Singleton @RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS") public class ZohoAPIService { private static Logger logger = Logger.getLogger(ZohoAPIService.class.getName()); private HttpClient httpClient; private Jsonb jsonb; @Inject ZohoOAuthManager zohoOAuthManager; @PostConstruct void init() { this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); JsonbConfig config = new JsonbConfig().withDateFormat("yyyy-MM-dd", Locale.getDefault()) .setProperty("jsonb.fail-on-unknown-properties", false); // Ignoriert unbekannte Felder this.jsonb = JsonbBuilder.create(config); } /** * This finder method returns the contact ID to a given company name. * The method returns null if no company with this name exists * * @param companyName * @param contactType * @param accessToken * @return * @throws PluginException */ public String findContactIDByCompanyName(String companyName, String contactType, String accessToken) throws PluginException { // Print JSON to console for verification logger.info("├── Zoho lookup contact (" + contactType + ") '" + companyName + "'..."); try { // https://www.zohoapis.com/books/v3/contacts?organization_id=10234695&company_name=xxx' String uri = zohoOAuthManager.getBaseURI() + "contacts?organization_id=" + zohoOAuthManager.getOrganization() + "&company_name=" + URLEncoder.encode(companyName, StandardCharsets.UTF_8); logger.fine("│ ├── uri= " + uri); logger.fine("│ ├── accessToken=" + accessToken); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").GET().build(); HttpResponse response; response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // Print response details to console logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); logger.fine("│ ├── Response Body: " + response.body()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to find Contacts - Status code: " + response.statusCode() + ", Response: " + response.body()); } ZohoContactFindResponse contactsResponse = jsonb.fromJson(response.body(), ZohoContactFindResponse.class); if (contactsResponse != null && contactsResponse.getContacts() != null) { // Durchsuche die Ergebnisse nach exakter Übereinstimmung for (ZohoContact contact : contactsResponse.getContacts()) { if (companyName.equalsIgnoreCase(contact.getCompanyName()) && contactType.equalsIgnoreCase((contact.getContactType()))) { logger.info("│ ├── contactID= " + contact.getContactId()); return contact.getContactId(); } } } } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to find Contacts: " + e.getMessage()); } return null; } /** * This finder method returns the tax ID to a given percentage. * The method returns null if no tax with this percentage exists * * @param tax_percentage * @param accessToken * @return taxID * @throws PluginException */ public String findTaxIDByPercentage(float tax_percentage, String accessToken) throws PluginException { if (tax_percentage == 0.0) { return null; } // Print JSON to console for verification logger.info("├── Zoho lookup tax '" + tax_percentage + "'..."); try { // https://www.zohoapis.com/books/v3/settings/taxes?organization_id=10234695&company_name=xxx' String uri = zohoOAuthManager.getBaseURI() + "settings/taxes?organization_id=" + zohoOAuthManager.getOrganization(); logger.fine("│ ├── uri= " + uri); logger.fine("│ ├── accessToken=" + accessToken); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").GET().build(); HttpResponse response; response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // Print response details to console logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); logger.fine("│ ├── Response Body: " + response.body()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to find Contacts - Status code: " + response.statusCode() + ", Response: " + response.body()); } ZohoTaxListResponse taxListResponse = jsonb.fromJson(response.body(), ZohoTaxListResponse.class); if (taxListResponse != null && taxListResponse.getTaxes() != null) { // Durchsuche die Ergebnisse nach exakter Übereinstimmung for (ZohoTax tax : taxListResponse.getTaxes()) { if (tax_percentage == tax.getRate()) { logger.info("│ ├── taxID= " + tax.getTaxId()); return tax.getTaxId(); } } } logger.warning("│ ├── No tax with " + tax_percentage + "% found in Zoho taxes!"); } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to find tax: " + e.getMessage()); } return null; } /** * Exports an invocie document * * @see https://www.zoho.com/books/api/v3/invoices/#create-an-invoice * * @param invoice * @throws PluginException */ public ZohoInvoice exportInvoice(ItemCollection invoice, String contactID, String accessToken) throws PluginException { logger.info("├── Zoho Export Invoice " + invoice.getItemValueString("invoice.number") + "..."); // Konvertiere ItemCollection zu ZohoInvoiceDTO ZohoInvoice zohoInvoice = convertToZohoInvoice(invoice, contactID); try { String requestBody = jsonb.toJson(zohoInvoice); // Print JSON to console for verification logger.fine("├── Zoho Invoice JSON Request..."); logger.fine("│ ├── " + requestBody); // https://www.zohoapis.com/books/v3/invoices?organization_id=10234695' String uri = zohoOAuthManager.getBaseURI() + "invoices?organization_id=" + zohoOAuthManager.getOrganization(); logger.fine("│ ├── uri= " + uri); logger.fine("│ ├── accessToken=" + accessToken); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // Print response details to console logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to create invoice. Status code: " + response.statusCode() + ", Response: " + response.body()); } // Erfolgreiche Erstellung String bodyContent = response.body(); logger.fine("│ ├── Response Body: " + bodyContent); System.out.print(bodyContent); ZohoInvoiceCreateResponse createResponse = jsonb.fromJson(bodyContent, ZohoInvoiceCreateResponse.class); if (createResponse != null) { logger.fine("│ ├── OK - customerID=" + createResponse.getInvoice().getCustomerId()); logger.info("│ ├── OK - invoiceID=" + createResponse.getInvoice().getInvoiceId()); zohoInvoice = createResponse.getInvoice(); // now mark the invoice as send markInvoiceAsSend(zohoInvoice.getInvoiceId(), "invoices", accessToken); logger.info("│ ├── Invoice Export successful."); return zohoInvoice; } } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to export invoice to Zoho: " + e.getMessage(), e); } return null; } /** * Exports an Credit Note - outgoing invoice total < 0,00 * * @see https://www.zoho.com/books/api/v3/expenses/#create-an-expense * * @param invoice * @throws PluginException */ public ZohoCreditnote exportCreditnote(ItemCollection invoice, String contactID, String accessToken) throws PluginException { logger.info("├── Zoho Export creditnote " + invoice.getItemValueString("invoice.number") + "..."); // Konvertiere ItemCollection zu ZohoInvoiceDTO ZohoCreditnote zohoCreditnote = convertToZohoCreditnote(invoice, contactID); try { String requestBody = jsonb.toJson(zohoCreditnote); // Print JSON to console for verification logger.fine("├── Zoho CreditNote JSON Request..."); logger.fine("│ ├── " + requestBody); // https://www.zohoapis.com/books/v3/creditnotes?organization_id=10234695' String uri = zohoOAuthManager.getBaseURI() + "creditnotes?organization_id=" + zohoOAuthManager.getOrganization(); logger.fine("│ ├── uri= " + uri); logger.fine("│ ├── accessToken=" + accessToken); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // Print response details to console logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to create creditnote. Status code: " + response.statusCode() + ", Response: " + response.body()); } // Erfolgreiche Erstellung String bodyContent = response.body(); logger.fine("│ ├── Response Body: " + bodyContent); System.out.print(bodyContent); ZohoCreditnoteCreateResponse createResponse = jsonb.fromJson(bodyContent, ZohoCreditnoteCreateResponse.class); if (createResponse != null) { logger.info("│ ├── OK - customerID=" + createResponse.getCreditnote().getCustomerId()); logger.info("│ ├── OK - creditNoteID=" + createResponse.getCreditnote().getCreditNoteNumber()); zohoCreditnote = createResponse.getCreditnote(); // now mark the invoice as send logger.info("│ ├── VendorCredit Export successful."); return zohoCreditnote; } } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to export creditnote to Zoho: " + e.getMessage(), e); } return null; } /** * Exports an Eingangsrechnung document * * @see https://www.zoho.com/books/api/v3/expenses/#create-an-expense * * @param invoice * @throws PluginException */ public ZohoBill exportBill(ItemCollection invoice, String contactID, String accountID, String accessToken) throws PluginException { logger.info("├── Zoho Export Bill " + invoice.getItemValueString("invoice.number") + "..."); // Konvertiere ItemCollection zu ZohoInvoiceDTO ZohoBill zohoBill = convertToZohoBill(invoice, contactID, accountID); try { String requestBody = jsonb.toJson(zohoBill); // Print JSON to console for verification logger.fine("├── Zoho Bill JSON Request..."); logger.fine("│ ├── " + requestBody); // https://www.zohoapis.com/books/v3/expenses?organization_id=10234695' String uri = zohoOAuthManager.getBaseURI() + "bills?organization_id=" + zohoOAuthManager.getOrganization(); logger.fine("│ ├── uri= " + uri); logger.fine("│ ├── accessToken=" + accessToken); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // Print response details to console logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to create expense. Status code: " + response.statusCode() + ", Response: " + response.body()); } // Erfolgreiche Erstellung String bodyContent = response.body(); logger.fine("│ ├── Response Body: " + bodyContent); System.out.print(bodyContent); ZohoBillCreateResponse createResponse = jsonb.fromJson(bodyContent, ZohoBillCreateResponse.class); if (createResponse != null) { logger.info("│ ├── OK - customerID=" + createResponse.getBill().getVendorId()); logger.info("│ ├── OK - billID=" + createResponse.getBill().getBillId()); zohoBill = createResponse.getBill(); // now mark the invoice as send logger.info("│ ├── Bill Export successful."); return zohoBill; } } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to export bill to Zoho: " + e.getMessage(), e); } return null; } /** * Exports an Eingangsrechnung as a Vendor Credit * * @see https://www.zoho.com/books/api/v3/expenses/#create-an-expense * * @param invoice * @throws PluginException */ public ZohoVendorCredit exportVendorCredit(ItemCollection invoice, String contactID, String accountID, String accessToken) throws PluginException { logger.info("├── Zoho Export VendorCredit " + invoice.getItemValueString("invoice.number") + "..."); // Konvertiere ItemCollection zu ZohoInvoiceDTO ZohoVendorCredit zohoVendorCredit = convertToZohoVendorCredit(invoice, contactID, accountID); try { String requestBody = jsonb.toJson(zohoVendorCredit); // Print JSON to console for verification logger.fine("├── Zoho VendorCredit JSON Request..."); logger.fine("│ ├── " + requestBody); // https://www.zohoapis.com/books/v3/expenses?organization_id=10234695' String uri = zohoOAuthManager.getBaseURI() + "vendorcredits?organization_id=" + zohoOAuthManager.getOrganization(); logger.fine("│ ├── uri= " + uri); logger.fine("│ ├── accessToken=" + accessToken); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // Print response details to console logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to create expense. Status code: " + response.statusCode() + ", Response: " + response.body()); } // Erfolgreiche Erstellung String bodyContent = response.body(); logger.fine("│ ├── Response Body: " + bodyContent); System.out.print(bodyContent); ZohoVendorCreditCreateResponse createResponse = jsonb.fromJson(bodyContent, ZohoVendorCreditCreateResponse.class); if (createResponse != null) { logger.info("│ ├── OK - customerID=" + createResponse.getVendorCredit().getVendorId()); logger.info("│ ├── OK - creditID=" + createResponse.getVendorCredit().getCreditId()); zohoVendorCredit = createResponse.getVendorCredit(); // now mark the invoice as send logger.info("│ ├── VendorCredit Export successful."); return zohoVendorCredit; } } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to export vendor credit to Zoho: " + e.getMessage(), e); } return null; } public String lookupContact(String companyName, String contactType, String accessToken) throws PluginException { // first lookup contact! String contactID = ""; contactID = findContactIDByCompanyName(companyName, contactType, accessToken); if (contactID == null) { logger.info("│ ├── contact '" + companyName + "' not found"); ZohoContact contact = createContact(companyName, contactType, accessToken); contactID = contact.getContactId(); } return contactID; } /** * Erstellt einen neuen Contact in Zoho Books * * @param companyName Der Firmenname für den neuen Contact * @param contactType - customer/vendor * @return Der erstellte ZohoContact mit contact_id * @throws PluginException Wenn der API-Aufruf fehlschlägt */ public ZohoContact createContact(String companyName, String contactType, String accessToken) throws PluginException { logger.info("├── Zoho create contact '" + companyName + "'..."); try { // Contact-DTO vorbereiten ZohoContact newContact = new ZohoContact(); newContact.setContactName(companyName); // Wir verwenden den Firmennamen auch als Contact Name newContact.setCompanyName(companyName); newContact.setContactType(contactType); // JSON serialisieren String requestBody = jsonb.toJson(newContact); logger.fine("│ ├── " + requestBody); String uri = zohoOAuthManager.getBaseURI() + "contacts?organization_id=" + zohoOAuthManager.getOrganization(); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); logger.fine("│ ├── Response Body: " + response.body()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to create invoice. Status code: " + response.statusCode() + ", Response: " + response.body()); } // Erfolgreiche Erstellung ZohoContactCreateResponse createResponse = jsonb.fromJson(response.body(), ZohoContactCreateResponse.class); if (createResponse != null) { logger.info("│ ├── OK - contactID=" + createResponse.getContact().getContactId()); return createResponse.getContact(); } else { throw new PluginException(this.getClass().getName(), "Zoho API error", "Response could not be resolved!"); } } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to create contact: " + e.getMessage(), e); } } private ZohoInvoice convertToZohoInvoice(ItemCollection invoice, String contactID) throws PluginException { ZohoInvoice zohoInvoice = new ZohoInvoice(); // Basisinformationen zohoInvoice.setInvoiceNumber(invoice.getItemValueString("invoice.number")); zohoInvoice.setDate( LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault())); zohoInvoice.setDueDate( LocalDate.ofInstant(invoice.getItemValueDate("invoice.duedate").toInstant(), ZoneId.systemDefault())); zohoInvoice.setCustomerId(contactID); // convert total by rate zohoInvoice.setTotal(convertInvoiceTotalToAED(invoice, invoice.getItemValueDouble("invoice.total"))); /* * Change: 10.06.2025 * Das Cargosoft unlogische Positionen sendet, ZOHO diese aber erzwingt, * erzeugen wir nur noch eine einzige Zeile mit dem tatsächlichen Betrag! * */ // List positionen = InvoiceUtil.explodeChildList(invoice); // List lineItems = positionen.stream().map(item -> { // ItemCollection itemCol = (ItemCollection) item; // ZohoInvoiceItem lineItem = new ZohoInvoiceItem(); // lineItem.setName(itemCol.getItemValueString("datev.text")); // lineItem.setDescription(itemCol.getItemValueString("billingtext")); // lineItem.setRate(convertInvoiceTotalToAED(invoice, // itemCol.getItemValueDouble("datev.umsatz"))); // lineItem.setQuantity(1); // return lineItem; // }).collect(Collectors.toList()); List lineItems = new ArrayList<>(); ZohoInvoiceItem lineItem = new ZohoInvoiceItem(); lineItem.setName(invoice.getItemValueString("invoice.text")); lineItem.setDescription(""); lineItem.setRate(convertInvoiceTotalToAED(invoice, invoice.getItemValueDouble("invoice.total"))); lineItem.setQuantity(1); lineItems.add(lineItem); zohoInvoice.setLineItems(lineItems); return zohoInvoice; } private ZohoCreditnote convertToZohoCreditnote(ItemCollection invoice, String contactID) throws PluginException { ZohoCreditnote zohoCreditnote = new ZohoCreditnote(); // Basisinformationen zohoCreditnote.setCreditNoteNumber(invoice.getItemValueString("invoice.number")); zohoCreditnote.setDate( LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault())); zohoCreditnote.setDueDate( LocalDate.ofInstant(invoice.getItemValueDate("invoice.duedate").toInstant(), ZoneId.systemDefault())); zohoCreditnote.setCustomerId(contactID); // convert total by rate - negativer betrag! zohoCreditnote .setTotal(convertInvoiceTotalToAED(invoice, Math.abs(invoice.getItemValueDouble("invoice.total")))); /* * Change: 10.06.2025 * Das Cargosoft unlogische Positionen sendet, ZOHO diese aber erzwingt, * erzeugen wir nur noch eine einzige Zeile mit dem tatsächlichen Betrag! * */ // List positionen = InvoiceUtil.explodeChildList(invoice); // List lineItems = positionen.stream().map(item -> { // ItemCollection itemCol = (ItemCollection) item; // ZohoInvoiceItem lineItem = new ZohoInvoiceItem(); // lineItem.setName(itemCol.getItemValueString("datev.text")); // lineItem.setDescription(itemCol.getItemValueString("billingtext")); // lineItem.setRate(convertInvoiceTotalToAED(invoice, // itemCol.getItemValueDouble("datev.umsatz"))); // lineItem.setQuantity(1); // return lineItem; // }).collect(Collectors.toList()); List lineItems = new ArrayList<>(); ZohoInvoiceItem lineItem = new ZohoInvoiceItem(); lineItem.setName(invoice.getItemValueString("invoice.text")); lineItem.setDescription(""); // negativer Betrag! lineItem.setRate(convertInvoiceTotalToAED(invoice, Math.abs(invoice.getItemValueDouble("invoice.total")))); lineItem.setQuantity(1); lineItems.add(lineItem); zohoCreditnote.setLineItems(lineItems); return zohoCreditnote; } /** * Diese Hilfsmethode rechnet den Rechnungsbetrag in Fremdwährung auf AED um * * es ist eine Fremdwährung * Herr Heolzl 15.05.2025 * However the backend reports the P&L & Balance sheet will reflect in AED as * AGL Dubai books are maintained in AED therefore we need to have an * exchange rate which can be fixed @3.6725 AED for 1 USD. * * Aus diesem Grund rechnen wir direkt in AED um! * * @return */ public double convertInvoiceTotalToAED(ItemCollection invoice, double total) { // convert total by fixed rate? String currency = invoice.getItemValueString("invoice.currency"); if (!currency.isEmpty() && !currency.equalsIgnoreCase("AED")) { // es ist eine Fremdwährung // Herr Hoelzl 15.05.2025 // However the backend reports the P&L & Balance sheet will reflect in AED as // AGL Dubai books are maintained in AED therefore we need to have an exchange // rate which can be fixed @3.6725 AED for 1 USD. // // Aus diesem Grund rechnen wir direkt in AED um! // wir verwenden unsere Rate // double rate=3.6725; double rate = 0; if (invoice.getModelVersion().startsWith("rechnungsausgang-")) { rate = invoice.getItemValueDouble("invoice.rate"); } else { rate = invoice.getItemValueDouble("invoice.exchangerate"); } if (rate == 0) { rate = 0.27229; // (3.6725) = default rate } double aedBetrag = total / rate; logger.info("│ ├── convert into AED=" + aedBetrag); return aedBetrag; } else { return total; } } private ZohoBill convertToZohoBill(ItemCollection invoice, String contactID, String accountID) throws PluginException { ZohoBill zohoBill = new ZohoBill(); // Basisinformationen zohoBill.setInvoiceNumber(invoice.getItemValueString("invoice.number")); zohoBill.setDate( LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault())); // convert total by rate zohoBill.setAmount(convertInvoiceTotalToAED(invoice, invoice.getItemValueDouble("invoice.total"))); // Kunden-ID (muss angepasst werden je nachdem wie du sie speicherst) zohoBill.setVendorId(contactID); // zohoBill.setPaid_through_account_id("1700"); List positionen = InvoiceUtil.explodeChildList(invoice); // {amount=[11825.00], total=[11825.00], numpos=[1], name=[AB-DEFGHI-1234-567], // tax=[0], category=[STORAGE], invoice.period=[]} List lineItems = positionen.stream().map(item -> { ItemCollection itemCol = (ItemCollection) item; ZohoBillItem lineItem = new ZohoBillItem(); lineItem.setName(itemCol.getItemValueString("name")); lineItem.setAccountId(accountID); lineItem.setQuantity(1); lineItem.setRate(convertInvoiceTotalToAED(invoice, itemCol.getItemValueDouble("amount"))); // tax - tax_id, tax_percentage try { float tax = itemCol.getItemValueFloat("tax"); String taxID = findTaxIDByPercentage(tax, accountID); if (taxID != null && !taxID.isEmpty()) { lineItem.setTaxId(taxID); } } catch (PluginException e) { logger.info("│ ├── failed to compute taxId: " + e.getMessage()); } return lineItem; }).collect(Collectors.toList()); zohoBill.setLineItems(lineItems); return zohoBill; } private ZohoVendorCredit convertToZohoVendorCredit(ItemCollection invoice, String contactID, String accountID) throws PluginException { ZohoVendorCredit zohoVendorCredit = new ZohoVendorCredit(); // Basisinformationen zohoVendorCredit.setInvoiceNumber(invoice.getItemValueString("invoice.number")); zohoVendorCredit.setDate( LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault())); // convert total by rate // zohoVendorCredit.setAmount(convertInvoiceTotalToAED(invoice, // invoice.getItemValueDouble("invoice.total"))); // Kunden-ID (muss angepasst werden je nachdem wie du sie speicherst) zohoVendorCredit.setVendorId(contactID); // zohoBill.setPaid_through_account_id("1700"); List positionen = InvoiceUtil.explodeChildList(invoice); // {amount=[11825.00], total=[11825.00], numpos=[1], name=[AB-DEFGHI-1234-567], // tax=[0], category=[STORAGE], invoice.period=[]} List lineItems = positionen.stream().map(item -> { ItemCollection itemCol = (ItemCollection) item; ZohoBillItem lineItem = new ZohoBillItem(); lineItem.setName(itemCol.getItemValueString("name")); lineItem.setAccountId(accountID); lineItem.setQuantity(1); lineItem.setRate(convertInvoiceTotalToAED(invoice, itemCol.getItemValueDouble("amount"))); // tax - tax_id, tax_percentage try { float tax = itemCol.getItemValueFloat("tax"); String taxID = findTaxIDByPercentage(tax, accountID); if (taxID != null && !taxID.isEmpty()) { lineItem.setTaxId(taxID); } } catch (PluginException e) { logger.info("│ ├── failed to compute taxId: " + e.getMessage()); } return lineItem; }).collect(Collectors.toList()); zohoVendorCredit.setLineItems(lineItems); return zohoVendorCredit; } /** * This method adds an attachment to an invoice * * @param invoiceId The Zoho invoice ID * @param file The FileData object containing the attachment * @param uriPattern The API endpoint pattern (e.g., "invoices") * @param accessToken The OAuth access token * @throws PluginException If the API call fails */ public void attacheFileData(String invoiceId, FileData file, String uriPattern, String accessToken) throws PluginException { String uri = zohoOAuthManager.getBaseURI() + uriPattern + "/" + // invoiceId + // "/attachment" + // "?organization_id=" + zohoOAuthManager.getOrganization(); logger.info("│ ├── add attachment '" + file.getName() + "'..."); logger.fine("│ ├── ...uri: " + uri + "..."); try { // Generate a unique boundary String boundary = "----WebKitFormBoundary" + System.currentTimeMillis(); // Build multipart request body byte[] header = ("--" + boundary + "\r\n" + "Content-Disposition: form-data; name=\"attachment\"; filename=\"" + file.getName() + "\"\r\n" + "Content-Type: " + file.getContentType() + "\r\n\r\n").getBytes(StandardCharsets.UTF_8); byte[] footer = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8); logger.fine("│ ├── content size=" + file.getContent().length); HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers .ofByteArrays(Arrays.asList(header, file.getContent(), footer)); // Create and send the request HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "multipart/form-data; boundary=" + boundary).POST(bodyPublisher).build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); logger.fine("│ ├── Response Body: " + response.body()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to add attachment. Status code: " + response.statusCode() + ", Response: " + response.body()); } logger.info("│ ├── OK - Attachment added successfully"); } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to add attachment: " + e.getMessage(), e); } } /** * This method marks an invoice as send * * * curl --request POST \ --url * 'https://www.zohoapis.com/books/v3/invoices/982000000567114/status/sent?organization_id=10234695' * \ --header 'Authorization: Zoho-oauthtoken * 1000.41d9xxxxxxxxxxxxxxxxxxxxxxxxc2d1.8fccxxxxxxxxxxxxxxxxxxxxxxxx125f' * * * @param invoiceId The Zoho invoice ID * @param uriPattern The API endpoint pattern (e.g., "invoices") * @param accessToken The OAuth access token * @throws PluginException If the API call fails */ public void markInvoiceAsSend(String invoiceId, String uriPattern, String accessToken) throws PluginException { String uri = zohoOAuthManager.getBaseURI() + uriPattern + "/" + // invoiceId + // "/status/sent" + // "?organization_id=" + zohoOAuthManager.getOrganization(); logger.fine("│ ├── mark invoice as sent '" + invoiceId + "'..."); logger.fine("│ ├── ...uri: " + uri + "..."); try { // JSON serialisieren String requestBody = ""; logger.fine("│ ├── " + requestBody); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); logger.fine("│ ├── Response Body: " + response.body()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to create invoice. Status code: " + response.statusCode() + ", Response: " + response.body()); } logger.info("│ ├── OK - status update 'send' successfully"); } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to add attachment: " + e.getMessage(), e); } } /** * Sucht eine currency ID Wegen eifnachheit parsen wir hier direkt die JSON * struktur * * @param currency * @param accessToken * @return * @throws PluginException */ public String lookupCurrency(String currency, String accessToken) throws PluginException { // Print JSON to console for verification logger.info("├── Zoho lookup currency '" + currency + "'..."); try { // https://www.zohoapis.com/books/v3/contacts?organization_id=10234695&company_name=xxx' String uri = zohoOAuthManager.getBaseURI() + "settings/currencies?organization_id=" + zohoOAuthManager.getOrganization(); logger.fine("│ ├── uri= " + uri); logger.fine("│ ├── accessToken=" + accessToken); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").GET().build(); HttpResponse response; response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // Print response details to console logger.fine("├── Zoho Response:"); logger.fine("│ ├── Status Code: " + response.statusCode()); logger.fine("│ ├── Response Body: " + response.body()); if (response.statusCode() >= 300) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to find currencies - Status code: " + response.statusCode() + ", Response: " + response.body()); } String json = response.body(); String pattern = "\"currency_code\":\"" + Pattern.quote(currency) + "\".*?\"currency_id\":\"(\\d+)\""; Matcher matcher = Pattern.compile(pattern).matcher(json); String result = matcher.find() ? matcher.group(1) : null; logger.info("│ ├── Currency ID=" + result); return result; } catch (IOException | InterruptedException e) { throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to find Contacts: " + e.getMessage()); } } }