diff --git a/doc/zoho/README.md b/doc/zoho/README.md index 71dab2a..299445f 100644 --- a/doc/zoho/README.md +++ b/doc/zoho/README.md @@ -8,6 +8,7 @@ https://api-console.zoho.eu/ https://api-console.zoho.eu/client/1000.NLTM2KUEHI696MSLGEIANATVGGX7IN ``` +OrganizationID=20105697367 client ID= 1000.NLTM2KUEHI696MSLGEIANATVGGX7IN client secret= ee9ba49f16e77c64abb89cea13542d2326d8d03ce8 ``` 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 new file mode 100644 index 0000000..d32068c --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoController.java @@ -0,0 +1,74 @@ +package com.alexanderlogistics.zoho; + +/******************************************************************************* + * Imixs Workflow Technology + * Copyright (C) 2003, 2008 Imixs Software Solutions GmbH, + * http://www.imixs.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You can receive a copy of the GNU General Public + * License at http://www.gnu.org/licenses/gpl.html + * + * Contributors: + * Imixs Software Solutions GmbH - initial API and implementation + * Ralph Soika + * + *******************************************************************************/ + +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.inject.Inject; +import jakarta.inject.Named; + +/** + * Der ZohoController ist für das Admin Interface um Access Tokens zu erzeugen. + * + * @see admin/zoho.xhml + * + * @author rsoika + * + */ +@Named("zohoController") +@ApplicationScoped +public class ZohoController extends ConfigController { + + public static final String ZOHO_CONFIGURATION = "ZOHO_CONFIGURATION"; + + private static final long serialVersionUID = 1L; + private static Logger logger = Logger.getLogger(ZohoController.class.getName()); + + @Inject + ZohoOAuthManager zohoOAuthManager; + + public ZohoController() { + super(); + setName(ZOHO_CONFIGURATION); + } + + /** + * Ruft zohoOAuhtManager.updateToken auf + */ + public void updateAccessToken() { + // + try { + zohoOAuthManager.updateTokens(this.getWorkitem().getItemValueString("zoho.code")); + } catch (PluginException e) { + String message = "Failed to generate token: " + e.getMessage(); + logger.warning(message); + this.getWorkitem().setItemValue("message", message); + } + } +} diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthGrantService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthGrantService.java index 33c5544..3d195e9 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthGrantService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthGrantService.java @@ -117,9 +117,9 @@ public class ZohoOAuthGrantService { logger.info("│ ├── location=" + location); logger.info("│ ├── accounts-server=" + accountsServer); - zohoOAuthManager.updateTokens(code); + // zohoOAuthManager.updateTokens(code); - return "Zoho Grant Token received!"; + return "Zoho Callback received!"; } } 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 f918417..90b46b3 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 @@ -8,18 +8,18 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.Optional; import java.util.logging.Logger; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.imixs.workflow.exceptions.InvalidAccessException; +import org.imixs.workflow.ItemCollection; +import org.imixs.workflow.exceptions.PluginException; +import org.imixs.workflow.office.config.ConfigService; import jakarta.annotation.PostConstruct; import jakarta.annotation.security.RunAs; +import jakarta.ejb.EJB; import jakarta.ejb.Lock; import jakarta.ejb.LockType; import jakarta.ejb.Singleton; -import jakarta.inject.Inject; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; @@ -39,13 +39,11 @@ public class ZohoOAuthManager { private String accessToken; private long accessTokenExpiry; // Timestamp (System.currentTimeMillis() + expires_in*1000) - @Inject - @ConfigProperty(name = "zoho.client.id", defaultValue = "1000.NLTM2KUEHI696MSLGEIANATVGGX7IN") - Optional clientID; + private String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant"; + public static final String ERROR_CONFIG = "CONFIG_ERROR"; - @Inject - @ConfigProperty(name = "zoho.client.secret", defaultValue = "ee9ba49f16e77c64abb89cea13542d2326d8d03ce8") - Optional clientSecret; + @EJB + ConfigService configService; private HttpClient httpClient; private Jsonb jsonb; @@ -67,35 +65,25 @@ public class ZohoOAuthManager { * * * @param code + * @throws PluginException */ @Lock(LockType.WRITE) - public void updateTokens(String code) { + public void updateTokens(String code) throws PluginException { logger.info("├── update Zoho tokens"); - if (!clientID.isPresent()) { - throw new InvalidAccessException("Missing client ID"); - } - if (!clientSecret.isPresent()) { - throw new InvalidAccessException("Missing client secret"); - } + refreshToken = null; + accessToken = null; + try { - String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant"; - - // Korrekte URL-Konstruktion mit Query-Parametern - // String requestUrl = "https://accounts.zoho.com/oauth/v2/token" + - // "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) + - // "&client_id=" + URLEncoder.encode(clientID.get(), StandardCharsets.UTF_8) + - // "&client_secret=" + URLEncoder.encode(clientSecret.get(), - // StandardCharsets.UTF_8) + - // "&redirect_uri=" + URLEncoder.encode("http://www.zoho.com/books", - // StandardCharsets.UTF_8) + - // "&grant_type=authorization_code"; + ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION); + String clientID = zohoConfig.getItemValueString("zoho.clientid"); + String clientSecret = zohoConfig.getItemValueString("zoho.clientsecret"); String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) + - "&client_id=" + URLEncoder.encode(clientID.get(), StandardCharsets.UTF_8) + - "&client_secret=" + URLEncoder.encode(clientSecret.get(), 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"; @@ -108,36 +96,44 @@ public class ZohoOAuthManager { .build(); HttpResponse response; - response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - logger.info("│ ├── Request statusCode=" + response.statusCode()); + logger.info("│ ├── Response statusCode=" + response.statusCode()); if (response.statusCode() == 200) { - // 1. Response-Logging für Debugging - logger.info("│ ├── Request statusCode=" + response.body()); + logger.info("│ ├── Response Body=" + response.body()); + + if (response.body().contains("error")) { + throw new PluginException(ZohoOAuthManager.class.getSimpleName(), + ZohoOAuthManager.ERROR_CONFIG, + "Ungültiger Access Code!"); + } TokenResponse tokenResponse = jsonb.fromJson(response.body(), TokenResponse.class); - this.accessToken = tokenResponse.getAccess_token(); - logger.info("│ ├── accessToken=" + accessToken); - this.refreshToken = tokenResponse.getRefresh_token(); - logger.info("│ ├── refreshToken=" + refreshToken); - this.accessTokenExpiry = System.currentTimeMillis() + - (tokenResponse.getExpires_in() * 1000); + accessToken = tokenResponse.getAccess_token(); + logger.info("│ ├── accessToken=" + accessToken); + refreshToken = tokenResponse.getRefresh_token(); + logger.info("│ ├── refreshToken=" + refreshToken); logger.info("│ ├── expires_in=" + tokenResponse.getExpires_in()); + + zohoConfig.setItemValue("zoho.accessToken", accessToken); + zohoConfig.setItemValue("zoho.refreshToken", refreshToken); + zohoConfig.setItemValue("zoho.expires", tokenResponse.getExpires_in()); + zohoConfig.setItemValue("message", "Access Token OK"); + configService.save(zohoConfig); } else { throw new RuntimeException("Failed to get tokens: " + response.statusCode() + " - " + response.body()); } } catch (IOException | InterruptedException e) { - throw new InvalidAccessException("Unable to resolve access tokens: " + e.getMessage()); + throw new PluginException(ZohoOAuthManager.class.getSimpleName(), + ZohoOAuthManager.ERROR_CONFIG, "Unable to resolve access tokens: " + e.getMessage()); } - } @Lock(LockType.READ) @@ -146,4 +142,59 @@ public class ZohoOAuthManager { return null; } + /** + * This method updates the Access Token based on the current Refresh Token + * + * @throws IOException + */ + public void testRefreshAccessToken() throws IOException { + + // logger.info("├── refresh Zoho access token"); + // refreshToken = + // "1000.cf458816e97f848df8134ce1c7e923a1.5b9dbdee317e35d7f0c619e7648d55ff"; + // try { + + // String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + + // "?refresh_token=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) + + // "&client_id=" + URLEncoder.encode(clientID.get(), StandardCharsets.UTF_8) + + // "&client_secret=" + URLEncoder.encode(clientSecret.get(), + // StandardCharsets.UTF_8) + + // "&grant_type=refresh_token"; + + // logger.info("│ ├── Request URI=" + requestUrl); + + // HttpRequest request = HttpRequest.newBuilder() + // .uri(URI.create(requestUrl)) + // .header("Content-Type", "application/x-www-form-urlencoded") + // .POST(HttpRequest.BodyPublishers.noBody()) // Wichtig: Kein Body! + // .build(); + + // HttpResponse response; + + // response = httpClient.send(request, + // HttpResponse.BodyHandlers.ofString()); + + // logger.info("│ ├── Response statusCode=" + response.statusCode()); + // if (response.statusCode() == 200) { + + // // 1. Response-Logging für Debugging + // logger.info("│ ├── Response Body=" + response.body()); + + // TokenResponse tokenResponse = jsonb.fromJson(response.body(), + // TokenResponse.class); + + // String accessToken = tokenResponse.getAccess_token(); + // logger.info("│ ├── accessToken=" + accessToken); + // logger.info("│ ├── expires_in=" + tokenResponse.getExpires_in()); + // } else { + // throw new RuntimeException("Failed to get tokens: " + + // response.statusCode() + " - " + response.body()); + // } + + // } catch (IOException | InterruptedException e) { + // throw new InvalidAccessException("Unable to resolve access tokens: " + + // e.getMessage()); + // } + + } } diff --git a/office-alexander-logistics-app/src/main/webapp/layout/commandbox_admin_custom.xhtml b/office-alexander-logistics-app/src/main/webapp/layout/commandbox_admin_custom.xhtml index 9144306..de35d21 100644 --- a/office-alexander-logistics-app/src/main/webapp/layout/commandbox_admin_custom.xhtml +++ b/office-alexander-logistics-app/src/main/webapp/layout/commandbox_admin_custom.xhtml @@ -23,9 +23,12 @@
  • OP List Export
  • -
  • +
  • Parameter
  • +
  • + Zoho +
  • \ No newline at end of file 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 new file mode 100644 index 0000000..f137890 --- /dev/null +++ b/office-alexander-logistics-app/src/main/webapp/pages/admin/zoho.xhtml @@ -0,0 +1,150 @@ + + + + + + + + + +
    +
    +

    ZOHO - API

    + + +
    + +
    +
    + + +
    + + +
    + + +
    +
    +
    Client ID
    +
    + + + +
    +
    +
    +
    Client Secret
    +
    + + +
    +
    +
    +
    Organization
    +
    + + +
    +
    +
    + + +
    +
    +
    Access Code
    +
    + + +
    +
    +

    + Fordern Sie einen ZOHO API COde + an + +

      +
    1. Melden Sie sich auf der ZOHO API Console an
    2. +
    3. + Legen Sie einen API Self Client an +
    4. +
    5. + Kopieren Sie Client ID und Client Secret sowie die Organization ID +
    6. +
    7. + Generieren Sie einen neuen Access Code +
      + Scope:
      + ZohoBooks.invoices.CREATE,ZohoBooks.invoices.READ,ZohoBooks.invoices.UPDATE,ZohoBooks.invoices.DELETE +
    8. +
    9. + Code über den Button "Create" anfordern und in das Feld "Access Code" + übertragen +
    10. + +
    11. + Access Token über den Button "Access Token Generieren" speichern +
    12. +
    + + + +
    + +
    + AccessToken: + +
    + RefreshToken: + +

    + + +
    + +
    +
    +
    +
    + + + +
    +
    +
    +
    + + + +
    \ No newline at end of file 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 202ff8f..37245c6 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 @@ -1,5 +1,8 @@ package com.alexanderlogistics.zoho; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import java.io.IOException; import java.net.URI; import java.net.URLEncoder; @@ -35,11 +38,15 @@ public class TestUpdateTokens { private HttpClient httpClient; private Jsonb jsonb; - private String code = "1000.a6cff79178a76099a625eb41249a6c91.b3679ff9365902502d2e3ca55303d5d6"; + private String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant"; private String clientID = "1000.GR5FTPZYB06HEWSFSSX4DJU3OBD4EJ"; private String clientSecret = "3f3e74c5212bbc924a7ebf77f887c6296db1106c47"; + private String code = null; + private String accessToken = null; + private String refreshToken = null; + @BeforeEach public void setup() throws PluginException, ModelException { logger.info("setup test environment ..."); @@ -58,11 +65,11 @@ public class TestUpdateTokens { public void testUpdateTokens() throws IOException { logger.info("├── update Zoho tokens"); - + refreshToken = null; + accessToken = null; + code = "1000.48bd85a3965cf724f4691757cfd8d93b.8ae5e3ee7d70f90d307da4d289a9b067"; try { - String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant"; - String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8) + "&client_id=" + URLEncoder.encode(clientID, StandardCharsets.UTF_8) + @@ -83,18 +90,22 @@ public class TestUpdateTokens { response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - logger.info("│ ├── Request statusCode=" + response.statusCode()); + logger.info("│ ├── Response statusCode=" + response.statusCode()); if (response.statusCode() == 200) { // 1. Response-Logging für Debugging - logger.info("│ ├── Request statusCode=" + response.body()); + logger.info("│ ├── Response Body=" + response.body()); + + assertNotNull(response.body()); + assertFalse(response.body().contains("error")); TokenResponse tokenResponse = jsonb.fromJson(response.body(), TokenResponse.class); - String accessToken = tokenResponse.getAccess_token(); + accessToken = tokenResponse.getAccess_token(); + assertNotNull(accessToken); logger.info("│ ├── accessToken=" + accessToken); - String refreshToken = tokenResponse.getRefresh_token(); + refreshToken = tokenResponse.getRefresh_token(); logger.info("│ ├── refreshToken=" + refreshToken); logger.info("│ ├── expires_in=" + tokenResponse.getExpires_in()); } else { @@ -108,4 +119,152 @@ public class TestUpdateTokens { } + /** + * Testet den Refresh Token + * + * POST: + * {Accounts_URL}/oauth/v2/token?refresh_token={refresh_token}&client_id={client_id}&client_secret={client_secret}&grant_type=refresh_token + * + * @throws IOException + */ + @Test + public void testRefreshAccessToken() throws IOException { + + logger.info("├── refresh Zoho access token"); + refreshToken = "1000.cf458816e97f848df8134ce1c7e923a1.5b9dbdee317e35d7f0c619e7648d55ff"; + try { + + String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + + "?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"; + + logger.info("│ ├── Request URI=" + requestUrl); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.noBody()) // Wichtig: Kein Body! + .build(); + + HttpResponse response; + + response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString()); + + logger.info("│ ├── Response statusCode=" + response.statusCode()); + if (response.statusCode() == 200) { + + // 1. Response-Logging für Debugging + logger.info("│ ├── Response Body=" + response.body()); + + TokenResponse tokenResponse = jsonb.fromJson(response.body(), + TokenResponse.class); + + String accessToken = tokenResponse.getAccess_token(); + logger.info("│ ├── accessToken=" + accessToken); + logger.info("│ ├── expires_in=" + tokenResponse.getExpires_in()); + } else { + throw new RuntimeException("Failed to get tokens: " + + response.statusCode() + " - " + response.body()); + } + + } catch (IOException | InterruptedException e) { + throw new InvalidAccessException("Unable to resolve access tokens: " + e.getMessage()); + } + + } + + /** + * Testet den eigentlichen API Zugriff mit Hilfe des zuvor generieren Access + * Token + * + * GET: https://{zohoapis_domain}/writer/api/v1/documents + * + * @see https://www.zoho.com/books/api/v3/introduction/#overview + * @throws IOException + */ + @Test + public void testGetOrganizations() throws IOException { + + logger.info("├── get Zoho organizations"); + accessToken = "1000.21912dc6af0906707f51d336fd98ab61.eb153952d0d86a828fcb7e18426c2c1a"; + try { + + String url = "https://www.zohoapis.eu/books/v3/organizations"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .header("Authorization", "Zoho-oauthtoken " + accessToken) + .build(); + + HttpResponse response = httpClient.send( + request, + HttpResponse.BodyHandlers.ofString()); + + logger.info("│ ├── Response statusCode=" + response.statusCode()); + + // 1. Response-Logging für Debugging + logger.info("│ ├── Response Body=" + response.body()); + + if (response.statusCode() >= 300) { + + throw new RuntimeException("API request failed with status code: " + response.statusCode()); + } + + } catch (IOException | InterruptedException e) { + throw new InvalidAccessException("Unable to resolve access tokens: " + e.getMessage()); + } + + } + + /** + * Testet den eigentlichen API Zugriff mit Hilfe des zuvor generieren Access + * Token + * + * + * curl --request GET \ + * --url 'https://www.zohoapis.com/books/v3/invoices?organization_id=10234695' \ + * --header 'Authorization: Zoho-oauthtoken + * 1000.41d9xxxxxxxxxxxxxxxxxxxxxxxxc2d1.8fccxxxxxxxxxxxxxxxxxxxxxxxx125f' + * + * + * @throws IOException + */ + @Test + public void testGetInvoices() throws IOException { + + logger.info("├── get Zoho documents"); + accessToken = "1000.21912dc6af0906707f51d336fd98ab61.eb153952d0d86a828fcb7e18426c2c1a"; + try { + + String url = "https://www.zohoapis.eu/books/v3/invoices?organization_id=20105697367"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .header("Authorization", "Zoho-oauthtoken " + accessToken) + .build(); + + HttpResponse response = httpClient.send( + request, + HttpResponse.BodyHandlers.ofString()); + + logger.info("│ ├── Response statusCode=" + response.statusCode()); + + // 1. Response-Logging für Debugging + logger.info("│ ├── Response Body=" + response.body()); + + if (response.statusCode() >= 300) { + + throw new RuntimeException("API request failed with status code: " + response.statusCode()); + } + + } catch (IOException | InterruptedException e) { + throw new InvalidAccessException("Unable to resolve access tokens: " + e.getMessage()); + } + + } } diff --git a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/zoho/test.json b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/zoho/test.json new file mode 100644 index 0000000..831eaed --- /dev/null +++ b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/zoho/test.json @@ -0,0 +1,97 @@ +{ + "code": 0, + "message": "success", + "invoices": [ + { + "ach_payment_initiated": false, + "invoice_id": "777249000000055583", + "zcrm_potential_id": "", + "customer_id": "777249000000055569", + "zcrm_potential_name": "", + "customer_name": "Mr. Foo", + "company_name": "", + "status": "overdue", + "invoice_number": "INV-000001", + "reference_number": "23452345", + "date": "2025-04-21", + "due_date": "2025-04-21", + "due_days": "Overdue by 3 days", + "email": "ralph.soika@imixs.com", + "project_name": "", + "billing_address": { + "address": "", + "street2": "", + "city": "", + "state": "", + "zipcode": "", + "country": "", + "phone": "", + "fax": "", + "attention": "" + }, + "shipping_address": { + "address": "", + "street2": "", + "city": "", + "state": "", + "zipcode": "", + "country": "", + "phone": "", + "fax": "", + "attention": "" + }, + "country": "", + "phone": "", + "created_by": "Ralph Soika", + "total": 50.00, + "balance": 50.00, + "payment_expected_date": "", + "custom_fields": [], + "custom_field_hash": {}, + "tags": [], + "salesperson_name": "", + "shipping_charge": 0.00, + "adjustment": 0.00, + "created_time": "2025-04-21T13:36:26+0200", + "last_modified_time": "2025-04-21T13:42:56+0200", + "updated_time": "2025-04-21T13:42:56+0200", + "is_viewed_by_client": false, + "has_attachment": true, + "client_viewed_time": "", + "is_viewed_in_mail": false, + "is_emailed": true, + "mail_first_viewed_time": "", + "mail_last_viewed_time": "", + "is_correction_invoice": false, + "color_code": "", + "current_sub_status_id": "", + "current_sub_status": "overdue", + "currency_id": "777249000000000071", + "schedule_time": "", + "currency_code": "EUR", + "currency_symbol": "€", + "template_type": "standard", + "invoice_url": "https://zohosecurepay.eu/books/imixs/secure?CInvoiceID=2-64277fbd3cd8bc86f8e581a3a7e1544618c191c094de1ae938827291b0828c92b5f796a1ec94bfb1fdcce7621ddf18bf61770563d9fd283207b7fa0e24d232a416b0b5e010c61cd7 ", + "transaction_type": "renewal", + "reminders_sent": 0, + "last_reminder_sent_date": "", + "last_payment_date": "", + "template_id": "777249000000000103", + "documents": "", + "salesperson_id": "", + "write_off_amount": 0.00, + "exchange_rate": 1.00, + "unprocessed_payment_amount": 0.00 + } + ], + "page_context": { + "page": 1, + "per_page": 200, + "has_more_page": false, + "report_name": "Invoices", + "applied_filter": "Status.All", + "custom_fields": [], + "sort_column": "created_time", + "sort_order": "D" + } +} \ No newline at end of file