diff --git a/doc/zoho/README.md b/doc/zoho/README.md index fa01f61..01d5676 100644 --- a/doc/zoho/README.md +++ b/doc/zoho/README.md @@ -18,7 +18,7 @@ Base API URI= https://accounts.zoho.eu/ ## 1. Client Einrichten -https://api-console.zoho.eu/client/1000.NLTM2KUEHI696MSLGEIANATVGGX7IN +https://api-console.zoho.eu/ ## 2. Generating Grant Token diff --git a/doc/zoho/SECRETS.md b/doc/zoho/SECRETS.md index 952e01f..c88a5de 100644 --- a/doc/zoho/SECRETS.md +++ b/doc/zoho/SECRETS.md @@ -19,6 +19,60 @@ zoho.scope ZohoBooks.contacts.ALL ZohoBooks.invoices.CREATE ZohoBooks.i ``` +## Forum + +form@imixs.com +imixs4zoho + +https://api-console.zoho.eu/ + +ZohoBooks.contacts.ALL,ZohoBooks.invoices.ALL,ZohoBooks.bills.ALL + +``` +zoho.accesstoken 1.....3 +zoho.apidomain https://www.zohoapis.eu +zoho.clientid 1000.5SMVXH92JIA5BTIGJF27OY41TAFFAS +zoho.clientsecret 64aabb5e819757e471c9d0dff6a072b1bcf2c7687d +zoho.code 10- +zoho.organization 886888983 +zoho.refreshtoken 10...ff440e +zoho.scope ZohoBooks.contacts.ALL ZohoBooks.invoices.CREATE ZohoBooks.invoices.READ ZohoBooks.invoices.UPDATE + +``` + +## Atari + +atari.600xl@gmail.com +800xlAtari + +``` +zoho.apidomain https://www.zohoapis.eu +zoho.clientid 1000.86O4P321HDYDRX0Q0N1G7YOJRJ9D5W +zoho.clientsecret 2a67b6cfc364f5df27cc8eb33e9b0e4e8a0221ebc3 +zoho.code 1000.c7dc89c06111b93f1242b587fabed4ac.44c591dffbd978a316e3d90ed3fefc78 +zoho.organization 886924765 +zoho.scope ZohoBooks.contacts.ALL,ZohoBooks.invoices.ALL,ZohoBooks.bills.ALL +``` + +## Prod DWC + +info@imixs.com +imixs4zoho + +https://api-console.zoho.com/ + +ZohoBooks.contacts.ALL,ZohoBooks.invoices.ALL,ZohoBooks.bills.ALL + +``` + +zoho.apidomain https://www.zohoapis.com +zoho.clientid 1000.MQE1SU8UGY6F2PBPASOXGTLWYQD1FS +zoho.clientsecret 1ca70e1ab803b3fed6b1b5e00858c316e2003d289b +zoho.organization 884218508 +zoho.scope ZohoBooks.contacts.ALL,ZohoBooks.invoices.ALL,ZohoBooks.bills.ALL + +``` + # API Dokumentation - API Docu: https://www.zoho.com/books/api/v3/introduction/ diff --git a/doc/zoho/bill-create-example.json b/doc/zoho/bill-create-example.json new file mode 100644 index 0000000..7ab5cde --- /dev/null +++ b/doc/zoho/bill-create-example.json @@ -0,0 +1,167 @@ +{ + "code": 0, + "message": "The bill has been created.", + "bill": { + "bill_id": "6206847000000103003", + "vendor_id": "6206847000000100015", + "vendor_name": "Feldmuehle GmbH", + "source": "Api", + "contact_category": "", + "tax_treatment": "", + "unused_credits_payable_amount": 0.00, + "status": "paid", + "color_code": "", + "current_sub_status_id": "", + "current_sub_status": "paid", + "sub_statuses": [], + "bill_number": "28195", + "date": "2024-07-26", + "due_date": "2024-07-26", + "discount_setting": "flat", + "tds_calculation_type": "tds_item_level", + "payment_terms": 0, + "payment_terms_label": "Due on Receipt", + "payment_expected_date": "", + "reference_number": "", + "scanned_po_number": "", + "recurring_bill_id": "", + "due_by_days": 286, + "due_in_days": "", + "currency_id": "6206847000000000109", + "currency_code": "EUR", + "currency_symbol": "€", + "currency_name_formatted": "EUR- Euro", + "documents": [], + "subject_content": "", + "price_precision": 2, + "exchange_rate": 1.00, + "custom_fields": [], + "custom_field_hash": {}, + "is_viewed_by_client": false, + "client_viewed_time": "", + "is_item_level_tax_calc": false, + "is_inclusive_tax": false, + "tax_rounding": "entity_level", + "is_uber_bill": false, + "is_tally_bill": false, + "track_discount_in_account": true, + "is_bill_reconciliation_violated": false, + "bill_order_type": "", + "line_items": [ + { + "purchaseorder_id": "", + "purchaseorder_item_id": "", + "receive_id": "", + "line_item_id": "6206847000000103013", + "item_id": "", + "name": "AB-DEFGHI-1234-567", + "account_id": "6206847000000092271", + "account_name": "TEst", + "description": "", + "bcy_rate": 0.00, + "rate": 0.00, + "sales_rate": "", + "pricebook_id": "", + "header_id": "", + "header_name": "", + "tags": [], + "quantity": 1.00, + "discount": 0.00, + "discounts": [], + "discount_account_id": "", + "discount_account_name": "", + "markup_percent": 0.00, + "tax_id": "", + "tax_name": "", + "tax_type": "tax", + "tax_percentage": 0, + "line_item_taxes": [], + "item_total": 0.00, + "item_order": 1, + "unit": "", + "item_type": "", + "image_name": "", + "image_type": "", + "is_billable": false, + "customer_id": "", + "receipt_line_item_id": "", + "customer_name": "", + "project_id": "", + "project_name": "", + "invoice_id": "", + "invoice_number": "", + "item_custom_fields": [], + "purchase_request_items": [], + "item_matching_type": "" + } + ], + "submitted_date": "", + "submitted_by": "", + "submitted_by_name": "", + "submitted_by_email": "", + "submitted_by_photo_url": "", + "submitter_id": "", + "approver_id": "", + "adjustment": 0.00, + "adjustment_description": "", + "discount_amount": 0.00, + "discount": 0.00, + "discount_applied_on_amount": 0.00, + "is_discount_before_tax": true, + "discount_account_id": "", + "discount_account_name": "", + "discount_type": "entity_level", + "sub_total": 0.00, + "sub_total_inclusive_of_tax": 0.00, + "tax_total": 0.00, + "discount_total": 0.00, + "discount_percent": 0.00, + "total": 0.00, + "payment_made": 0.00, + "vendor_credits_applied": 0.00, + "is_line_item_invoiced": false, + "purchaseorders": [], + "taxes": [], + "computation_type": "basic", + "tax_override_preference": "no_override", + "tds_override_preference": "no_override", + "balance": 0.00, + "unprocessed_payment_amount": 0.00, + "billing_address_id": "6206847000000100017", + "billing_address": { + "address": "", + "street2": "", + "city": "", + "state": "", + "zip": "", + "country": "", + "fax": "", + "phone": "", + "attention": "" + }, + "payments": [], + "vendor_credits": [], + "created_time": "2025-05-08T21:36:04+0100", + "created_by_id": "6206847000000092001", + "last_modified_id": "6206847000000092001", + "last_modified_time": "2025-05-08T21:36:04+0100", + "reference_id": "", + "notes": "", + "terms": "", + "attachment_name": "", + "template_id": "6206847000000031017", + "template_name": "Standard Template", + "page_width": "8.27in", + "page_height": "11.69in", + "orientation": "portrait", + "template_type": "standard", + "is_approval_required": false, + "allocated_landed_costs": [], + "unallocated_landed_costs": [], + "entity_type": "bill", + "total_retention_amount": 0.00, + "retention_items": [], + "can_send_in_mail": false, + "approvers_list": [] + } +} \ No newline at end of file diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java index 040279e..f7364c6 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java @@ -21,9 +21,15 @@ 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.ZohoExpense; +import com.alexanderlogistics.zoho.dto.ZohoExpenseCreateResponse; +import com.alexanderlogistics.zoho.dto.ZohoExpenseItem; import com.alexanderlogistics.zoho.dto.ZohoInvoice; import com.alexanderlogistics.zoho.dto.ZohoInvoiceCreateResponse; import com.alexanderlogistics.zoho.dto.ZohoInvoiceItem; @@ -53,12 +59,12 @@ public class ZohoAPIService { private static Logger logger = Logger.getLogger(ZohoAPIService.class.getName()); - @Inject - ZohoOAuthManager zohoOAuthManager; - private HttpClient httpClient; private Jsonb jsonb; + @Inject + ZohoOAuthManager zohoOAuthManager; + @PostConstruct void init() { this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); @@ -68,10 +74,11 @@ public class ZohoAPIService { this.jsonb = JsonbBuilder.create(config); } - public String findContactIDByCompanyName(String companyName, String accessToken) throws PluginException { + public String findContactIDByCompanyName(String companyName, String contactType, String accessToken) + throws PluginException { // Print JSON to console for verification - logger.info("├── Zoho lookup contact '" + companyName + "'..."); + 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=" @@ -83,7 +90,9 @@ public class ZohoAPIService { .header("Authorization", "Zoho-oauthtoken " + accessToken) .header("Content-Type", "application/json").GET().build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse response; + + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // Print response details to console logger.fine("├── Zoho Response:"); @@ -100,16 +109,18 @@ public class ZohoAPIService { if (contactsResponse != null && contactsResponse.getContacts() != null) { // Durchsuche die Ergebnisse nach exakter Übereinstimmung for (ZohoContact contact : contactsResponse.getContacts()) { - if (companyName.equalsIgnoreCase(contact.getCompanyName())) { + if (companyName.equalsIgnoreCase(contact.getCompanyName()) + && contactType.equalsIgnoreCase((contact.getContactType()))) { logger.info("│ ├── contactID= " + contact.getContactId()); return contact.getContactId(); } } } - return null; - } catch (Exception e) { - throw new PluginException(this.getClass().getName(), "Zoho API error", "Failed to find contacts", e); + } catch (IOException | InterruptedException e) { + throw new PluginException(this.getClass().getName(), "Zoho API error", + "Failed to find Contacts: " + e.getMessage()); } + return null; } @@ -121,21 +132,12 @@ public class ZohoAPIService { * @param invoice * @throws PluginException */ - public ZohoInvoice exportInvoice(ItemCollection invoice, List files) throws PluginException { - String accessToken = zohoOAuthManager.getValidAccessToken(); + public ZohoInvoice exportInvoice(ItemCollection invoice, String contactID, String accessToken) + throws PluginException { logger.info("├── Zoho Export Invoice " + invoice.getItemValueString("invoice.number") + "..."); - // first lookup contact! - String contactID = ""; - contactID = findContactIDByCompanyName(invoice.getItemValueString("dbtr.name"), accessToken); - if (contactID == null) { - logger.info("│ ├── contact not found"); - ZohoContact contact = createContact(invoice.getItemValueString("dbtr.name"), accessToken); - contactID = contact.getContactId(); - } - // Konvertiere ItemCollection zu ZohoInvoiceDTO - ZohoInvoice zohoInvoice = convertToZohoInvoice(invoice, contactID, accessToken); + ZohoInvoice zohoInvoice = convertToZohoInvoice(invoice, contactID); try { String requestBody = jsonb.toJson(zohoInvoice); @@ -173,52 +175,193 @@ public class ZohoAPIService { System.out.print(bodyContent); ZohoInvoiceCreateResponse createResponse = jsonb.fromJson(bodyContent, ZohoInvoiceCreateResponse.class); - logger.fine("│ ├── OK - customerID=" + createResponse.getInvoice().getCustomerId()); - logger.info("│ ├── OK - invoiceID=" + createResponse.getInvoice().getInvoiceId()); - if (createResponse != null) { + logger.fine("│ ├── OK - customerID=" + createResponse.getInvoice().getCustomerId()); + logger.info("│ ├── OK - invoiceID=" + createResponse.getInvoice().getInvoiceId()); zohoInvoice = createResponse.getInvoice(); - // now add attachments (optional) - try { - if (files != null) { - for (FileData file : files) { - attacheFileData(zohoInvoice.getInvoiceId(), file, "invoices", accessToken); - } - } - // now mark the invoice as send - markInvoiceAsSend(zohoInvoice.getInvoiceId(), "invoices", accessToken); - } catch (PluginException e) { - logger.warning("Failed to export Invoice: " + e.getMessage()); - } + // now mark the invoice as send + markInvoiceAsSend(zohoInvoice.getInvoiceId(), "invoices", accessToken); + logger.info("│ ├── Invoice Export successful."); return zohoInvoice; - } else { - throw new PluginException(this.getClass().getName(), "Zoho API error", - "Failed to export invoice to Zoho: unable to resolve response"); } } 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 Eingangsrechnung document + * + * @see https://www.zoho.com/books/api/v3/expenses/#create-an-expense + * + * @param invoice + * @throws PluginException + */ + public ZohoExpense exportExpense(ItemCollection invoice, String contactID, String accessToken) + throws PluginException { + logger.info("├── Zoho Export Expense " + invoice.getItemValueString("invoice.number") + "..."); + + // Konvertiere ItemCollection zu ZohoInvoiceDTO + ZohoExpense zohoExpense = convertToZohoExpense(invoice, contactID); + + try { + String requestBody = jsonb.toJson(zohoExpense); + + // Print JSON to console for verification + logger.fine("├── Zoho Expense JSON Request..."); + logger.fine("│ ├── " + requestBody); + + // https://www.zohoapis.com/books/v3/expenses?organization_id=10234695' + String uri = zohoOAuthManager.getBaseURI() + "expenses?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); + ZohoExpenseCreateResponse createResponse = jsonb.fromJson(bodyContent, ZohoExpenseCreateResponse.class); + + if (createResponse != null) { + logger.info("│ ├── OK - customerID=" + createResponse.getExpense().getCustomerId()); + logger.info("│ ├── OK - expenseID=" + createResponse.getExpense().getExpenseId()); + zohoExpense = createResponse.getExpense(); + + // now mark the invoice as send + + logger.info("│ ├── Invoice Export successful."); + return zohoExpense; + } + + } 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 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; + + } + + 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 accessToken) throws PluginException { + 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); @@ -258,8 +401,7 @@ public class ZohoAPIService { } - private ZohoInvoice convertToZohoInvoice(ItemCollection invoice, String contactID, String accessToken) - throws PluginException { + private ZohoInvoice convertToZohoInvoice(ItemCollection invoice, String contactID) throws PluginException { ZohoInvoice zohoInvoice = new ZohoInvoice(); // Basisinformationen @@ -290,6 +432,65 @@ public class ZohoAPIService { return zohoInvoice; } + private ZohoExpense convertToZohoExpense(ItemCollection invoice, String contactID) throws PluginException { + + ZohoExpense zohoExpense = new ZohoExpense(); + // Basisinformationen + + zohoExpense.setInvoiceNumber(invoice.getItemValueString("invoice.number")); + zohoExpense.setDate( + LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault())); + + zohoExpense.setAmount(invoice.getItemValueDouble("invoice.total")); + // Kunden-ID (muss angepasst werden je nachdem wie du sie speicherst) + zohoExpense.setCustomerId(contactID); + zohoExpense.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; + ZohoExpenseItem lineItem = new ZohoExpenseItem(); + lineItem.setDescription(itemCol.getItemValueString("name")); + + return lineItem; + }).collect(Collectors.toList()); + + zohoExpense.setLineItems(lineItems); + return zohoExpense; + } + + 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())); + + zohoBill.setAmount(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.setAmount(itemCol.getItemValueDouble("amount")); + + return lineItem; + }).collect(Collectors.toList()); + + zohoBill.setLineItems(lineItems); + return zohoBill; + } + /** * This method adds an attachment to an invoice * diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoController.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoController.java index 292012a..1e61a5f 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoController.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoController.java @@ -29,7 +29,7 @@ import java.util.logging.Logger; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.office.config.ConfigController; -import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.ConversationScoped; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -42,7 +42,8 @@ import jakarta.inject.Named; * */ @Named("zohoController") -@ApplicationScoped +// @ApplicationScoped +@ConversationScoped public class ZohoController extends ConfigController { public static final String ZOHO_CONFIGURATION = "ZOHO_CONFIGURATION"; @@ -64,7 +65,7 @@ public class ZohoController extends ConfigController { public void updateAccessToken() { // try { - zohoOAuthManager.updateTokens(this.getWorkitem().getItemValueString("zoho.code")); + zohoOAuthManager.updateTokens(this.getWorkitem()); } catch (PluginException e) { String message = "Failed to generate token: " + e.getMessage(); logger.warning(message); diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoExportAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoExportAdapter.java index 9394df1..270efe8 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoExportAdapter.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoExportAdapter.java @@ -12,6 +12,7 @@ import org.imixs.workflow.engine.WorkflowService; import org.imixs.workflow.exceptions.AdapterException; import org.imixs.workflow.exceptions.PluginException; +import com.alexanderlogistics.zoho.dto.ZohoBill; import com.alexanderlogistics.zoho.dto.ZohoInvoice; import jakarta.inject.Inject; @@ -37,6 +38,8 @@ public class ZohoExportAdapter implements SignalAdapter { @Inject SnapshotService snapshotService; + @Inject + ZohoOAuthManager zohoOAuthManager; @Inject @ConfigProperty(name = ENV_ZOHO_EXPORT_MAX_ATTACHMENT_SIZE, defaultValue = "10485760") @@ -50,21 +53,82 @@ public class ZohoExportAdapter implements SignalAdapter { @Override public ItemCollection execute(ItemCollection document, ItemCollection event) throws AdapterException, PluginException { - logger.info("├── Invoice Export"); - - // Load attachments - List files = null; - ItemCollection snapshot = snapshotService.findSnapshot(document); - - if (snapshot != null) { - files = snapshot.getFileData(); - } - // read the cargosoft export options ItemCollection evalItemCollection = workflowService.evalWorkflowResult(event, "zoho", document, false); - ZohoInvoice zohoInvoice = zohoAPIService.exportInvoice(document, files); + // get access token + String accessToken = zohoOAuthManager.getValidAccessToken(); + + // Rechnungsausgang + if (document.getModelVersion().startsWith("rechnungsausgang-")) { + logger.info("├── Invoice Export"); + exportRechnungsausgang(document, accessToken); + } + + // Rechnungseingang + if (document.getModelVersion().startsWith("rechnungseingang-")) { + logger.info("├── Bill Export"); + exportRechnungseingang(document, accessToken); + } return document; } + + /** + * Help method, - finds/updates - contact, - export invoice, - change state, + * -attache files + * + * @param document + * @param accessToken + * @throws PluginException + */ + private void exportRechnungsausgang(ItemCollection document, String accessToken) throws PluginException { + // find companyID + String contactID = zohoAPIService.lookupContact(document.getItemValueString("dbtr.name"), "customer", + accessToken); + + ZohoInvoice zohoInvoice = zohoAPIService.exportInvoice(document, contactID, accessToken); + + // Attache Files.... + List files = null; + ItemCollection snapshot = snapshotService.findSnapshot(document); + if (snapshot != null) { + files = snapshot.getFileData(); + if (files != null) { + for (FileData file : files) { + zohoAPIService.attacheFileData(zohoInvoice.getInvoiceId(), file, "invoices", accessToken); + } + } + } + } + + /** + * Help method, - finds/updates - contact, - export invoice, - change state, + * -attache files + * + * @param document + * @param accessToken + * @throws PluginException + */ + private void exportRechnungseingang(ItemCollection document, String accessToken) throws PluginException { + // find companyID + String contactID = zohoAPIService.lookupContact(document.getItemValueString("cdtr.name"), "vendor", + accessToken); + + String accountID = zohoOAuthManager.getAccountId(); + + ZohoBill zohoBill = zohoAPIService.exportBill(document, contactID, accountID, accessToken); + + // Attache Files.... + List files = null; + ItemCollection snapshot = snapshotService.findSnapshot(document); + if (snapshot != null) { + files = snapshot.getFileData(); + if (files != null) { + for (FileData file : files) { + zohoAPIService.attacheFileData(zohoBill.getBillId(), file, "bills", accessToken); + } + } + } + } } \ No newline at end of file diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthManager.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthManager.java index 666c5fe..28300ff 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthManager.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthManager.java @@ -85,6 +85,14 @@ public class ZohoOAuthManager { return null; } + public String getAccountId() throws PluginException { + ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION, false); + if (zohoConfig != null) { + return zohoConfig.getItemValueString("zoho.accountid"); + } + return null; + } + public String getBaseURI() throws PluginException { String uri = ""; ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION, false); @@ -109,21 +117,27 @@ public class ZohoOAuthManager { * @throws PluginException */ @Lock(LockType.WRITE) - public void updateTokens(String code) throws PluginException { + public void updateTokens(ItemCollection zohoConfig) throws PluginException { logger.info("├── update Zoho tokens"); try { - - ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION, false); + String code = zohoConfig.getItemValueString("zoho.code"); String clientID = zohoConfig.getItemValueString("zoho.clientid"); String clientSecret = zohoConfig.getItemValueString("zoho.clientsecret"); - String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + "?code=" + // String requestUrl = zohoConfig.getItemValueString("zoho.oauth.uri") + + // "?code=" + // + URLEncoder.encode(code, StandardCharsets.UTF_8) + "&client_id=" + // + URLEncoder.encode(clientID, StandardCharsets.UTF_8) + "&client_secret=" + // + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) + "&redirect_uri=" + // + URLEncoder.encode(redirectURI, StandardCharsets.UTF_8) + + // "&grant_type=authorization_code"; + + String requestUrl = zohoConfig.getItemValueString("zoho.oauth.uri") + "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) + "&client_id=" + URLEncoder.encode(clientID, StandardCharsets.UTF_8) + "&client_secret=" - + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) + "&redirect_uri=" - + URLEncoder.encode(redirectURI, StandardCharsets.UTF_8) + "&grant_type=authorization_code"; + + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) + "&grant_type=authorization_code"; - logger.fine("│ ├── Request URI=" + requestUrl); + logger.info("│ ├── Request URI=" + requestUrl); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(requestUrl)) .header("Content-Type", "application/x-www-form-urlencoded") @@ -133,10 +147,10 @@ public class ZohoOAuthManager { HttpResponse response; response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - logger.fine("│ ├── Response statusCode=" + response.statusCode()); + logger.info("│ ├── Response statusCode=" + response.statusCode()); if (response.statusCode() == 200) { // 1. Response-Logging für Debugging - logger.fine("│ ├── Response Body=" + response.body()); + logger.info("│ ├── Response Body=" + response.body()); if (response.body().contains("error")) { throw new PluginException(ZohoOAuthManager.class.getSimpleName(), ZohoOAuthManager.ERROR_CONFIG, @@ -172,7 +186,7 @@ public class ZohoOAuthManager { String refreshToken = zohoConfig.getItemValueString("zoho.refreshToken"); try { - String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + "?refresh_token=" + String requestUrl = zohoConfig.getItemValueString("zoho.oauth.uri") + "?refresh_token=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) + "&client_id=" + URLEncoder.encode(clientID, StandardCharsets.UTF_8) + "&client_secret=" + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) + "&grant_type=refresh_token"; @@ -218,33 +232,33 @@ public class ZohoOAuthManager { String apiDomain = tokenResponse.getApi_domain(); if (apiDomain != null && !apiDomain.isEmpty()) { - logger.fine("│ ├── apiDomain=" + apiDomain); + logger.info("│ ├── apiDomain=" + apiDomain); zohoConfig.setItemValue("zoho.apiDomain", apiDomain); } String scope = tokenResponse.getScope(); if (scope != null && !scope.isEmpty()) { - logger.fine("│ ├── scope=" + scope); + logger.info("│ ├── scope=" + scope); zohoConfig.setItemValue("zoho.scope", scope); } String accessToken = tokenResponse.getAccess_token(); if (accessToken != null && !accessToken.isEmpty()) { - logger.fine("│ ├── accessToken=" + accessToken); + logger.info("│ ├── accessToken=" + accessToken); zohoConfig.setItemValue("zoho.accessToken", accessToken); } String refreshToken = tokenResponse.getRefresh_token(); if (refreshToken != null && !refreshToken.isEmpty()) { - logger.fine("│ ├── refreshToken=" + refreshToken); + logger.info("│ ├── refreshToken=" + refreshToken); zohoConfig.setItemValue("zoho.refreshToken", refreshToken); } - logger.fine("│ ├── expires_in=" + tokenResponse.getExpires_in()); + logger.info("│ ├── expires_in=" + tokenResponse.getExpires_in()); // Expires-Zeit in Millisekunden berechnen long expiresInSeconds = tokenResponse.getExpires_in() - 60; long currentTimeMillis = System.currentTimeMillis(); long expiresTimeMillis = currentTimeMillis + (expiresInSeconds * 1000); // Expires-Zeit in ein Date-Objekt konvertieren Date expiresDate = new Date(expiresTimeMillis); - logger.fine("│ ├── expire date=" + expiresDate); + logger.info("│ ├── expire date=" + expiresDate); zohoConfig.setItemValue("zoho.expires", expiresDate); zohoConfig.setItemValue("message", "Access Token OK"); configService.save(zohoConfig); diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBill.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBill.java new file mode 100644 index 0000000..5720977 --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBill.java @@ -0,0 +1,90 @@ +package com.alexanderlogistics.zoho.dto; + +import java.time.LocalDate; +import java.util.List; + +import jakarta.json.bind.annotation.JsonbDateFormat; +import jakarta.json.bind.annotation.JsonbProperty; + +public class ZohoBill { + + @JsonbProperty("bill_id") + private String billId; + + @JsonbProperty("vendor_id") + private String vendorId; + + @JsonbDateFormat("yyyy-MM-dd") + private LocalDate date; + + @JsonbProperty("due_date") + @JsonbDateFormat("yyyy-MM-dd") + private LocalDate dueDate; + + @JsonbProperty("bill_number") + private String invoiceNumber; + + @JsonbProperty("line_items") + private List lineItems; + + private double amount; + + // Getter und Setter + + public String getBillId() { + return billId; + } + + public void setBillId(String billId) { + this.billId = billId; + } + + public String getVendorId() { + return vendorId; + } + + public void setVendorId(String vendorId) { + this.vendorId = vendorId; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public LocalDate getDueDate() { + return dueDate; + } + + public void setDueDate(LocalDate duedate) { + this.dueDate = duedate; + } + + public String getInvoiceNumber() { + return invoiceNumber; + } + + public void setInvoiceNumber(String invoiceNumber) { + this.invoiceNumber = invoiceNumber; + } + + public List getLineItems() { + return lineItems; + } + + public void setLineItems(List lineItems) { + this.lineItems = lineItems; + } + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } + +} diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBillCreateResponse.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBillCreateResponse.java new file mode 100644 index 0000000..2c80b5a --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBillCreateResponse.java @@ -0,0 +1,39 @@ +package com.alexanderlogistics.zoho.dto; + +import jakarta.json.bind.annotation.JsonbProperty; + +public class ZohoBillCreateResponse { + @JsonbProperty("code") + private int code; + + @JsonbProperty("message") + private String message; + + @JsonbProperty("bill") + private ZohoBill bill; + + // Getter und Setter + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public ZohoBill getBill() { + return bill; + } + + public void setBill(ZohoBill bill) { + this.bill = bill; + } +} diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBillItem.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBillItem.java new file mode 100644 index 0000000..0cca422 --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoBillItem.java @@ -0,0 +1,47 @@ +package com.alexanderlogistics.zoho.dto; + +import jakarta.json.bind.annotation.JsonbProperty; + +public class ZohoBillItem { + private String description; + private String name; + private double amount; + + @JsonbProperty("account_id") + private String accountId; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + // Getter und Setter + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } + +} diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoContact.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoContact.java index d715136..5086a79 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoContact.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoContact.java @@ -13,6 +13,9 @@ public class ZohoContact { @JsonbProperty("company_name") private String companyName; + @JsonbProperty("contact_type") + private String contactType; + // Weitere Felder nach Bedarf // Getter und Setter @@ -40,4 +43,12 @@ public class ZohoContact { this.companyName = companyName; } + public String getContactType() { + return contactType; + } + + public void setContactType(String contactType) { + this.contactType = contactType; + } + } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpense.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpense.java index 3dcb56b..3103c56 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpense.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpense.java @@ -11,32 +11,42 @@ public class ZohoExpense { @JsonbProperty("expense_id") private String expenseId; + @JsonbProperty("account_id") + private String accountId; + + @JsonbProperty("paid_through_account_id") + private String paid_through_account_id; + @JsonbProperty("customer_id") private String customerId; @JsonbDateFormat("yyyy-MM-dd") private LocalDate date; - @JsonbProperty("due_date") - @JsonbDateFormat("yyyy-MM-dd") - private LocalDate dueDate; - @JsonbProperty("reference_number") private String invoiceNumber; @JsonbProperty("line_items") - private List lineItems; + private List lineItems; - private double total; + private double amount; // Getter und Setter - public String getInvoiceId() { - return invoiceId; + public String getExpenseId() { + return expenseId; } - public void setInvoiceId(String invoiceId) { - this.invoiceId = invoiceId; + public void setExpenseId(String expenseId) { + this.expenseId = expenseId; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; } public String getCustomerId() { @@ -55,14 +65,6 @@ public class ZohoExpense { this.date = date; } - public LocalDate getDueDate() { - return dueDate; - } - - public void setDueDate(LocalDate duedate) { - this.dueDate = duedate; - } - public String getInvoiceNumber() { return invoiceNumber; } @@ -71,19 +73,27 @@ public class ZohoExpense { this.invoiceNumber = invoiceNumber; } - public List getLineItems() { + public List getLineItems() { return lineItems; } - public void setLineItems(List lineItems) { + public void setLineItems(List lineItems) { this.lineItems = lineItems; } - public double getTotal() { - return total; + public double getAmount() { + return amount; } - public void setTotal(double total) { - this.total = total; + public void setAmount(double amount) { + this.amount = amount; + } + + public String getPaid_through_account_id() { + return paid_through_account_id; + } + + public void setPaid_through_account_id(String paid_through_account_id) { + this.paid_through_account_id = paid_through_account_id; } } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpenseCreateResponse.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpenseCreateResponse.java new file mode 100644 index 0000000..53e90c9 --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpenseCreateResponse.java @@ -0,0 +1,39 @@ +package com.alexanderlogistics.zoho.dto; + +import jakarta.json.bind.annotation.JsonbProperty; + +public class ZohoExpenseCreateResponse { + @JsonbProperty("code") + private int code; + + @JsonbProperty("message") + private String message; + + @JsonbProperty("expense") + private ZohoExpense expense; + + // Getter und Setter + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public ZohoExpense getExpense() { + return expense; + } + + public void setExpense(ZohoExpense expense) { + this.expense = expense; + } +} diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpenseItem.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpenseItem.java new file mode 100644 index 0000000..7837b3e --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoExpenseItem.java @@ -0,0 +1,16 @@ +package com.alexanderlogistics.zoho.dto; + +public class ZohoExpenseItem { + private String description; + + // Getter und Setter + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/office-alexander-logistics-app/src/main/webapp/pages/admin/zoho.xhtml b/office-alexander-logistics-app/src/main/webapp/pages/admin/zoho.xhtml index 486afd4..c074abc 100644 --- a/office-alexander-logistics-app/src/main/webapp/pages/admin/zoho.xhtml +++ b/office-alexander-logistics-app/src/main/webapp/pages/admin/zoho.xhtml @@ -42,12 +42,28 @@
+ +
+
API OAuth URL * + +
+
+ + + +
+
+ + +
API Base URL *
+
@@ -60,6 +76,17 @@ + +
+
Accountant ID *
+
+ + +
+
+ +
Client ID *
@@ -106,7 +133,7 @@ Generieren Sie einen neuen Access Code
Scope: - ZohoBooks.contacts.ALL,ZohoBooks.invoices.CREATE,ZohoBooks.invoices.READ,ZohoBooks.invoices.UPDATE + ZohoBooks.contacts.ALL,ZohoBooks.invoices.ALL,ZohoBooks.bills.ALL
  • Code über den Button "Create" anfordern und in das Feld "Access Code" diff --git a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/zoho/TestUpdateTokens.java b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/zoho/TestUpdateTokens.java index 1e7a144..30a15d7 100644 --- a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/zoho/TestUpdateTokens.java +++ b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/zoho/TestUpdateTokens.java @@ -42,8 +42,23 @@ public class TestUpdateTokens { private String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant"; - private String clientID = "1000.GR5FTPZYB06HEWSFSSX4DJU3OBD4EJ"; - private String clientSecret = "3f3e74c5212bbc924a7ebf77f887c6296db1106c47"; + // private String clientID = "1000.GR5FTPZYB06HEWSFSSX4DJU3OBD4EJ"; + // private String clientSecret = "3f3e74c5212bbc924a7ebf77f887c6296db1106c47"; + + // forum + // private String clientID = "1000.ECQXCXYOTQ9LGLRK2V4PYU94OORJKR"; + // private String clientSecret = "ce93df3860fc3d804cd015a0bec9b92dcfcf6329b8"; + + // DWC + // private String clientID = "1000.MQE1SU8UGY6F2PBPASOXGTLWYQD1FS"; + // private String clientSecret = "1ca70e1ab803b3fed6b1b5e00858c316e2003d289b"; + + // Atari + private String clientID = "1000.86O4P321HDYDRX0Q0N1G7YOJRJ9D5W"; + private String clientSecret = "2a67b6cfc364f5df27cc8eb33e9b0e4e8a0221ebc3"; + + // ROLES + // ZohoBooks.contacts.ALL,ZohoBooks.invoices.ALL,ZohoBooks.bills.ALL private String code = null; private String accessToken = null; @@ -67,14 +82,20 @@ public class TestUpdateTokens { logger.info("├── update Zoho tokens"); refreshToken = null; accessToken = null; - code = "1000.48bd85a3965cf724f4691757cfd8d93b.8ae5e3ee7d70f90d307da4d289a9b067"; + code = "1000.d2a4e3a9622fc4c5eebe1ae5ee1989b5.d6899d23da56e6d8f73125ebaa498a3e"; try { - String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + "?code=" + // String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + "?code=" + // + URLEncoder.encode(code, StandardCharsets.UTF_8) + "&client_id=" + // + URLEncoder.encode(clientID, StandardCharsets.UTF_8) + "&client_secret=" + // + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) + "&redirect_uri=" + // + URLEncoder.encode(redirectURI, StandardCharsets.UTF_8) + + // "&grant_type=authorization_code"; + + String requestUrl = "https://accounts.zoho.com/oauth/v2/token" + "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) + "&client_id=" + URLEncoder.encode(clientID, StandardCharsets.UTF_8) + "&client_secret=" - + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) + "&redirect_uri=" - + URLEncoder.encode(redirectURI, StandardCharsets.UTF_8) + "&grant_type=authorization_code"; + + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) + "&grant_type=authorization_code"; logger.info("│ ├── Request URI=" + requestUrl);