Add person-to-person direct payments

To enable a payer to use the bot to make a payment to a payee:

1. To bind people's email addresses to their Slack IDs for payments, listen for add email messages.
2. To receive pay messages, listen for pay messages.
3. To start payment processing, create an interactive callback.
4. To enable the payer to approve or cancel the payment, create an approval endpoint and a cancellation endpoint.

Listen for add email messages

To make person-to-person payments through the Slack bot, bind people's PayPal email addresses to their Slack IDs. Then, extract emails from the bot to process payments.

To do so, configure the bot to listen for add paypal email commands to add emails. For example, add paypal myemail@email.com. With this method, people need not know other people's PayPal emails to make payments.

/****************************************************************
* Attach a PayPal payment email to a Slack user ID for payment
****************************************************************/
controller.hears(['add paypal (.*)'], 'direct_message,direct_mention,mention', function(bot, message) {
  var email = message.match[1];
  controller.storage.users.get(message.user, function(err, user) {
    // Create user if the user does not exist
    if (!user) {
      user = {
        id: message.user,
      };
    }
    // Extract single email from mailto string and store
    // Source: http://www.regular-expressions.info/email.html
    user.email = email.match(/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/)[0];
    
    // Save user record
    controller.storage.users.save(user, function(err, id) {
      bot.reply(message, 'Your PayPal payment email will be ' + user.email + ' from now on.');
    });
  });
});

Extract the email and search the bot storage to determine whether a user already exists. If not, add a user. Then, use regex to extract the email address because the email is in an extended form on Slack. Finally, add that email to the user record and display a bot reply.

Listen for pay messages

When a payer sends a direct pay message to your bot, set up the controller to receive and process those messages:

/****************************************************************
* If a payer makes a direct payment to someone else, provide
* a confirmation before pushing to callback for processing
****************************************************************/
controller.hears(['pay (.*) (.*) (.*)'], 'direct_message', function(bot, message) {
  var msg = message.match[0];
  var uid = message.match[1].replace(/\W+/g, '');
  var amount = message.match[2];
  var currency = message.match[3];
  
  bot.reply(message, {
    attachments: [{
      title: 'Would you like to ' + msg + '?',
      callback_id: '222',
      attachment_type: 'default',
      actions: [{
        name": 'yes',
        text": 'Yes',
        value": uid + '|' + amount + '|' + currency,
        type": 'button',
      },{
        name:'no',
        text: 'No',
        value: 'no',
        type: 'button',
      }]
    }]
  });
});

The hears method listens for the pay message.

For example, the payer types pay @USER 10 USD, which causes the controller to display yes and no buttons. The payer uses these buttons to begin payment to the payee.

Extract the user ID, amount, and currency. Then, send those values along with the yes button, if clicked.

Create an interactive callback

After the payer clicks yes or no, handle that activity through the interactive message callback. For no, simply cancel the payment. For yes, create a link to redirect the payer to PayPal to begin the payment.

/****************************************************************
* After the payer agrees to process a payment or agreement, 
* handle all payment requests in a callback
****************************************************************/
controller.on('interactive_message_callback', function(bot, message) {
  // If the payer clicks no, cancel the payment. Otherwise, process payment.
  if (message.actions[0].value === 'no') {
    bot.replyInteractive(message, 'Payment process cancelled');
  } else {
    bot.replyInteractive(message, 'Generating your payment link. Hold on a moment...');
    
    // Prepare base payment message for bot to respond with
    var reply = {
      attachments: [{
        fallback: 'Payment initiation information failed to load',
        color: '#36a64f',
        pretext: 'Click the link below to initiate payment',
        title: 'Make payment to COMPANY',
        footer: 'PayPal Payment Bot',
        footer_icon: 'https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2016-08-17/70252203425_a7e48673014756aad9a5_96.jpg',
        ts: message.ts
      }]
    };
  
    var dataArr = message.actions[0].value.split('|');
        
    controller.storage.users.get(dataArr[0], function(err, user) {
      if (!user.email) {
        bot.replyInteractive(message, 'This person has no PayPal email associated with the account. Ask them to add one with the "add paypal EMAIL" command');
      }
          
      // Build PayPal payment request
      var payReq = JSON.stringify({
        intent: 'sale',
        redirect_urls: {
          return_url: redirect + '/process',
          cancel_url: redirect + '/cancel'
        },
        payer: {
          payment_method: 'paypal'
        },
        transactions: [{
          description: 'This is the payment transaction description.',
          amount: {
            total: dataArr[1],
            currency: dataArr[2]
          },
          payee: {
            email: user.email
          }
        }]
      });
        
      // Create payment request before PayPal redirect to approve
      paypal.payment.create(payReq, function(error, payment) {
        if(error) {
          console.error(JSON.stringify(error));
        } else {
          // Capture HATEOAS links
          var links = {};
          payment.links.forEach(function(linkObj) {
            links[linkObj.rel] = {
              href: linkObj.href,
              method: linkObj.method
            };
          })
    
          // If redirect URL exists, insert link into bot message and display
          if (links.hasOwnProperty('approval_url')) {
            reply.attachments[0].title_link = links['approval_url'].href;
            bot.replyInteractive(message, reply);
          } else {
            console.error('no redirect URI present');
          }
        }
      });
    });
  }
});

After the payer clicks yes, the app:

  • Creates an immediate, interactive reply to notify the payer that processing has begun.

  • Creates the interactive reply that houses the link to PayPal because you cannot automatically redirect the payer to a web address.

  • Creates the payment request object, which includes all required payment information.

  • Calls payment.create(...) with the payment request object.

  • If the payment starts successfully, extracts the approval URL to which you redirect the payer, adds it to the bot reply, and displays the reply to the payer.

Create an approval endpoint

After the payer confirms payment, redirect him or her to the /process endpoint in your app.

To handle this redirect, your app must add an HTTP GET /process endpoint:

/****************************************************************
* After the payer is redirected to PayPal, process a direct
* PayPal payment 
****************************************************************/
controller.webserver.get('/process', function(req, res) {
  // Extract payment confirmation information needed to process payment
  var paymentId = req.query.paymentId;
  var payerId = { payer_id: req.query.PayerID };

  // Try to complete the payment for the payer
  paypal.payment.execute(paymentId, payerId, function(error, payment) {
    if(error) {
      console.error(JSON.stringify(error));
    } else {
      if (payment.state == 'approved') { 
        res.send('Payment completed successfully');
      } else {
        res.send('Payment not successful');
      }
    }
  });
});

After that endpoint is triggered, extract the payment ID and payer ID that PayPal sends. After extraction, call payment.execute(...) with the variables to complete payment.

Create a cancellation endpoint

If the payer cancels the payment during the PayPal payment confirmation step, redirect the payer to the /cancel endpoint in the app.

To handle this redirect, add an HTTP GET /cancel endpoint.

/****************************************************************
* Payment incomplete: The payer canceled the PayPal payment
****************************************************************/
controller.webserver.get('/cancel', function(req, res) {
  res.send('Payment canceled');
});

This example simply displays a Payment canceled message. Your app should provide a properly handled experience.