Articles

Zoho: Email Deliverability / SPF / DKIM / DMARC / Toolkit

What?
A quick article on the SPF & DKIM setup to improve email deliverability from a ZohoApp and methods to check.

Why?
Not sure how I came across it as most of our customers don't use ZohoMail, but there is a Zoho Toolkit to run some checks on the email deliverability for a customer: https://zohomail.tools/#runChecks.

How?
So first I'll outline the steps to set this up in ZohoCRM and then how to troubleshoot issues with this. One important thing to note is that the setup needs to be done by whoever manages the domain for the client.
ZohoCRM Webhook: Create ZohoInventory Records from an eBay order

What?
This is a not-so-quick article that queries an eBay order and creates the relevant ZohoInventory item, contact, sales order, package slip, shipment order, invoice, payment records...

Why?
I'm storing this here as a reference and taking the bits that I need for the various clients that request an eBay integration. This is a comprehensive snippet which does the whole lot.

How?
Using the method of "mega-functions", here is the code snippet for one function which will accept as parameter the eBay order reference and generate all the respective records in Zoho Inventory. We're using a ZohoCRM webhook, because CRM webhooks run more reliably then the ones we have found in ZohoBooks and other Zoho Apps.

Zoho Deluge: Get All Orders from eBay

What?
Following on from my article: ZohoCRM: Get All eBay Active Listings, this is how to get all the orders from a client's eBay.

Why?
Our use-case is a data migration from eBay to a fresh instance of Zoho Inventory. In this task, we are simply getting a CSV with all the order IDs for a particular year output as a spreadsheet.

How?
Similar to how we retrieved the eBay active listings (see article link above), we're going to use the GetOrders call for order (sales) management.

ZohoCRM: Get Organization Business Hours using Deluge/API

What?
A super quick article on getting the business hours set in ZohoCRM.

Why?
When creating a booking system in ZohoCreator, I want to enter the default working shift for an employee either existing or that has been added to the system based on the business hours set at the organization level in ZohoCRM.

How?
Using an invokeURL, the key here is you'll find them in the settings; so check that your connection has the scope to access both Organization details (ZohoCRM.org.READ is enough - perhaps optional) and access to Settings (ZohoCRM.settings.READ). I'm calling mine "mycrmconnection".
ZohoDeluge: Check Shipment Status via DHL API

What?
A quick note here on how to connect to the DHL API and check on a package given a tracking number.

Why?
In our megafunction to generate a customer, a product/item, a sales order, an invoice, a package slip/delivery note, a shipment order; as well as record any payments, inventory adjustments, status changes; all from an eBay Order coming into the system via a webhook... I would like to mark a shipment as delivered only if the courier, in this case DHL, confirms the tracked order was delivered.

How?
So first I'll give some brief instructions on how to get an API key from DHL as a developer and then I'll include the code to query the shipment status based on a tracking number.

ZohoAnalytics: Pivot Campaigns vs Contacts

What?
A very quick article of a quick solution but at least a working example of an ANSI-SQL (ZohoSQL) query pivoting campaign results vs contacts.

Why?
Simply the client wanted a report on customers on each row and then the campaigns as columns.

What we want:
Contact Name     Lead Source     Campaign 1     Campaign 2     Campaign 3
---------------- --------------- -------------- -------------- --------------
Joel             Google Ads      Accepted       Invited        Sent
Me               Web             Invited        Accepted       Sent
Myself           Walk-In         Accepted       Sent           Invited          
The catch is that our campaign names are in data rows not individual columns.

How?
There may be ways of doing this using the GUI and a "Pivot View" report but I always resort to a query when I get confused with the GUI:

ZohoBooks: Stripe Terminal Integration

What?
A quick article on some code added to a button in ZohoBooks off the invoice module to initiate your Stripe terminal to take a payment.

Why?
Just to make it easy on the staff at a counter or on the phone. They bring up the invoie in ZohoBooks, click on the button, and the Stripe terminal will ask for the amount on the invoice.

Well almost. We've gone the extra step in that we added a custom field that can override the full balance, to allow partial payments such as a deposit or instalment.

How?
I won't go in to how to create a button in ZohoBooks but you simply add it to the invoice and then when it prompts for some code, you give it the snippet below.

The Magic
You would create a button for each terminal
/* *******************************************************************************
Function:       Map Take_Payment( Map invoice, Map organization, Map user)
Label:          Take Payment
Trigger:        On button click
Purpose:		Preps stripe terminal to take payment for balance of invoice.
Inputs:         invoice
Outputs:        -

Date Created:   2023-02-24 
                - Initial release
				- Reads Books Invoice and sends the amount to the reader for a payment atempt
Date Modified:	2023-02-24 (Joel Lipman)
                - If custom field "Amount To Be Taken" is not greater than zero, then defaults to balance due of invoice.

More Information:
		TEST PAYMENT SCENARIOS WITH PHYSICAL TEST CARD 
		// Send in as payment endings: eg. $100.00 == payment approved
		00 Payment Approved
		01 Payment Declined // call issuer code
		05 Payment Declined Generic
		55 Payment Declined Incorrect Pin
		65 Payment Declined withdrawal_count_limit_exceeded
		75 Pin try exceeded

******************************************************************************* */
v_BooksOrgID = organization.get("organization_id");
//
// some Stripe variables (add your own here)
v_StripeTerminalID = "tmr_ABCDEFGabcdefg";
v_StripeCustomerKey = "sk_live_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ";
V_StripeLocationsEndpoint = " https://api.stripe.com/v1/terminal/locations";
//
// set Stripe header
m_Headers = Map();
m_Headers.put("Authorization","Bearer " + v_StripeCustomerKey);
//
// default to balance due on this Invoice
v_AmountToPay = ifnull(invoice.get("balance"),0);
//
// Get custom Amount to Pay from Invoice
l_CustomFields = invoice.get("custom_fields");
if(l_CustomFields.size() > 0)
{
	for each  m_CustomField in l_CustomFields
	{
		if(m_CustomField.get("label") == "Amount To Be Taken")
		{
			if(m_CustomField.get("value") > 0)
			{
				v_AmountToPay = m_CustomField.get("value");
			}
		}
	}
}
//
// format to Stripe amount
v_AmountToPay = v_AmountToPay.truncate(2);
v_StripeAmount = v_AmountToPay * 100;
v_StripeAmount = v_StripeAmount.floor();
v_StripeAmount = v_StripeAmount.toNumber();
info v_StripeAmount;
//
// Create payment intent in Stripe
v_PaymentIntentEndpoint = "https://api.stripe.com/v1/payment_intents";
m_Params = Map();
m_Params.put("amount",v_StripeAmount);
m_Params.put("currency","gbp");
//m_Params.put("automatic_payment_methods[enabled]", false);
m_Params.put("payment_method_types[]","card_present");
m_Params.put("capture_method","manual");
v_DescriptionString = "IN: " + invoice.get("invoice_number") + " ID: " + invoice.get("invoice_id");
m_Params.put("description",v_DescriptionString);
// Later Add Code to  "customer_id": "123456700000001234567", get customer id then Email
v_BooksCustomerID = invoice.get("customer_id");
r_CustomerDetails = zoho.books.getRecordsByID("contacts",v_BooksOrgID,v_BooksCustomerID,"zbooks");
v_CustomerCheckCode = r_CustomerDetails.get("code");
if(v_CustomerCheckCode == 0)
{
	m_ContactDetails = r_CustomerDetails.get("contact");
	if(m_ContactDetails != null)
	{
		m_Params.put("receipt_email",m_ContactDetails.get("email"));
	}
}
r_CreatePaymentIntent = invokeurl
[
	url :v_PaymentIntentEndpoint
	type :POST
	parameters:m_Params
	headers:m_Headers
];
info "Payment Intent Create";
info r_CreatePaymentIntent;
//
v_CheckObject = ifnull(r_CreatePaymentIntent.get("object"),"-");
v_CheckAmount = ifnull(r_CreatePaymentIntent.get("amount"),"-");
//
// Process Payment Intent
if(v_CheckObject == "payment_intent")
{
	v_PaymentIntentID = r_CreatePaymentIntent.get("id");
	info "Payment Intent Created Successfuly!!! ID: ";
	info v_PaymentIntentID;
	//
	// Hand Off Payment Intent to Reader
	v_ReaderHandOffEndpoint = "https://api.stripe.com/v1/terminal/readers/" + v_StripeTerminalID + "/process_payment_intent";
	m_PaymentHandOffParams = Map();
	m_PaymentHandOffParams.put("payment_intent",v_PaymentIntentID);
	r_ReaderPaymentHandOff = invokeurl
	[
		url :v_ReaderHandOffEndpoint
		type :POST
		parameters:m_PaymentHandOffParams
		headers:m_Headers
	];
	info r_ReaderPaymentHandOff;
}
return r_ReaderPaymentHandOff;


The incoming webhook
You now need to receive the Stripe webhook when it comes back into ZohoBooks to record it against the invoice. Note how we added the invoice reference and Zoho ID in the description of the payment capture in our previous bit of code:

/* *******************************************************************************
Function:       Map stripe_terminal_payment( Map invoice, Map organization, Map user)
Label:          stripe_terminal_payment
Trigger:        Incoming Webhook
Purpose:		Listens for stripe terminal payments.
OAuth URL:		https://www.zohoapis.com/books/v3/settings/incomingwebhooks/iw_stripe_terminal_payment/execute?auth_type=oauth
Inputs:         invoice
Outputs:        -

Date Created:   2023-03-23 
                - Initial release
				- been successfully proccessed by the physical reader
Date Modified:	2024-05-21 (Joel Lipman)
                - Revamp of code as per best practices
                - Correct error: Check and update the code in line 74 as there is a Exception : Value is empty and 'get' function cannot be applied

More Information:
		Navigate to payments, then find pending webhook response event, terminal.reader.action_succeeded, then view event details

******************************************************************************* */
//
// initialize
m_Blank = Map();
m_Response = Map();
v_BooksOrgID = organization.get("organization_id");
//
// Stripe API Key (add your own here)
v_StripeCustomerKey = "sk_live_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ";
//
// set Stripe header
m_Headers = Map();
m_Headers.put("Authorization","Bearer " + v_StripeCustomerKey);
//
// capture response webhook
m_Webhook = Map();
m_Webhook.put("data",body.get("data"));
m_Webhook.put("type",body.get("type"));
//
// Check Type 
m_Data = ifnull(m_Webhook.get("data"),m_Blank);
v_Type = ifnull(m_Webhook.get("type"),"-");
if(v_Type == "terminal.reader.action_succeeded")
{
	// Get Payment Intent that needs to be Captured
	m_Object = m_Data.get("object");
	m_Action = m_Object.get("action");
	m_ProcessPaymentIntent = m_Action.get("process_payment_intent");
	v_PaymentIntentID = m_ProcessPaymentIntent.get("payment_intent");
	//
	// Retrieve details on the Payment Intent
	v_PaymentIntentEndpoint = "https://api.stripe.com/v1/payment_intents/" + v_PaymentIntentID;
	r_PaymentIntentDetails = invokeurl
	[
		url :v_PaymentIntentEndpoint
		type :GET
		headers:m_Headers
	];
	//
	// get amount (used to capture payment intent)
	v_Amount = ifnull(r_PaymentIntentDetails.get("amount"),0);
	v_AmountReceived = ifnull(r_PaymentIntentDetails.get("amount_received"),0);
	v_AmountCapturable = ifnull(r_PaymentIntentDetails.get("amount_capturable"),0);
	v_CaptureIntentEndpoint = "https://api.stripe.com/v1/payment_intents/" + v_PaymentIntentID + "/capture";
	m_CaptureParams = Map();
	m_CaptureParams.put("amount_to_capture",v_AmountCapturable);
	r_CapturePayment = invokeurl
	[
		url :v_CaptureIntentEndpoint
		type :POST
		parameters:m_CaptureParams
		headers:m_Headers
	];
	//
	// get card details (we need to store last 4 digits)
	v_StripeReference = "";
	v_Last4Digits = "";
	v_ZB_InvoiceID = 0;
	m_Charges = ifnull(r_PaymentIntentDetails.get("charges"),m_Blank);
	l_Data = ifnull(m_Charges.get("data"),{});
	for each  m_Data in l_Data
	{
		if(m_Data.get("id") != null)
		{
			v_StripeReference = m_Data.get("id");
		}
		if(m_Data.get("payment_method_details") != null)
		{
			m_CardPresent = ifnull(m_Data.get("payment_method_details").get("card_present"),m_Blank);
			v_Last4Digits = ifnull(m_CardPresent.get("last4"),"");
		}
		if(m_Data.get("description") != null)
		{
			v_ChargeDescription = ifnull(m_Data.get("description"),"");
			v_ZB_InvoiceID = v_ChargeDescription.getSuffix("ID: ");
			v_ZB_InvoiceID = if(isNumber(v_ZB_InvoiceID),v_ZB_InvoiceID,0).toLong();
		}
	}
	//
	// create payment record
	if(v_ZB_InvoiceID != 0)
	{
		//
		// get ZohoBooks nominal account for Stripe
		v_NominalAccountID = "";
		r_ChartOfAccounts = invokeurl
		[
			url :"https://www.zohoapis.com/books/v3/chartofaccounts?organization_id=" + v_BooksOrgID
			type :GET
			connection:"zbooks"
		];
		if(r_ChartOfAccounts.get("chartofaccounts") != null)
		{
			for each  m_NomAccount in r_ChartOfAccounts.get("chartofaccounts")
			{
				if(m_NomAccount.get("account_name").equalsIgnoreCase("Stripe Clearing"))
				{
					v_NominalAccountID = m_NomAccount.get("account_id");
				}
			}
		}
		//
		// retrieve invoice details from ZohoBooks
		r_InvoiceDetails = zoho.books.getRecordsByID("invoices",v_BooksOrgID,v_ZB_InvoiceID,"zbooks");
		m_Invoice = ifnull(r_InvoiceDetails.get("invoice"),m_Blank);
		if(m_Invoice.get("customer_id") != null)
		{
			m_CreatePayment = Map();
			m_CreatePayment.put("customer_id",m_Invoice.get("customer_id"));
			m_CreatePayment.put("payment_mode","In Person - Card");
			m_CreatePayment.put("amount",v_AmountReceived / 100);
			m_CreatePayment.put("date",zoho.currentdate.toString("yyyy-MM-dd"));
			//
			l_Invoices = List();
			m_ThisInvoice = Map();
			m_ThisInvoice.put("invoice_id",v_ZB_InvoiceID.toString());
			m_ThisInvoice.put("amount_applied",v_AmountReceived / 100);
			l_Invoices.add(m_ThisInvoice);
			m_CreatePayment.put("invoices",l_Invoices);
			m_CreatePayment.put("invoice_id",v_ZB_InvoiceID.toString());
			m_CreatePayment.put("amount_applied",v_AmountReceived / 100);
			//
			v_PaymentRef = if(v_StripeReference=="", m_Invoice.get("invoice_number"), v_StripeReference);
			m_CreatePayment.put("reference_number",v_PaymentRef);
			m_CreatePayment.put("account_id",v_NominalAccountID);
			//info m_CreatePayment;
			//
			r_CreatePayment = zoho.books.createRecord("customerpayments",v_BooksOrgID,m_CreatePayment,"zbooks");
			//info r_CreatePayment;
			if(r_CreatePayment.get("message") != null)
			{
				if(r_CreatePayment.get("message").contains("payment has been created"))
				{
					//
					// update the invoice (request by client to store last 4 digits on invoice)
					l_CustomFields = List();
					m_CustomField = Map();
					m_CustomField.put("api_name","cf_last_4_digits");
					m_CustomField.put("value",v_Last4Digits);
					l_CustomFields.add(m_CustomField);
					m_UpdateInvoice = Map();
					m_UpdateInvoice.put("custom_fields",l_CustomFields);
					r_UpdateInvoice = zoho.books.updateRecord("Invoices",v_BooksOrgID,v_ZB_InvoiceID,m_UpdateInvoice,"zbooks");
				}
			}
		}
	}
}
m_Response.put("message",r_CreatePayment.get("message"));
m_Response.put("code",0);
return m_Response;


Source(s):

ZohoCRM: Integrate ChatGPT to ZohoZIA

What?
Following on from the article: Zoho Cliq: Integrate OpenAI and ChatGPT 3.5 Turbo. This is the next step as we get it to look at data within ZohoCRM.

Why?
Because it is a serious improvement upon ZohoZIA and we get a lot of requests around customizing ZohoZIA...

How?
So taking ideas from my previous article for ZohoCliq; let's use what we know to develop a ZohoZIA for a demo system.

There are several steps, so first we will set up the ZIA action "Ask ChatGPT" using the GUI, then we will capture the question using Deluge code and query the OpenAI ChatGPT API to return a response.

Credit where Credit is Due:


Feel free to copy, redistribute and share this information. All that we ask is that you attribute credit and possibly even a link back to this website as it really helps in our search engine rankings.

Disclaimer: Please note that the information provided on this website is intended for informational purposes only and does not represent a warranty. The opinions expressed are those of the author only. We recommend testing any solutions in a development environment before implementing them in production. The articles are based on our good faith efforts and were current at the time of writing, reflecting our practical experience in a commercial setting.

Thank you for visiting and, as always, we hope this website was of some use to you!

Kind Regards,

Joel Lipman
www.joellipman.com

Please publish modules in offcanvas position.