<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[2302] sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network: WordCamp Budgets Dashboard: Add screen to monitor/approve Sponsor Invoices.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/2302">2302</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/2302","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>iandunn</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2016-01-16 00:22:43 +0000 (Sat, 16 Jan 2016)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>WordCamp Budgets Dashboard: Add screen to monitor/approve Sponsor Invoices.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkbootstrapphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/bootstrap.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/css/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkcsssponsorinvoicescss">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/css/sponsor-invoices.css</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkincludessponsorinvoicesdashboardphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/includes/sponsor-invoices-dashboard.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkincludessponsorinvoiceslisttablephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/includes/sponsor-invoices-list-table.php</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/javascript/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkjavascriptsponsorinvoicesjs">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/javascript/sponsor-invoices.js</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/views/</li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/views/sponsor-invoices/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkviewssponsorinvoicespagesponsorinvoicesphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/views/sponsor-invoices/page-sponsor-invoices.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkbootstrapphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/bootstrap.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/bootstrap.php   2016-01-16 00:21:14 UTC (rev 2301)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/bootstrap.php     2016-01-16 00:22:43 UTC (rev 2302)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -14,9 +14,17 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> defined( 'WPINC' ) or die();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-if ( is_admin() ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+if ( is_admin() || defined( 'DOING_CRON' ) ) {
+       /*
+        * The bootloader for WordCamp Budgets only loads files during is_admin(), because that's all that plugin
+        * needs, but this plugin also needs some of them to be active during cron jobs.
+        */
+       require_once( WP_PLUGIN_DIR . '/wordcamp-payments/includes/wordcamp-budgets.php' );
+       require_once( WP_PLUGIN_DIR . '/wordcamp-payments/includes/sponsor-invoice.php'  );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         require_once( __DIR__ . '/includes/wordcamp-budgets-dashboard.php' );
</span><span class="cx" style="display: block; padding: 0 10px">        require_once( __DIR__ . '/includes/payment-requests-dashboard.php' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        require_once( __DIR__ . '/includes/sponsor-invoices-dashboard.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $GLOBALS['Payment_Requests_Dashboard'] = new \Payment_Requests_Dashboard();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkcsssponsorinvoicescss"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/css/sponsor-invoices.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/css/sponsor-invoices.css                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/css/sponsor-invoices.css  2016-01-16 00:22:43 UTC (rev 2302)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,11 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.wcbdsi-approve-invoice.hidden {
+       display: none;
+}
+
+.wcbdsi-inline-notice {
+       margin-bottom: 5px;
+       box-shadow:    none;
+       border-top:    1px solid rgba( 0, 0, 0, 0.1 );
+       border-right:  1px solid rgba( 0, 0, 0, 0.1 );
+       border-bottom: 1px solid rgba( 0, 0, 0, 0.1 );
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkincludessponsorinvoicesdashboardphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/includes/sponsor-invoices-dashboard.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/includes/sponsor-invoices-dashboard.php                         (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/includes/sponsor-invoices-dashboard.php   2016-01-16 00:22:43 UTC (rev 2302)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,450 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\Budgets_Dashboard\Sponsor_Invoices;
+
+defined( 'WPINC' ) or die();
+
+const LATEST_DATABASE_VERSION = 1;
+
+if ( defined( 'DOING_AJAX' ) ) {
+       add_action( 'wp_ajax_wcbdsi_approve_invoice', __NAMESPACE__ . '\handle_approve_invoice_request'       );
+       add_action( 'save_post',                      __NAMESPACE__ . '\update_index_row',              10, 2 );
+
+} elseif ( defined( 'DOING_CRON' ) ) {
+       add_action( 'wcbdsi_check_for_paid_invoices', __NAMESPACE__ . '\check_for_paid_invoices'       );
+       add_action( 'save_post',                      __NAMESPACE__ . '\update_index_row',       10, 2 );
+
+} elseif ( is_network_admin() ) {
+       add_action( 'plugins_loaded',        __NAMESPACE__ . '\schedule_cron_events'  );
+       add_action( 'network_admin_menu',    __NAMESPACE__ . '\register_submenu_page' );
+       add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_scripts'       );
+       add_action( 'init',                  __NAMESPACE__ . '\upgrade_database'      );
+
+} elseif ( is_admin() ) {
+       add_action( 'save_post',    __NAMESPACE__ . '\update_index_row', 11, 2 );   // See note in callback about priority
+       add_action( 'trashed_post', __NAMESPACE__ . '\delete_index_row'        );
+       add_action( 'delete_post',  __NAMESPACE__ . '\delete_index_row'        );
+}
+
+/**
+ * Schedule cron job when plugin is activated
+ */
+function schedule_cron_events() {
+       if ( wp_next_scheduled( 'wcbdsi_check_for_paid_invoices' ) ) {
+               return;
+       }
+
+       wp_schedule_event( current_time( 'timestamp' ), 'hourly', 'wcbdsi_check_for_paid_invoices' );
+}
+
+/**
+ * Register the admin page
+ */
+function register_submenu_page() {
+       $hook_suffix = add_dashboard_page(
+               'WordCamp Sponsor Invoices',
+               'Sponsor Invoices',
+               'manage_network',
+               'sponsor-invoices-dashboard',
+               __NAMESPACE__ . '\render_submenu_page'
+       );
+
+       add_action( "admin_print_scripts-$hook_suffix", __NAMESPACE__ . '\enqueue_scripts' );
+}
+
+/**
+ * Render the admin page
+ */
+function render_submenu_page() {
+       require_once( __DIR__ . '/sponsor-invoices-list-table.php' );
+
+       $list_table = new Sponsor_Invoices_List_Table();
+       $sections   = get_submenu_page_sections();
+
+       $list_table->prepare_items();
+
+       switch ( $_GET['section'] ) {
+               case 'submitted':
+                       $section_explanation = 'These invoices have been completed by the organizer, but need to be approved by a deputy before being sent to the sponsor.';
+                       break;
+
+               case 'approved':
+                       $section_explanation = "These invoices have been approved by a deputy and sent to the sponsor, but haven't been paid yet.";
+                       break;
+
+               case 'paid':
+                       $section_explanation = 'These invoices have been paid by the sponsor.';
+                       break;
+       }
+
+       require_once( dirname( __DIR__ ) . '/views/sponsor-invoices/page-sponsor-invoices.php' );
+}
+
+/**
+ * Get all the data needed to render the section tabs
+ *
+ * @return array
+ */
+function get_submenu_page_sections() {
+       $statuses        = \WordCamp\Budgets\Sponsor_Invoices\get_custom_statuses();
+       $sections        = array();
+       $current_section = sanitize_text_field( $_GET['section'] );
+
+       foreach ( $statuses as $status_slug => $status_name ) {
+               $status_slug = str_replace( 'wcbsi_', '', $status_slug );    // make the URL easier to read
+
+               $classes = 'nav-tab';
+               if ( $status_slug === $current_section ) {
+                       $classes .= ' nav-tab-active';
+               }
+
+               $href = add_query_arg(
+                       array(
+                               'page'    => 'sponsor-invoices-dashboard',
+                               'section' => $status_slug,
+                       ),
+                       network_admin_url( 'index.php' )
+               );
+
+               $sections[ $status_slug ] = array(
+                       'classes' => $classes,
+                       'href'    => $href,
+                       'text'    => $status_name,
+               );
+       }
+
+       return $sections;
+}
+
+/**
+ * Enqueue JavaScript and CSS files
+ */
+function enqueue_scripts() {
+       wp_enqueue_script(
+               'wcbd-sponsor-invoices',
+               plugins_url( 'javascript/sponsor-invoices.js', __DIR__ ),
+               array( 'jquery', 'underscore' ),
+               1,
+               true
+       );
+
+       wp_enqueue_style(
+               'wcbd-sponsor-invoices',
+               plugins_url( 'css/sponsor-invoices.css', __DIR__ ),
+               array(),
+               1
+       );
+}
+
+/**
+ * Create or update the database tables
+ */
+function upgrade_database() {
+       global $wpdb;
+
+       $current_database_version = get_site_option( 'wcbdsi_database_version', 0 );
+
+       if ( version_compare( $current_database_version, LATEST_DATABASE_VERSION, '>=' ) ) {
+               return;
+       }
+
+       $table_name = get_index_table_name();
+       require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
+
+       $schema = "
+               CREATE TABLE $table_name (
+                       blog_id       int( 11 )        unsigned NOT NULL default '0',
+                       invoice_id    int( 11 )        unsigned NOT NULL default '0',
+                       invoice_title varchar( 75 )             NOT NULL default '',
+                       status        varchar( 30 )             NOT NULL default '',
+                       wordcamp_name varchar( 75 )             NOT NULL default '',
+                       sponsor_name  varchar( 30 )             NOT NULL default '',
+                       description   varchar( 75 )             NOT NULL default '',
+                       currency      varchar( 3  )             NOT NULL default '',
+                       due_date      int( 11 )        unsigned NOT NULL default '0',
+                       amount        numeric( 10, 2 ) unsigned NOT NULL default '0',
+
+                       PRIMARY KEY (blog_id, invoice_id),
+                       KEY status (status)
+               )
+               DEFAULT CHARACTER SET {$wpdb->charset}
+               COLLATE {$wpdb->collate};
+       ";
+
+       dbDelta( $schema );
+
+       update_site_option( 'wcbdsi_database_version', LATEST_DATABASE_VERSION );
+}
+
+/**
+ * Returns the name of the custom table.
+ */
+function get_index_table_name() {
+       global $wpdb;
+
+       return $wpdb->get_blog_prefix( 0 ) . 'wcbd_sponsor_invoice_index';
+}
+
+/**
+ * Handle an AJAX request to approve an invoice
+ */
+function handle_approve_invoice_request() {
+       $required_parameters = array( 'action', 'nonce', 'site_id', 'invoice_id' );
+
+       foreach ( $required_parameters as $parameter ) {
+               if ( empty( $_REQUEST[ $parameter ] ) ) {
+                       wp_send_json_error( array( 'error' => 'Required parameters not set.' ) );
+               }
+       }
+
+       $site_id    = absint( $_REQUEST['site_id'] );
+       $invoice_id = absint( $_REQUEST['invoice_id'] );
+
+       if ( ! wp_verify_nonce( $_REQUEST['nonce'], "wcbdsi-approve-invoice-$site_id-$invoice_id" ) || ! current_user_can( 'manage_network' ) ) {
+               wp_send_json_error( array( 'error' => 'Permission denied.' ) );
+       }
+
+       $quickbooks_result = send_invoice_to_quickbooks( $site_id, $invoice_id );
+
+       if ( 'sent' === $quickbooks_result ) {
+               update_invoice_status(           $site_id, $invoice_id, 'approved' );
+               notify_organizer_status_changed( $site_id, $invoice_id, 'approved' );
+               wp_send_json_success( array( 'message' => 'The invoice has been approved.' ) );
+       } else {
+               wp_send_json_error( array( 'error' => $quickbooks_result ) );
+       }
+}
+
+/**
+ * Send an invoice to the sponsor through QuickBooks Online's API
+ *
+ * @param int $site_id
+ * @param int $invoice_id
+ *
+ * @return string
+ */
+function send_invoice_to_quickbooks( $site_id, $invoice_id ) {
+       switch_to_blog( $site_id );
+
+       $invoice_meta = get_post_custom( $invoice_id );
+       $sponsor_meta = get_post_custom( $invoice_meta['_wcbsi_sponsor_id'][0] );
+
+       /* these are the values needed for the API call. they're guaranteed to exist.
+       wp_send_json_error( array(
+               $sponsor_meta['_wcpt_sponsor_company_name'][0],
+               $sponsor_meta['_wcpt_sponsor_first_name'][0],
+               $sponsor_meta['_wcpt_sponsor_last_name'][0],
+               $sponsor_meta['_wcpt_sponsor_email_address'][0],
+               $sponsor_meta['_wcpt_sponsor_phone_number'][0],
+               $sponsor_meta['_wcpt_sponsor_tax_resale_number'][0],
+
+               $sponsor_meta['_wcpt_sponsor_street_address1'][0],
+               $sponsor_meta['_wcpt_sponsor_street_address2'][0],
+               $sponsor_meta['_wcpt_sponsor_city'][0],
+               $sponsor_meta['_wcpt_sponsor_state'][0],
+               $sponsor_meta['_wcpt_sponsor_zip_code'][0],
+               $sponsor_meta['_wcpt_sponsor_country'][0],
+
+               $invoice_meta['_wcbsi_due_date'][0],
+               $invoice_meta['_wcbsi_description'][0],
+               $invoice_meta['_wcbsi_currency'][0],
+               $invoice_meta['_wcbsi_amount'][0],
+               $invoice_meta['_wcbsi_invoice_message'][0],
+       ) );
+       */
+
+       $sent = 'QuickBooks integration is not complete yet.';
+       // todo return 'sent' (string) on success, or an error message string on failure
+
+       restore_current_blog();
+
+       return $sent;
+}
+
+/**
+ * Send a request to QuickBooks to check if any sent invoices have been paid
+ *
+ * If any have been, update the status of the local copy, and notify the organizer who sent the invoice.
+ */
+function check_for_paid_invoices() {
+       global $wpdb;
+
+       $table_name = get_index_table_name();
+
+       /*
+        * This query is limited at 100 rows to avoid requesting too much data from QBO. In most cases it shouldn't be
+        * a problem, but it's possible that at some point the number of pending invoices will exceed the limit, and
+        * we'll need to refactor this to update them in batches.
+        */
+       $sent_invoices = $wpdb->get_results( "
+               SELECT *
+               FROM $table_name
+               WHERE status = 'wcbsi_approved'
+               LIMIT 100
+       "); // todo if QBO's API imposes a limit, then update this to match
+
+       // todo fake data for testing. replace w/ API call to QBO
+       $updated_invoices = array(
+               //array( 'blog_id' => 11, 'invoice_id' => 45499, 'status' => 'submitted' ),
+               //array( 'blog_id' => 11, 'invoice_id' => 45506, 'status' => 'paid' ),
+       );
+
+       foreach ( $updated_invoices as $invoice ) {
+               if ( 'paid' === $invoice['status'] ) {
+                       update_invoice_status(           $invoice['blog_id'], $invoice['invoice_id'], 'paid' );
+                       notify_organizer_status_changed( $invoice['blog_id'], $invoice['invoice_id'], 'paid' );
+               }
+       }
+}
+
+/**
+ * Update the status of an invoice
+ *
+ * @param int    $site_id
+ * @param int    $invoice_id
+ * $param string $new_status
+ */
+function update_invoice_status( $site_id, $invoice_id, $new_status ) {
+       switch_to_blog( $site_id );
+
+       // Disable the functions that run during a normal save, because they'd interfere with this
+       remove_filter( 'wp_insert_post_data', 'WordCamp\Budgets\Sponsor_Invoices\set_invoice_status', 10 );
+       remove_action( 'save_post',           'WordCamp\Budgets\Sponsor_Invoices\save_invoice',       10 );
+
+       wp_update_post(
+               array(
+                       'ID'          => $invoice_id,
+                       'post_status' => "wcbsi_$new_status",
+               ),
+               true
+       );
+
+       restore_current_blog();
+}
+
+/**
+ * Notify the organizer when the status of their invoice changes
+ *
+ * @param int    $site_id
+ * @param int    $invoice_id
+ * @param string $new_status
+ */
+function notify_organizer_status_changed( $site_id, $invoice_id, $new_status ) {
+       switch_to_blog( $site_id );
+
+       $invoice      = get_post( $invoice_id );
+       $to           = \WordCamp_Budgets::get_requester_formatted_email( $invoice->post_author );
+       $subject      = "Invoice for {$invoice->post_title} $new_status";
+       $sponsor_name = get_sponsor_name( $invoice_id );
+       $invoice_url  = admin_url( sprintf( 'post.php?post=%s&action=edit', $invoice_id ) );
+       $headers      = array( 'Reply-To: support@wordcamp.org' );
+
+       if ( 'approved' === $new_status ) {
+               $status_message = "has been approved by a WordCamp deputy, and sent to $sponsor_name. You will receive another notification when they have paid the invoice";
+       } elseif ( 'paid' === $new_status ) {
+               $status_message = "has been paid by $sponsor_name and is now complete";
+       }
+
+       $message = "
+               The invoice for `{$invoice->post_title}` $status_message.
+
+               You can view the invoice and its status at:
+
+               $invoice_url
+
+               If you have any questions, please reply to let us know.
+       ";
+       $message = str_replace( "\t", '', $message );
+
+       wp_mail( $to, $subject, $message, $headers );
+
+       restore_current_blog();
+}
+
+/**
+ * Get the name of the sponsor for a given invoice
+ *
+ * NOTE: This must be called from inside a switch_to_blog() context.
+ *
+ * @param int $invoice_id
+ *
+ * @return string
+ */
+function get_sponsor_name( $invoice_id ) {
+       $sponsor_name = '';
+       $sponsor_id   = get_post_meta( $invoice_id, '_wcbsi_sponsor_id',  true );
+       $sponsor      = get_post( $sponsor_id );
+
+       if ( is_a( $sponsor, 'WP_Post' ) ) {
+               $sponsor_name = $sponsor->post_title;
+       }
+
+       return $sponsor_name;
+}
+
+/**
+ * Add or update a row in the index
+ *
+ * NOTE: This must run after \WordCamp\Budgets\Sponsor_Invoices\save_invoice(), because otherwise the
+ * get_post_meta() calls would be fetching the old data, rather than the latest from the current process.
+ *
+ * @param int      $invoice_id
+ * @param \WP_Post $invoice
+ */
+function update_index_row( $invoice_id, $invoice ) {
+       global $wpdb;
+
+       if ( \WordCamp\Budgets\Sponsor_Invoices\POST_TYPE !== $invoice->post_type ) {
+               return;
+       }
+
+       // Drafts, etc aren't displayed in the list table, so there's no reason to index them
+       $ignored_statuses = array( 'auto-draft', 'draft', 'trash' );
+
+       if ( in_array( $invoice->post_status, $ignored_statuses, true ) ) {
+               return;
+       }
+
+       // todo use post_edit_is_actionable instead?
+
+       $index_row = array(
+               'blog_id'       => get_current_blog_id(),
+               'invoice_id'    => $invoice_id,
+               'invoice_title' => $invoice->post_title,
+               'status'        => $invoice->post_status,
+               'wordcamp_name' => get_wordcamp_name(),
+               'sponsor_name'  => get_sponsor_name( $invoice_id ),
+               'description'   => get_post_meta( $invoice_id, '_wcbsi_description', true ),
+               'currency'      => get_post_meta( $invoice_id, '_wcbsi_currency',    true ),
+               'due_date'      => get_post_meta( $invoice_id, '_wcbsi_due_date',    true ),
+               'amount'        => get_post_meta( $invoice_id, '_wcbsi_amount',      true ),
+       );
+
+       $formats = array( '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%f' );
+
+       $wpdb->replace( get_index_table_name(), $index_row, $formats );
+}
+
+/**
+ * Delete a row from the index
+ *
+ * @param int $invoice_id
+ */
+function delete_index_row( $invoice_id ) {
+       global $wpdb;
+
+       /*
+        * Normally we would check if $invoice_id is from the kind of post type we want, but that's not necessary in
+        * this case, because only invoices are added to the index to begin with. If $invoice_id is from some other
+        * post type, then $wpdb->delete() will return false with no negative consequences. That's quicker than having
+        * to query for the $invoice post in order to check the post type.
+        */
+
+       $wpdb->delete(
+               get_index_table_name(),
+               array(
+                       'blog_id'    => get_current_blog_id(),
+                       'invoice_id' => $invoice_id,
+               )
+       );
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkincludessponsorinvoiceslisttablephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/includes/sponsor-invoices-list-table.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/includes/sponsor-invoices-list-table.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/includes/sponsor-invoices-list-table.php  2016-01-16 00:22:43 UTC (rev 2302)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,169 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\Budgets_Dashboard\Sponsor_Invoices;
+defined( 'WPINC' ) or die();
+
+class Sponsor_Invoices_List_Table extends \WP_List_Table {
+
+       /**
+        * Define the table columns that will be rendered
+        */
+       public function get_columns() {
+               $columns = array(
+                       'invoice_title' => 'Invoice',
+                       'wordcamp_name' => 'WordCamp',
+                       'sponsor_name'  => 'Sponsor',
+                       'description'   => 'Description',
+                       'due_date'      => 'Due Date',
+                       'amount'        => 'Amount',
+               );
+
+               if ( 'submitted' === $_GET['section'] ) {
+                       $columns['approve_invoice'] = 'Approve';
+               }
+
+               return $columns;
+       }
+
+       /**
+        * Parses query arguments and queries the index table in the database.
+        */
+       public function prepare_items() {
+               global $wpdb;
+
+               /*
+                * Manually build the column headers
+                *
+                * See https://codex.wordpress.org/Class_Reference/WP_List_Table#Using_within_Meta_Boxes
+                *
+                * The alternative to this would be instantiating this object during `load-$hook-suffix`, and setting it
+                * to a global variable so it could be accessed later by render_submenu_page(). This is hacky, but that's
+                * worse.
+                */
+               $this->_column_headers = array(
+                       $this->get_columns(),
+                       array(),
+                       array(),
+                       $this->get_primary_column_name()
+               );
+
+               $table_name = get_index_table_name();
+               $status     = 'wcbsi_' . $_GET['section'];
+               $paged      = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 1;
+               $limit      = 30;
+               $offset     = $limit * ( $paged - 1 );
+
+               $this->items = $wpdb->get_results( $wpdb->prepare( "
+                       SELECT *
+                       FROM $table_name
+                       WHERE status = %s
+                       ORDER BY due_date ASC
+                       LIMIT %d
+                       OFFSET %d",
+                       $status,
+                       $limit,
+                       $offset
+               ) );
+
+               // A second query is faster than using SQL_CALC_FOUND_ROWS during the first query
+               $total_items = $wpdb->get_var( $wpdb->prepare( "
+                       SELECT count(blog_id)
+                       FROM $table_name
+                       WHERE status = %s",
+                       $status
+               ) );
+
+               $this->set_pagination_args( array(
+                       'total_items' => $total_items,
+                       'total_pages' => ceil( $total_items / $limit ),
+                       'per_page'    => $limit,
+               ) );
+       }
+
+       /**
+        * Render the value for the Invoice column
+        *
+        * @param object $index_row
+        */
+       protected function column_invoice_title( $index_row ) {
+               $edit_url = get_admin_url(
+                       $index_row->blog_id,
+                       sprintf( 'post.php?post=%s&action=edit', $index_row->invoice_id )
+               );
+
+               ob_start();
+               ?>
+
+               <a href="<?php echo esc_url( $edit_url ); ?>">
+                       <?php echo esc_html( $index_row->invoice_title ); ?>
+               </a>
+
+               <?php
+
+               return ob_get_clean();
+       }
+
+       /**
+        * Render the value for the Description column
+        *
+        * @param object $index_row
+        */
+       protected function column_description( $index_row ) {
+               return esc_html( substr( $index_row->description, 0, 75 ) );
+       }
+
+       /**
+        * Render the value for the Due Date column
+        *
+        * @param object $index_row
+        */
+       protected function column_due_date( $index_row ) {
+               return esc_html( date( 'Y-m-d', $index_row->due_date ) );
+       }
+
+       /**
+        * Render the value for the Due Date column
+        *
+        * @param object $index_row
+        */
+       protected function column_amount( $index_row ) {
+               return wp_kses(
+                       \WordCamp\Budgets_Dashboard\format_amount( $index_row->amount, $index_row->currency ),
+                       array( 'br' => array() )
+               );
+       }
+
+       /**
+        * Render the value for the Approve column
+        *
+        * @param object $index_row
+        */
+       protected function column_approve_invoice( $index_row ) {
+               $nonce = wp_create_nonce( "wcbdsi-approve-invoice-{$index_row->blog_id}-{$index_row->invoice_id}" );
+
+               ?>
+
+               <button
+                       class="wcbdsi-approve-invoice button-secondary"
+                       data-site-id="<?php    echo esc_attr( $index_row->blog_id    ); ?>"
+                       data-invoice-id="<?php echo esc_attr( $index_row->invoice_id ); ?>"
+                       data-nonce="<?php      echo esc_attr( $nonce                 ); ?>"
+               >
+                       Approve
+               </button>
+
+               <div class="wcbdsi-inline-notice hidden"><div> <?php // Populated dynamically ?>
+
+               <?php
+       }
+
+       /**
+        * Render the value for columns that don't have a explicit handler
+        *
+        * @param object $index_row
+        * @param string $column_name
+        */
+       protected function column_default( $index_row, $column_name ) {
+               echo esc_html( $index_row->$column_name );
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkjavascriptsponsorinvoicesjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/javascript/sponsor-invoices.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/javascript/sponsor-invoices.js                          (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/javascript/sponsor-invoices.js    2016-01-16 00:22:43 UTC (rev 2302)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,91 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+( function( $ ) {
+       'use strict';
+
+       window.WordCampBudgetsDashboard = window.WordCampBudgetsDashboard || {};
+
+       var app = window.WordCampBudgetsDashboard.SponsorInvoices = {
+               /**
+                * Initialization that runs as soon as this file has loaded
+                */
+               start : function() {
+                       try {
+                               $( '.wcbdsi-approve-invoice' ).click( app.approveInvoice );
+                       } catch ( exception ) {
+                               app.log( exception );
+                       }
+               },
+
+               /**
+                * Send an AJAX request to approve the invoice
+                *
+                * @param {event} event
+                */
+               approveInvoice : function( event ) {
+                       var approvalButton = $( this ),
+                           statusMessage  = $( this ).parent().find( '.wcbdsi-inline-notice' ),
+                           siteID         = approvalButton.data( 'site-id'    ),
+                               invoiceID      = approvalButton.data( 'invoice-id' ),
+                               nonce          = approvalButton.data( 'nonce'      );
+
+                       event.preventDefault();
+
+                       try {
+                               approvalButton.addClass( 'hidden' );
+                               statusMessage.html( 'Submitting to QuickBooks...' );   // todo show spinner instead
+                               statusMessage.removeClass( 'hidden' );
+
+                               $.post(
+                                       ajaxurl,
+                                       {
+                                               action     : 'wcbdsi_approve_invoice',
+                                               nonce      : nonce,
+                                               site_id    : siteID,
+                                               invoice_id : invoiceID
+                                       },
+
+                                       function( response ) {
+                                               // todo modularize this
+
+                                               try {
+                                                       if ( response.hasOwnProperty( 'success' ) && true === response.success ) {
+                                                               statusMessage.addClass( 'notice notice-success' );
+                                                               statusMessage.html( _.escape( response.data.message ) );
+                                                       } else {
+                                                               statusMessage.addClass( 'notice notice-error' );
+                                                               statusMessage.html( _.escape( 'ERROR: ' + response.data.error ) );
+
+                                                               // todo bring button back so they can try again?
+                                                       }
+                                               } catch ( exception ) {
+                                                       app.log( exception );
+                                               }
+                                       }
+                               );
+                       } catch ( exception ) {
+                               app.log( exception );
+                       }
+               },
+
+               /**
+                * Log a message to the console
+                *
+                * todo centralize for other modules
+                *
+                * @param {*} error
+                */
+               log : function( error ) {
+                       if ( ! window.console ) {
+                               return;
+                       }
+
+                       if ( 'string' === typeof error ) {
+                               console.log( 'WordCamp Budgets Dashboard: ' + error );
+                       } else {
+                               console.log( 'WordCamp Budgets Dashboard: ', error );
+                       }
+               }
+       };
+
+} )( jQuery );
+
+window.WordCampBudgetsDashboard.SponsorInvoices.start();
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcamppaymentsnetworkviewssponsorinvoicespagesponsorinvoicesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/views/sponsor-invoices/page-sponsor-invoices.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/views/sponsor-invoices/page-sponsor-invoices.php                                (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-payments-network/views/sponsor-invoices/page-sponsor-invoices.php  2016-01-16 00:22:43 UTC (rev 2302)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,29 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\Budgets_Dashboard\Sponsor_Invoices;
+defined( 'WPINC' ) or die();
+
+?>
+
+<div class="wrap">
+       <h1>Sponsor Invoices</h1>
+
+       <?php settings_errors(); ?>
+
+       <p>
+               <?php echo esc_html( $section_explanation ); ?>
+       </p>
+
+       <h3 class="nav-tab-wrapper">
+               <?php foreach ( $sections as $section ) : ?>
+                       <a
+                               class="<?php echo esc_attr( $section['classes'] ); ?>"
+                               href="<?php  echo esc_attr( $section['href']    ); ?>"
+                       >
+                               <?php echo esc_html( $section['text'] ); ?>
+                       </a>
+               <?php endforeach; ?>
+       </h3>
+
+       <?php $list_table->display(); ?>
+</div>
</ins></span></pre>
</div>
</div>

</body>
</html>