Blog

Send Easy Digital Downloads Transactions to Wave Apps without Zapier

I wrote a little WordPress plugin that connects Easy Digital Downloads (EDD) to Wave Apps bookkeeping using the Wave GraphQL API. If you are accepting Stripe payments using EDD’s Stripe extension or PayPal Payments using EDD’s PayPal Commerce Pro, this EDD extension will automatically move bookkeeping data into your Wave Apps transactions using Wave’s GraphQL API. It saves a ton of data-entry, and picks up where a free Zapier account leaves off.

In the plugin settings panel, you start by entering your Wave “full access token.” Get a token by creating an application in the Wave Apps developer portal. Once saved, this token allows the plugin to query its associated businesses. Choose your business (most cases there will only be one), and click save again. Now you can map your payment accounts and your download products to those matching in your Wave Chart of Accounts. Discounts and fees currently only go a Wave account you’ve set up to handle discounts or fees. If you’d like to see taxes, discounts, and fees developed out further to be more discriminating, get in touch or make a pull request.

I know what you’re thinking… You’re smart! Yes, it might be difficult to trust a third-party plugin with financial data. Sagehen Studio does not receive or transmit any (literally zero) of your data, sensitive or not. We don’t even know you’re using it unless you tell us (so drop a line)! Anyway, this plugin is very small, so it wouldn’t take very long to sift through the code and realize it’s only doing what it says it does: send transaction numbers via HTTP through a secure connection with Wave. And it does it for free.

Enjoy!

How to get a WordPress Media (attachment) ID

There are several ways to go about getting the attachment ID (of the ‘attachment’ WordPress post type, stored in the wp_posts WPDB [Wordpress DataBase] table. Here are some of them:

From the Media Library:

Navigate to Dashboard -> Media. Here you will see your WordPress uploads (Media), either in grid view or list view. They’re both fine, it’s just a preference thing. Instructions for both are below.

You can click the small icons to change the Media view from list to grid or vice versa.

List View

Use your mouse to hover over the chosen media and watch for the post ID in the lower left hand corner of your browser. In this case, the id is 9479.[/caption]

Grid View

In grid view, it’s a little bit different. You’ve got to click on the media file. A popup window will open up. Take a look in your browser URL bar and you will see the attachment ID there, after where it says “?item=” Like this:

In this case, the attachment ID is 9491

Lion Cub – An Ioncube license generator for Easy Digital Downloads

Whew, I went down a rabbit hole with this one! At some point I decided to share time-limited trial versions of plugins with potential customers. Ioncube is a PHP obfuscation/encryption drop in library that’s been around for a long time. It’s been useful to some people and really annoying to other people. Hey though — sometimes code just isn’t free. I thought I’d give it a try using Ioncube as a way to “protect my code.*”

I sell my plugins using Easy Digital Downloads as a seller platform. I find them to be somewhat more suited to digital product sales than WordPress, though sometimes I’m concerned with the direction the new ownership has taken the product. Oh well, it works well enough, and I’m really familiar with it at this point. In fact, I’ve written several plugins for Easy Digital Downloads, including EDDiMark, EDDiStamper PDF, EDD + Wave, and the plugin I’m talking about here: Lion Cub. (If you’re wondering why it’s called that, “Lion Cub” was just a cute and pleasant name I came up with while tossing Ioncube around in my head.) Lion Cub is a simple but pretty well fleshed-out plugin for WordPress and Easy Digital Downloads which allows you to set up automatic Ioncube license creations on-the-fly with Easy Digital Download customer download purchases. Each purchase item can have a license key automatically added to its plugin package/folder on download request, such that the task for the customer to open your plugin with that license is easy/minimal. If you use Ioncube and have thought about protecting your plugins using the Ioncube software, then Lion Cub would make that much simpler for you. The plugin is free.

Learn more about the Lion Cub plugin at Github.


* Looking back, I’m not sure what I was thinking at that time. There are a lot of reasons why shipping obfuscated code is pointless, including the fact that codebases change so often and most of WordPress world is GPL licensed anyway. Thank goodness we are free to change our minds! At this time, I’m not sharing trial versions but I’m not not sharing trial versions.

Custom WP + Online Accounting Application Integrations

Are you paying someone to manually enter your WooCommerce, Easy Digital Downloads, or Gravity Forms sales into Quickbooks Online (QBO), FreshBooks, or Wave accounting software?

Even worse, are you doing it yourself?

Is Zapier not working or your Zap is not successfully moving transactions into your accounting application?

There is another way! Did you know that most reputable online accounting apps feature APIs which allow hands-free data transfer, making manual data entry unnecessary?

I am expert with WordPress and the programming languages it uses. I also know how to use REST and GraphQL APIs. I will write a custom plugin for your WordPress installation which will allow you to take advantage of these APIs. I can securely automate your bookkeeping data entry (customers, invoices, payments, refunds, etc). For a one-time upfront flat rate fee (to develop your plugin) you can save hundreds if not thousands of hours of time and drastically cut your bookkeeping costs.

Get in touch to talk about a custom WordPress + accounting API solution for your small business.

A Side Project

Things have gotten nuts for just about everyone lately, us included. I hope you’re still hanging in there. It’ll get better. It mostly always does. 🙂

If you weren’t aware, I run a small business developing and supporting WordPress plugins. In other words, I’m a computer nerd. Luckily right now the plugins are stable and they can sit tight for a bit. I can focus on customer support… and help run my partner’s business. He lost his longtime secretary to the task of Covid-19 era homeschooling, and I have stepped in to help while we see what this quarter brings. So, I’m very busy, very very busy. Luckily programming for the web lends itself well to streamlining a business, and we’ve been able to automate some bits. Last week I spent some hours over two days writing some code which replaces heaps of manual data entry for his business, practically replacing a full-time employee.

At first I did this using Zapier. We found Zapier a couple months ago, and it’s pretty cool. I was playing with their trial period, creating Zaps to connect his (and my) WordPress-driven sites to our online accounting softwares. This was going pretty well! It was a little “stiff,” meaning I couldn’t tweak or customize as much as I would have liked, but it was doing the trick and saving a bookkeeper a lot of data entry. I pulled the trigger on Zapier’s lower paid plan while it was still 30% off on sale. We were set to go for the year. Until after a week when it became apparent I hadn’t purchased a big enough plan. Whoops! What I didn’t understand yet (because I was busy setting up Zaps and answering phones while barely having enough time left over to do things like eat and shower) is that each Zap can have a lot of tasks. Some of our zaps had 5 or 7 tasks. It just took me a while to understand that each successful “action” is a “task,” and counts towards billing. Whoops, we had chosen the wrong plan.

I wrote to Zapier to ask if they would honor the sale price for a plan upgrade, since we had simply made some beginner mistakes and chosen the wrong plan. I figured they’d just switch us up, especially since I’d been in close contact with them over the on-boarding and had even offered them several carefully thought-out ideas for Zapier UX improvements. Several emails later it was clear this wasn’t going to happen, and well, I’d already rolled up my coding sleeves. Here’s how we replaced our Zaps with… simple PHP, and not much of it.

GraphQL

Things got interesting. I was looking to send data from a WordPress site to a site using a GraphQL-based API, same as Zapier would have to, so I started to look into GraphQL. As sort of a self-taught lone wolf lady developer, I may or may not keep up on all the jargon and the come-and-go new languages and whatnot. But at first glance, it seemed like in order to take advantage of GraphQL, I’d need to involve plugins and node.js and use React, a language I’m not familiar with. All these funky, trademarked words kept popping out at me — words I’d never spent much time thinking about. I started putting together some queries using PHP and JavaScript, then it dawned on me: we’re sending HTTP requests. WordPress does this with a native function, wp_remote_post(). Why am I over-complicating things?

You don’t need fancy

Quickly, I built a plugin to test for an existing customer, add a customer if it didn’t exist, create an invoice, and add a payment inside our online accounting software (Wave). A ton of data entry is done instantaneously, effortlessly. To boot, I could do some data sanitization first, and I could build my own filters and pathways, something Zapier charges more for. This was all built around my method my_remote_post() :

/**
*
* MAKE HTTP REQUEST TO WAVE!
*
*/
private function my_remote_post( $data ) {
    $headers = array(
        // you gotta set up a Wave API connection for this,
        // check out: https://developer.waveapps.com/hc/en-us/articles/360020948171#application
        'Authorization' => 'Bearer 5****************************n',
        'Content-Type' => 'application/json',
    );

    $response = wp_remote_post( 'https://gql.waveapps.com/graphql/public', array(
        'method' => 'POST',
        'timeout' => 45,
        'blocking' => true,
        'headers' => $headers,
        'body' => $data,
        'cookies' => array()
    ) );

    if ( is_wp_error( $response ) ) {
        $error_message = $response->get_error_message();
        error_log( 'Wave HTTP request error' . print_r( $error_message, true) );
        return false;
    } else {
        // for testing:
        // error_log( 'Successful Wave HTTP request response: ' . print_r( $response, true ) );
    }
    return $response;
}

Simple enough, right? This can be adjusted ad lib according to the wp_remote_post spec. Now let’s skip ahead. We’ve gotten some customer data from a submitted payment form. I’ve already used it to check if our customer exists. The customer doesn’t exist. So, let’s automagically create a customer in Wave using GraphQL:

$data = wp_json_encode([
    'query' => 'mutation ($input: CustomerCreateInput!) { customerCreate(input: $input) { didSucceed, inputErrors { code, message, path }, customer { id } } }',
    'variables' => array(
        'input' => array(
            'businessId' => $businessID,
            'name' => $full_name,
            'firstName' => $first_name,
            'lastName' => $last_name,
            'email' => $email,
            'phone' => $phone,
            'currency' => 'USD',
            'address' => array(
                'addressLine1' => $address1,
                'addressLine2' => $address2,
                'city' => $city,
                'provinceCode' => $province,
                'countryCode' => $country,
                'postalCode' => $zip
            )
        )
    )
]);
$response = $this->wp_remote_post( $data );
// maybe make it easier to parse now?
$response = json_decode( $response['body'], true );

So by now if you’re familiar with PHP, you can see how we have easily connected to Wave using the GraphQL language. I’ve put the GraphQL query and variables in a variable called $data, and sent it in the body of an HTTP request. I can adjust my GraphQL query to return only the data I need, and nothing more. Slick. The code above is similar to what I used except I took more advantage of OOP to make form data available throughout my classes. It doesn’t take very long to begin GraphQL queries and how they work. And yes, they’re pretty elegant!

Things I got stuck on

While working on this, Wave’s API (GraphQL) playground became helpful and almost enjoyable. Similar IDEs are available elsewhere, and if you’re learning GraphQL you will likely run into them. I did get a little stuck on things like Wave Business ID…

Wave Business ID
What is it?

Go to “Settings” in your Wave admin panel, and after “https://next.waveapps.com/” in the URL bar will be a very long alphanumeric with dashes. Cut and paste that into an online Base 64 encoder (or use PHP base64_encode()), and you’ll have the business ID to use in GraphQL queries to Wave. You’ll find that then customers and product ids are similar, often the business id, PLUS more characters to specify a hierarchical relationship.

Product ID
A Wave product ID is put together like this:

$waveProductID = $businessID . base64_encode( ';Product:' ) . base64_encode( $program );

Where $program is found under Sales -> Products & Services -> edit chosen product -> find digits after ‘/edit/’ in the URL bar.

“Provinces”
Wave is a Canadian company and speaks of provinces and poutine while we speak of states and potatoes and gravy. The new spec for Wave states is ISO-1366 code (“US-CA” or “US-NJ”), while Zapier was still taking a state slug (“california” or “new-jersey,” for example). You’ve just got to create a lookup table to change your customer’s state input into the ISO-3166 format.

The Bottom Line

The code above isn’t complete (plug-n-play), but if you know someone who writes PHP, it should light the way. With this tip, you’d be able to connect to Wave (or really most any service using GraphQL as an API) to catch a WooCommerce sale using ‘woocommerce_payment_complete’ hook or a Gravity Forms submission using the ‘gform_after_submission’ hook. If you’re hunting around the web looking for help with any of this, I hope this helped. We’re all trying to get through right now, and if I can help save you a little time or money, too, then great!

Capital One + Wave Apps Accounting Integration

Related to this topic is my WordPress plugin which gathers transaction information from Capital One and sends it to Wave via the GraphQL API. You can learn more about that on my Github page for the Capital One + Wave plugin. The plugin makes it much easier to get business and account data (account IDs) from Wave, since it uses the API directly.

An Easy Digital Download Software Licensing Server Attack

If you’ve purchased the Easy Digital Downloads Software Licensing add-on (min price $99/yr) and you have your eyes on API server traffic, you might have noticed that if your site runs CRON jobs as frequently as every minute, you are potentially getting hit by thousands of HTTP requests every hour (depending of course on how many license keys you’ve issued to customers). This can lead to DB errors and worse. That’s just not cool for the price.

EDD provides some “free” example integration code to get users started. Great, but we really shouldn’t use any volunteered code without an audit. Here’s the code they’ve provided:

/**
 * Initialize the updater. Hooked into `init` to work with the
 * wp_version_check cron job, which allows auto-updates.
 */
function edd_sl_sample_plugin_updater() {

	// To support auto-updates, this needs to run during the wp_version_check cron job for privileged users.
	$doing_cron = defined( 'DOING_CRON' ) && DOING_CRON;
	if ( ! current_user_can( 'manage_options' ) && ! $doing_cron ) {
		return;
	}

	// retrieve our license key from the DB
	$license_key = trim( get_option( 'edd_sample_license_key' ) );

	// setup the updater
	$edd_updater = new EDD_SL_Plugin_Updater(
		EDD_SAMPLE_STORE_URL,
		__FILE__,
		array(
			'version' => '1.0',                    // current version number
			'license' => $license_key,             // license key (used get_option above to retrieve from DB)
			'item_id' => EDD_SAMPLE_ITEM_ID,       // ID of the product
			'author'  => 'Easy Digital Downloads', // author of this plugin
			'beta'    => false,
		)
	);

}
add_action( 'init', 'edd_sl_sample_plugin_updater' );

Lines 7-11 of this PHP code indicate that as long as a site administrator is logged into the site and loads any page, or every time a CRON job runs, fire the EDD_SL_Plugin_Updater() class, which runs calls to the server running the API. Some CRON jobs run every minute, plus there are all the other cron jobs. Some admin users can load A LOT OF PAGES every minute, too.

YOIKS!

I added some checks to their suggested code so that even though the function runs with every page load (on the ‘init’ hook), it only ends up running if:

  • CRON is not running yet an administrator is viewing a plugin update screen
  • CRON is running and WP auto updates are turned on for the plugin

If auto updates are turned on this could still mean a lot of requests, but this certainly cuts way down on pointless API calls. Transients could also certainly be set; some plugins rate limit that way.

Slightly Optimized Version:

/**
 * Initialize the updater. Hooked into `init` to work with the
 * wp_version_check cron job, which allows auto-updates.
 */
function edd_sl_sample_plugin_updater() {

    if ( wp_installing() ) {
        return;
    }

    $doing_cron = defined( 'DOING_CRON' ) && DOING_CRON;

    if ( $doing_cron ) {
        /** 
         * If running cron, only continue if plugin auto updates are on
         * (The 'wp_version_check' CRON job supports auto-updates)
         */ 
        if ( 'yes' !== get_option( 'my_plugin_auto_updates', 'yes' ) ) { // if your plugin doesn't have a setting for this you can probably fetch the setting using hooks
            return;
        } else {
            // Probably wise to get a transient right here to slow this down...
        }
    } else {
        // If not doing cron, only check for updates for admin users
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        global $pagenow;
        // Cut back on requests by only continuing if on plugin updates page
        if ( isset( $pagenow ) && 'plugins.php' !== $pagenow && 'update-core.php' !== $pagenow ) {
            return;
        }
    }
    // retrieve our license key from the DB
    $license_key = trim( get_option( 'edd_sample_license_key' ) );

    // setup the updater
    $edd_updater = new EDD_SL_Plugin_Updater(
        EDD_SAMPLE_STORE_URL,
        __FILE__,
        array(
            'version' => '1.0',                    // current version number
            'license' => $license_key,             // license key (used get_option above to retrieve from DB)
            'item_id' => EDD_SAMPLE_ITEM_ID,       // ID of the product
            'author'  => 'Easy Digital Downloads', // author of this plugin
            'beta'    => false,
        )
    );

}
add_action( 'init', 'edd_sl_sample_plugin_updater' );

Get in touch if you need help with this or you have better ideas!