Skip to content

Instantly share code, notes, and snippets.

@johnotu
Created December 7, 2018 18:51
Show Gist options
  • Save johnotu/80af25cadf1ba3dadaaef7ccdc8e3c91 to your computer and use it in GitHub Desktop.
Save johnotu/80af25cadf1ba3dadaaef7ccdc8e3c91 to your computer and use it in GitHub Desktop.
Code Snippets - Custom Payments for Messenger
/**
* Handle messages received by bot server
*
*/
'use strict';
// Assume you have these functions defined and exported in your project folder
const sendTextMessage = require('../actions/sendTextMsg');
const sendButtonMsg = require('../actions/sendButtonMsg');
const sendReceiptMessage = require('../actions/sendReceiptMsg');
const botApi = require('../utils/botApi');
const receivedMessage = (event, storeName, user, pageObj) => {
const senderID = event.sender.id;
const recipientID = event.recipient.id;
const timeOfMessage = event.timestamp;
const message = event.message;
console.log('Received message for user %d and page %d at %d with message:', senderID, recipientID, timeOfMessage);
const messageId = message.mid;
const quickReply = message.quick_reply;
if (quickReply) {
// Quick reply received
const payload = quickReply.payload;
console.log('Quick reply for message %s with payload %s', messageId, payload);
if (payload.split('***')[0] === 'orderQty') {
// Prepare order data for sending to db
const orderInfo = payload.split('***');
const qty = orderInfo[1]; const productId = orderInfo[2]; const productName = orderInfo[3]; const productPrice = orderInfo[4]; const productImage = orderInfo[5]; const productDesc = orderInfo[6];
const orderedProducts = [{
product_id: productId,
product_qty: qty,
product_name: productName,
product_price: productPrice,
product_image: productImage,
product_description: productDesc
}];
const orderOwner = {
'name': user.name,
'address': user.address,
'phone_number': user.phone_number,
'receipt_address': {
'street_1': user.address,
'city': user.city,
'postal_code': user.postal_code,
'state': user.state,
'country': user.country_code
}
};
const orderTotal = orderedProducts.reduce((total, product) => {
return total + (parseInt(product.product_price) * parseInt(product.product_qty));
}, 0);
const orderNumber = botApi.getOrderNumber();
const orderData = {
'order_number': orderNumber,
'order_items': orderedProducts,
'order_total': orderTotal,
'order_owner': orderOwner,
'order_status': 'Pending'
};
botApi.postOrder(storeName, orderData, (err, resp, body) => {
if (!err && resp.statusCode === 200) {
const submittedOrder = JSON.parse(body).order;
const msg = `Thank you ${user.first_name}, your order #${submittedOrder.orderNumber} has been received. Please find your invoice attached below.`;
sendTextMessage({
recipientId: senderID,
messageText: msg,
page_token: pageObj.page_token,
callback: () => {
// Send user a receipt
sendReceiptMessage(senderID, submittedOrder, pageObj.page_token, () => {
// Send payment options to user
const buttonMsg = {
attachment: {
type: 'template',
payload: {
template_type: 'button',
text: 'How would you like to pay for your order?',
buttons: [{
type: 'postback',
payload: `pay_mobilemoney_${submittedOrder.order_number}_${submittedOrder.order_total}_${submittedOrder.order_owner.phone_number}`,
title: 'Mobile Money (Ghana)'
},
{
type: 'web_url',
url: `${process.env.SERVER_URL}payment?store_name=${storeName}&user_id=${senderID}&mm_number=${submittedOrder.order_owner.phone_number}&page_name=${pageObj.page_name}&page_id=${pageObj.page_id}&order_number=${submittedOrder.order_number}&order_total=${submittedOrder.order_total}&order_id=${submittedOrder.order._id}`,
title: 'Card',
webview_height_ratio: 'compact',
messenger_extensions: true
},
{
type: 'postback',
payload: `pay_cashondelivery_${submittedOrder.order_number}_${submittedOrder.order_total}_${submittedOrder.order_owner.phone_number}`,
title: 'Cash on delivery'
}]
}
}
};
sendButtonMsg(senderID, buttonMsg, pageObj.page_token);
});
}
});
} else {
// You should handle errors here
}
});
}
}
};
module.exports = receivedMessage;
/**
* Handle postbacks received by bot server
*
*/
'use strict';
// Assume you have these functions defined and exported in your project folder
const sendTextMessage = require('../actions/sendTextMsg');
const processMobilemoney = require('./process-mobilemoney-gh');
const receivedPostback = (event, pageObj) => {
const senderID = event.sender.id;
const payload = event.postback.payload;
if (payload.split('_')[0] === 'pay') {
// Get order details for processing
const payData = payload.split('_');
const payMethod = payData[1];
const orderObj = {
orderNumber: payData[2],
orderTotal: payData[3],
orderOwnerPhone: payData[4]
};
switch (payMethod) {
case 'cashondelivery':
sendTextMessage({
recipientId: senderID,
messageText: `You have choosen to pay Cash on Delivery of order #${orderObj.orderNumber}. Expect your delivery soon!`,
page_token: pageObj.page_token
});
break;
case 'mobilemoney':
// Get number prefix, network and route for mobile money processing
const numberPrefix = orderObj.orderOwnerPhone.slice(0, 3);
const network = '';
const route = '';
processMobilemoney(senderID, orderObj, numberPrefix, network, route, pageObj);
break;
// You can add more cases for other payment method implementations here
}
}
};
module.exports = receivedPostback;
<html lang="en">
<head>
<title>Card payment</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous"> >
<meta name="viewport" , content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<style>
#item-list {
margin-top: 3.5em;
}
</style>
</head>
<body>
<nav class="navbar fixed-top navbar-light" style="background-color: #ecf0f1">
<div>
<h5 class="font-weight-bold">
<%= page_name %>
</h5>
</div>
</nav>
<div id="item-list">
<div class='container'>
<h5>Payment for Order #
<%= order_number %>
</h5>
<div id="paystackEmbedContainer"></div>
</div>
</div>
<script type="text/javascript" src="https://ravesandboxapi.flutterwave.com/flwv3-pug/getpaidx/api/flwpbf-inline.js"></script>
<script>
// Facebook JavaScript SDK
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) { return; }
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.com/en_US/messenger.Extensions.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'Messenger'));
window.extAsyncInit = function () {
}
// Get URL parameters
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
let store_name = getParameterByName('store_name'), userPSID = getParameterByName('user_id'), page_id = getParameterByName('page_id');
const API_publicKey = "FLWPUBK-144f6ce78a70af769b682b23c55c5396-X";
// Made this an IIFE so it runs as soon as page loads
(function payWithRave() {
var x = getpaidSetup({
PBFPubKey: API_publicKey,
customer_email: "<%= user_email %>",
amount: '<%= order_total %>',
custom_logo: 'https://i.postimg.cc/nc49nnqc/jstitbit-logo.png',
customer_phone: "233264537375",
currency: "GHS",
country: 'GH',
txref: "rave-test-jstitbit-123456",
meta: [{
metaname: "orderNumber",
metavalue: "<%=order_number%>"
}],
onclose: function () { },
callback: function (response) {
var txRef = response.tx.txRef;
let confirmReq = new Request(`/payment/card/status`, {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
'bot-token': '234dhfjrtuyildkjeuuthnselki'
}),
body: JSON.stringify({
"response": response,
"order_id": '<%= order_id %>',
"order_number": '<%= order_number %>',
"user_psid": userPSID,
"page_id": page_id
})
});
fetch(confirmReq)
.then(
response => {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ', response.status);
return;
} else {
response.json().then(data => {
// Close payment modal
x.close();
// Close Messenger webview and return to conversation thread
MessengerExtensions.requestCloseBrowser(function success() {
//console.log(success)
}, function error(err) {
console.log(err)
});
})
}
}
)
.catch(err => {
console.log('Fetch Error :-S', err);
});
}
});
}());
</script>
</body>
</html>
/**
* Handle card payment using Flutterwave API
*
*/
'use strict';
const express = require('express');
const router = express.Router();
const request = require('request');
const botApi = require('../utils/botApi');
const sendTextMessage = require('../actions/sendTextMsg');
/**
* GET route to render payment page in webview
*/
router.get('/', (req, res) => {
// Render payment webview with order, user and page data
return res.render('payment-webview', {
order_number: req.query.order_number,
order_total: req.query.order_total,
user_email: `${req.query.mm_number}@aidahbot.com`,
user_psid: req.query.user_id,
page_id: req.query.page_id,
page_name: req.query.page_name,
order_id: req.query.order_id
});
});
/**
* POST route to verify status of card payment
*/
router.post('/card/status', (req, res) => {
// Send a 200 status to webview to trigger closing
res.send({ status: 200, msg: 'Payment to be confirmed' });
// Verify payment
const response = req.body.response; const orderId = req.body.order_id;
if (
response.tx.chargeResponseCode === '00' ||
response.tx.chargeResponseCode === '0'
) {
request({
url: 'https://ravesandboxapi.flutterwave.com/flwv3-pug/getpaidx/api/v2/verify',
header: { 'content-type': 'application/json' },
method: 'POST',
json: {
'SECKEY': process.env.FLUTTERWAVE_SECKEY,
'txref': response.tx.txRef
}
}, (err, resp, body) => {
if (!err && resp.statusCode === 200) {
const verifyResp = body;
botApi.getOrder(orderId, (err, resp, body) => {
if (!err && resp.statusCode === 200) {
const order = JSON.parse(body).order;
if (verifyResp.status === 'success' && verifyResp.data.chargecode === '00') {
const pageObj = botApi.getPage(req.body.page_id);
if (verifyResp.data.amount === parseInt(order.order_total) && req.body.order_number === order.order_number) {
// Payment was successful
sendTextMessage({
messageText: `Thank you ${order.order_owner.name}! We have received your payment of GH¢${order.order_total} for order #${order.order_number}`,
recipientId: req.body.user_psid,
page_token: pageObj.page_token
});
} else {
// Payment failed for some reason
sendTextMessage({
messageText: `Oops! Something went wrong while we were processing your payment for order #${order.order_number}. Please try again or contact support`,
recipientId: req.body.user_psid,
page_token: pageObj.page_token
});
}
}
}
});
}
});
} else {
// redirect to a failure page.
};
});
module.exports = router;
/**
* Process mobile money payments using Mazzuma API
*
*/
'use strict';
const request = require('request');
const sendTextMessage = require('../actions/sendTextMsg');
const processMobilemoney = (senderID, orderObj, numberPrefix, network, route, pageObj) => {
// Check if number prefix matches a Ghanaian telco
if (!(['023', '024', '054', '055', '026', '056', '027', '057', '020', '050', '028'].includes(numberPrefix))) {
sendTextMessage({
recipientId: senderID,
messageText:
"Sorry, it seems you didn't enter a valid Ghanian mobile number. Please try again.",
page_token: pageObj.page_token
});
return;
}
// Set originating network according to prefix
if (numberPrefix === '024' || numberPrefix === '054' || numberPrefix === '055') {
network = 'mtn';
} else if (numberPrefix === '026' || numberPrefix === '056') {
network = 'airtel';
} else if (numberPrefix === '027' || numberPrefix === '057') {
network = 'tigo';
}
// Handle non-supported telco
if (!network) {
return sendTextMessage({
recipientId: senderID,
messageText:
'Apologies, we only support MTN, Airtel and Tigo mobile money payments for now. Sorry for the inconvenience.',
page_token: pageObj.page_token
});
}
// Set route option according to sending and receiving network
switch (network) {
case 'mtn':
route = 'rmta';
break;
case 'tigo':
route = 'rtta';
break;
case 'airtel':
route = 'rata';
break;
}
// Handle failure in setting route option
if (!route) {
return sendTextMessage({
recipientId: senderID,
messageText:
'We think an error has occurred. Kindly contact support.',
page_token: pageObj.page_token
});
}
// Notify user to watch out for USSD prompt
sendTextMessage({
recipientId: senderID,
messageText:
'You will be prompted to accept the transaction and enter your mobile money pin via USSD. Please note that you might incur additional charge from your telco as transaction cost.',
page_token: pageObj.page_token,
callback: () => {
sendTextMessage({
recipientId: senderID,
messageText:
'Please note that you might incur additional charge from your telco as transaction cost.',
page_token: pageObj.page_token
});
}
});
// Query Mazzuma mobile money API
request({
url: 'https://client.teamcyst.com/api_call.php',
headers: { 'content-type': 'application/json' },
method: 'POST',
json: {
'price': orderObj.orderTotal,
'network': network,
'recipient_number': pageObj.mm_number,
'sender': orderObj.orderOwnerPhone,
'option': route,
'apikey': process.env.MAZZUMA_TOKEN
}
}, (error, response, body) => {
if (error) { return console.log('Error processing mobile money ', error); }
if (body && body.code === 1) {
// Payment successful. Send appropriate notification to user
sendTextMessage({
recipientId: senderID,
messageText: `Payment has been received for order #${orderObj.orderNumber}. Your order is on the way!`,
page_token: pageObj.page_token
});
} else {
console.log('payment error: ', error);
// Payment failure. notify user accordingly
return sendTextMessage({
recipientId: senderID,
messageText: `Oops! An error occured and payment has NOT been received for order #${orderObj.orderNumber}. Please try again or contact support!`,
page_token: pageObj.page_token
});
}
});
};
module.exports = processMobilemoney;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment