<!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>