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;
- /* *******************************************************************************
- 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;
- /* *******************************************************************************
- 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):
- Stripe Dashboard - Developers - API Keys
- Stripe Docs - Decline codes - Learn about decline codes and how to resolve them when a charge fails.