<!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>[7122] sites/trunk/wordpressfoundation.org/public_html/content/plugins/wpf-stripe/wpf-stripe.php: WPF Stripe: Add WIP webhook handler to send emails for subscriptions.</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 { white-space: pre-line; 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/7122">7122</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/7122","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>2018-04-17 22:20:28 +0000 (Tue, 17 Apr 2018)</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'>WPF Stripe: Add WIP webhook handler to send emails for subscriptions.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressfoundationorgpublic_htmlcontentpluginswpfstripewpfstripephp">sites/trunk/wordpressfoundation.org/public_html/content/plugins/wpf-stripe/wpf-stripe.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordpressfoundationorgpublic_htmlcontentpluginswpfstripewpfstripephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpressfoundation.org/public_html/content/plugins/wpf-stripe/wpf-stripe.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpressfoundation.org/public_html/content/plugins/wpf-stripe/wpf-stripe.php 2018-04-17 22:20:25 UTC (rev 7121)
+++ sites/trunk/wordpressfoundation.org/public_html/content/plugins/wpf-stripe/wpf-stripe.php   2018-04-17 22:20:28 UTC (rev 7122)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -9,9 +9,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> namespace WordPress_Foundation\Stripe;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-use Exception;
-use Stripe\Stripe, Stripe\Customer, Stripe\Charge;
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+use stdClass;
+use Exception, UnexpectedValueException;
+use Stripe\Stripe, Stripe\Customer, Stripe\Charge, Stripe\Webhook, Stripe\Event, Stripe\Error as Stripe_Error;
+use WP_REST_Request, WP_Error;
+
</ins><span class="cx" style="display: block; padding: 0 10px"> defined( 'WPINC' ) || die();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> require_once( __DIR__ . '/email.php' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -223,6 +226,206 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'init', __NAMESPACE__ . '\process_payments' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Register REST API routes.
+ */
+function register_routes() {
+       $route_args = array(
+               'methods'  => array( 'POST' ),
+               'callback' => __NAMESPACE__ . '\handle_webhooks',
+       );
+
+       register_rest_route( 'wpf-stripe/v1', 'webhooks', $route_args );
+}
+// todo: not ready for production yet -- add_action( 'rest_api_init', __NAMESPACE__ . '\register_routes' );
+
+/**
+ * Handle webhooks sent from Stripe.
+ *
+ * @param WP_REST_Request $request
+ *
+ * @return array|WP_Error
+ */
+function handle_webhooks( $request ) {
+       require_once( __DIR__ . '/stripe-php/init.php' );
+       Stripe::setApiKey( STRIPE_SECRET_KEY );
+
+       // todo Stripe's "Billing" feature just launched, and it might replace the need for some of this?
+               // https://stripe.com/billing
+
+       $event = get_verified_event( $request );
+
+       if ( is_wp_error( $event ) ) {
+               return $event;
+       }
+
+       $transaction_data = get_transaction_data_from_event( $event );
+
+       /* todo After deploying all this, we need to create webhook in Stripe that sends all the specific events that we catch here */
+
+       switch ( $event->type ) {
+               case 'charge.succeeded':
+                       $template = empty( $event->data->object->invoice ) ? 'thanks-one-time-donation' : 'thanks-recurring-donation';
+                       print_r( compact( 'event', 'transaction_data', 'template' ) );
+
+                       //send_email( $template, $transaction_data );
+                       break;
+
+               /*
+                * We don't have any business logic to do here, but Stripe requires us to respond with a 200 status,
+                * or they'll delay the payment for 72 hours.
+                *
+                * See https://stripe.com/docs/subscriptions/webhooks#understand.
+                */
+               case 'invoice.created':
+                       break;
+
+               /*
+                   todo
+
+                   also want to send deleted email when a recurring donation fails?
+                       or does stripe do that automatically?
+                       it can do it automatically, but we don't have any control over template, and it just points them to our site to update details, which they can't do there
+                       so we need to send one saying, "charge failed and subscription cancelled. if you want to continue donating, please setup a new subscription"
+                       which hook? charge failling, or subscription cancelled? how to narrow it down to just situation where 1) it was a subscription; 2) the charge failed and the subscription was canceled
+                               need to distinguish from situation where one-time donation charged failed; and situation where subscription cancelled b/c they wanted to stop it and so we manually cancelled it
+                       https://stripe.com/docs/recipes/sending-emails-for-failed-payments
+
+                       charge.failed                             - don't use this, already showed error during donate flow for one time payments. if recurring payment failed, though, then ?
+                       invoice.payment_failed        - will re-attempt in 7 days. if that fails, then will cancel. can setup new donation
+                       customer.subscription.deleted - would also fire when we manually cancel. could just say, "if you didn't request this, then it's likely because your credit card expired or could not be charged. if you'd like to continue donating, please visit {url} to setup a new subscription
+               */
+
+               /**
+                * A subscription was deleted.
+                *
+                * The probably because the payment method failed multiple times, since Stripe doesn't provide a way for
+                * donors to cancel the subscription themselves.
+                *
+                * Stripe can send email for these automatically, but it won't let us control the content of the message,
+                * and the message it sends tells the donor to visit our site to update their payment details, which assumes
+                * that we have some custom code written to let them do that via API requests, which of course, we don't.
+                */
+               case 'customer.subscription.deleted':
+                       print_r( compact( 'event', 'transaction_data' ) );
+                       // send_mail( 'subscription-deleted', $transaction_data );
+                       break;
+
+               // An annual subscription payment is coming up soon.
+               case 'invoice.upcoming':
+                       print_r( compact( 'event', 'transaction_data' ) );
+                       //send_email( 'upcoming-renewal', $transaction_data );
+                       break;
+
+               default:
+                       log( 'unknown event', compact( 'event' ) );
+                       return new WP_Error( 'unknown_event', 'Unknown event.', array( 'status' => 400 ) );
+                       break;
+       }
+
+
+       //      var_dump( compact( 'event', 'customer' ) );
+       //      die();
+
+       return array(
+               'success' => true,
+               'message' => 'Event processed successfully, thank you kindly Stripe bot.'
+       );
+}
+
+/**
+ * Convert a REST API request to an authenticated Stripe Webhook Event.
+ *
+ * This authenticates the request based on its signature, so that it can be trusted as originating from Stripe.
+ *
+ * @param WP_REST_Request $request
+ *
+ * @return Event|WP_Error
+ */
+function get_verified_event( $request ) {
+       $signature = $request->get_header( 'Stripe-Signature' );
+       $payload   = $request->get_body();
+
+       try {
+               $event = Webhook::constructEvent( $payload, $signature, STRIPE_WEBHOOK_SECRET );
+       } catch ( UnexpectedValueException $exception ) {
+               $event = new WP_Error( 'invalid_payload', $exception->getMessage(), array( 'status' => 400 ) );
+       } catch ( Stripe_Error\SignatureVerification $exception ) {
+               $event = new WP_Error( 'invalid_signature', $exception->getMessage(), array( 'status' => 400 ) );
+       } catch ( Exception $exception ) {
+               $event = new WP_Error( $exception->getCode(), $exception->getMessage(), array( 'status' => 400 ) );
+       }
+
+       if ( is_wp_error( $event ) ) {
+               log( $event->get_error_message(), compact( 'payload', 'signature' ) );
+       }
+
+       return $event;
+}
+
+/**
+ * Extract the relevant
+ *
+ * @param Event $event
+ *
+ * @return array
+ */
+function get_transaction_data_from_event( $event ) {
+       switch ( $event->type ) {
+               case 'charge.succeeded':
+                       $source = $event->data->object->source;
+                       break;
+
+               case 'customer.subscription.deleted':
+               case 'invoice.upcoming':
+                       try {
+                               // is this the right property in $event for both of these cases?
+                               $customer = Customer::retrieve( $event->data->object->customer );
+                               $source   = $customer->sources->data[0];
+                               // [0] entry isn't necessarily the right one? need the one that's tied to the subscription. maybe 1st is correct in our case b/c Customer is not shared globally w/ other sites?
+                       } catch ( Exception $exception ) {
+                               log( $exception->getMessage(), compact( 'event', 'customer' ) );
+                               $source = new stdClass();
+                       }
+                       break;
+       }
+
+       /*
+        * todo - Having trouble testing this w/ their test webhook events. Their support said:
+        *              When you use the "Send test webhook" button for your webhook the test event we send you has fake IDs everywhere with zeroes such as evt_000000 so you can't retrieve it through the API.
+        *              The best solution to test this here is to simply create a customer in Test mode in your account and then update its card in the Dashboard for example to get a real event with real data in it.
+        */
+
+       // The keys here must match `get_merge_tags()`.
+       $transaction = array(
+               'transaction_date'   => isset( $event->data->object->created         ) ? date( 'Y-m-d', $event->data->object->created ) : '',
+               'transaction_amount' => isset( $event->data->object->amount          ) ? $event->data->object->amount                   : '',
+                               // todo event->data->object->amount_due ?
+               'email'              => isset( $event->data->object->receipt_email ) ? $event->data->object->receipt_email : '',
+               'full_name'          => isset( $source->name            )            ? $source->name                       : '',
+               'address1'           => isset( $source->address_line1   )            ? $source->address_line1    : '',
+               'address2'           => isset( $source->address_line2   )            ? $source->address_line2    : '',
+               'city'               => isset( $source->address_city    )            ? $source->address_city     : '',
+               'state'              => isset( $source->address_state   )            ? $source->address_state    : '',
+               'zip_code'           => isset( $source->address_zip     )            ? $source->address_zip      : '',
+               'country'            => isset( $source->address_country )            ? $source->address_country  : '',
+               'payment_card_type'  => isset( $source->brand           )            ? $source->brand            : '',
+               'payment_card_last4' => isset( $source->last4           )            ? $source->last4            : '',
+       );
+
+       $transaction['full_address'] = trim( sprintf(
+               "%s%s%s%s%s%s",
+               $transaction['address1'] ? $transaction['address1'] . "\n" : '',
+               $transaction['address2'] ? $transaction['address2'] . "\n" : '',
+               $transaction['city']     ? $transaction['city']     . ', ' : '',
+               $transaction['state']    ? $transaction['state']    . ' '  : '',
+               $transaction['zip_code'] ? $transaction['zip_code'] . "\n" : '',
+               $transaction['country']  ? $transaction['country']         : ''
+       ) );
+
+       return $transaction;
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Log an error message.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @param string $error_message
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -236,4 +439,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $error_message,
</span><span class="cx" style="display: block; padding: 0 10px">                wp_json_encode( $data )
</span><span class="cx" style="display: block; padding: 0 10px">        ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       // todo need to redact anything, like PII?
+               // any key named 'source' probably contains things we shouldn't log, see get_transaction_data_from_event()
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>