Split stripe-webhook into multiple files

This commit is contained in:
Keith Irwin 2023-03-14 15:00:10 -06:00
parent 9ae0020279
commit c402670d4d
Signed by: ki9
GPG Key ID: DF773B3F4A88DA86
15 changed files with 145 additions and 152 deletions

View File

@ -2,7 +2,6 @@
require('dotenv').config()
const {verify} = require('hcaptcha')
const mailer = require('./lib/mailer.js')
module.exports = async (req, res) => {
@ -18,7 +17,7 @@ module.exports = async (req, res) => {
// Send email
let mail_res; try {
console.log(`Sending email from to ${process.env.BUGS_MAIL_FROM}...`)
mail_res = await mailer.sendMail({
mail_res = await require('../lib/mailer').sendMail({
from: process.env.BUGS_MAIL_FROM,
to: process.env.ADMIN_EMAIL,
subject: 'Bug found!',

View File

@ -2,7 +2,6 @@
require('dotenv').config()
const {verify} = require('hcaptcha')
const mailer = require('./lib/mailer.js')
module.exports = async (req, res) => {
// console.log(`Received token: ${req.body['token']}`)
@ -26,7 +25,7 @@ module.exports = async (req, res) => {
// Send email
let mail_res; try {
console.log(`Sending email from ${from} to ${process.env.CONTACT_MAIL_TO}...`)
mail_res = await mailer.sendMail({
mail_res = await require('../lib/mailer').sendMail({
from: process.env.CONTACT_MAIL_FROM,
replyTo: from,
to: process.env.CONTACT_MAIL_TO,

View File

@ -13,10 +13,10 @@ app.listen(process.env.API_PORT||80)
app.get('/', (req, res) => res.sendStatus(200))
// Create new order
app.post('/order', jsonBodyParser, cors, require('./order.js'))
app.post('/order', jsonBodyParser, cors, require('./order'))
// Send email through contact form
app.post('/contact', jsonBodyParser, cors, require('./contact.js'))
app.post('/contact', jsonBodyParser, cors, require('./contact'))
// Send bug report through disclosure form
app.post('/bug', jsonBodyParser, cors, require('./bug.js'))
app.post('/bug', jsonBodyParser, cors, require('./bug'))

View File

@ -8,11 +8,7 @@ const ORDERS_DIR = `${__dirname}/../orders`
// TODO: Use an eleventy hook to save this outside the public _site dir
const PRODUCTS_FILE = `${__dirname}/../_site/assets/data/products.json`
const formatUSD = (v) => v.toLocaleString(undefined, {
style: 'currency', currency: 'USD' })
// https://stackoverflow.com/a/57708635/3006854
const fileExists = async (f) => !!(await fs.stat(f).catch(e=>false))
const formatUSD = require('../lib/formatUSD')
// Read json files
const readDataFile = async (path) => {
@ -43,7 +39,7 @@ const getUspsRates = (zip, weight, volume) => {
// Check if order.id exists in fs already
const generateOrderId = async () => {
const id = `r${Math.round(Math.random()*1000000000)}`
if (await fileExists(`${ORDERS_DIR}/${id}.json`))
if (await require('../lib/fileExists')(`${ORDERS_DIR}/${id}.json`))
return generateOrderId()
else return id
}

3
lib/fileExists.js Normal file
View File

@ -0,0 +1,3 @@
const fs = require('fs').promises
// https://stackoverflow.com/a/57708635/3006854
module.exports = async (f) => !!(await fs.stat(f).catch(e=>false))

2
lib/formatUSD.js Normal file
View File

@ -0,0 +1,2 @@
module.exports = (v) => v.toLocaleString(undefined, {
style: 'currency', currency: 'USD' })

6
lib/run.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = async (cmd) =>
await require('child_process').exec(cmd, (err, stdout, stderr) => {
if (err) console.error(err.message)
if (stderr) console.log(stderr)
console.log(stdout)
})

View File

@ -11,7 +11,7 @@
"serve": "npx @11ty/eleventy --quiet --serve",
"json": "npx @11ty/eleventy --to=json",
"api": "node api/index.js",
"stripe-webhook": "node stripe-webhook.js"
"stripe-webhook": "node stripe-webhook/index.js"
},
"author": {
"name": "Keith Irwin",

View File

@ -1,138 +0,0 @@
'use strict'
require('dotenv').config()
const fs = require('fs').promises
const axios = require('axios')
const express = require('express')
const app = express()
const ORDERS_DIR = `${__dirname}/orders`
const SHOP_DIR = `${__dirname}/_src/shop`
const SOLD_DIR = `${__dirname}/sold`
// Mailserver
const mailer = require('nodemailer').createTransport({
host: process.env.MAIL_SERVER,
port: 587,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
tls: {
rejectUnauthorized: false,
},
})
// Run a command
const run = async (cmd) =>
await require('child_process').exec(cmd, (err, stdout, stderr) => {
if (err) console.error(err.message)
if (stderr) console.log(stderr)
console.log(stdout)
})
// Format float to USD string
const formatUSD = (v) => v.toLocaleString(undefined, {
style: 'currency', currency: 'USD' })
// Start webhook forwarder so we don't need a public endpoint
run(`stripe listen --api-key '${process.env.STRIPE_SEC}' --forward-to 'http://localhost:${process.env.HOOK_PORT}/' --format 'JSON'`)
// Receive that webhook
app.listen(process.env.HOOK_PORT)
app.post('/', express.json(), async (req) => {
if (req.body.data.object.object==='charge') {
// Check if paid
if (!req.body.data.object.paid)
return console.log(`[${req.body.id}] Charge unpaid!`)
// Get order ID
const orderId = req.body.data.object.metadata.id
if (orderId == null)
return console.error(`[${req.body.id}] Charge has no metadata.id!`)
else
console.log(`[${req.body.id}] Charge paid for order ${orderId}`)
// Get order file
const orderFile = `${ORDERS_DIR}/${orderId}.json`
let order; try {
order = await JSON.parse(await fs.readFile(orderFile))
} catch (err) {
return console.error(`[${req.body.id}] Failed to retrieve order from ${orderFile}:\n${err}`)
}
// Save paidDate to order
try { order.paidDate = new Date()
fs.writeFile(orderFile, JSON.stringify(order,null,2))
} catch (err) {
console.error(`[${req.body.id}] Failed to write paidDate to ${orderFile}:\n${err}`)
}
// Email customer
const customer_mail_items_string = order.items.reduce((acc,cur) =>
acc += `${cur.qty} ${formatUSD(cur.price)} ${cur.subtotal} ${cur.name}\n`
, 'Qty Price Subtotal Item\n')
let customer_mail_res; try {
console.log(`Emailing order ${order.id} confirmation to customer at ${order.contact.email}`)
customer_mail_res = await mailer.sendMail({
from: process.env.SALES_MAIL_FROM,
to: order.contact.email,
replyTo: process.env.SALES_EMAIL,
subject: `Order ${order.id} for ${formatUSD(order.total)} has been processed`,
text: `Dear ${order.contact.name},\n\nThank you for your payment. We will ship your order as soon as possible and send the tracking info to this email address. For now, keep this copy of your order details as proof of your payment. \n\n---\n\nITEMS: \n${customer_mail_items_string}\nTOTALS: \nSubtotal: ${formatUSD(order.subtotal)}\nTax: ${formatUSD(order.taxAmount)}\nShipping: ${formatUSD(order.shipping.amount)}\nProcessing: ${formatUSD(order.processing)}\n\nCONTACT: \n${order.contact.name}\n${order.contact.phone}\n${order.contact.email}\n\nSHIPPING ADDRESS:\n${order.shipping.address.name}\n${order.shipping.address.addr1}${(order.shipping.address.addr2)?'\n'+order.shipping.address.addr2:''}\n${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}\n\n---\n\nFor questions, comments, or to cancel this order, reply to this email or call 719-936-7778 x1. \n\nThank you!\n\n`,
})
} catch (err) { console.error(err) }
console.log(`Sent email ${customer_mail_res.messageId} to customer for order ${order.id}`)
// Ntfy sales team
axios.post(
`${process.env.NTFY_DOMAIN}/${process.env.NTFY_TOPIC}`,
`Order ${order.id} needs ${order.items.length} items shipped.`,
{ headers: {
Title: `Order for ${formatUSD(order.total)} processed`,
Tags: 'moneybag,package'
} }
)
// Email sales team
const sales_mail_items_string = order.items.reduce((acc,cur) =>
acc += `${cur.pid}/${cur.sid} ${cur.qty} ${cur.weight} ${cur.volume}\n`
, 'PID/SID qty lbs in³\n')
let sales_mail_res; try {
console.log(`Emailing sales team at ${process.env.SALES_EMAIL} about order ${order.id}`)
sales_mail_res = await mailer.sendMail({
from: process.env.SALES_MAIL_FROM,
to: process.env.SALES_EMAIL,
subject: `Order for ${formatUSD(order.total)} processed`,
text: `Order ${order.id} needs ${order.items.length} items shipped: \n\n${sales_mail_items_string}\nSHIP TO:\n${order.shipping.address.name}\n${order.shipping.address.addr1}${(order.shipping.address.addr2)?'\n'+order.shipping.address.addr2:''}\n${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}\n\nCONTACT:\n${order.contact.name}\n${order.contact.phone}\n${order.contact.email}\n`,
})
} catch (err) { console.error(err) }
console.log(`Sent email ${sales_mail_res.messageId} to sales team for order ${order.id}`)
// Remove single products from store
const getSoldSids = new Promise((resolve, reject) => {
order.items.forEach(async (item,i,arr) => {
let sids = []
if (item.sid) {
console.log(`Moving sid ${item.sid} from ${SHOP_DIR} to ${SOLD_DIR}`)
sids.push(item.sid)
await run(`find ${SHOP_DIR} -type d -name ${item.sid} -exec mv {} ${SOLD_DIR}/${item.sid} \\;`)
}
if (i===arr.length -1) resolve(sids)
})
})
let sold_sids; try {
sold_sids = await getSoldSids
if (sold_sids.length>0) {
run('npx @11ty/eleventy --quiet')
await run('git add _src/shop')
await run(`git commit -m 'Sold ${sold_sids.join()}'`)
run('git push')
}
} catch (err) { console.error(err) }
}
})

View File

@ -0,0 +1,17 @@
'use strict'
require('dotenv').config()
formatUSD = require('../lib/formatUSD')
const customer_mail_items_string = order.items.reduce((acc,cur) =>
acc += `${cur.qty} ${formatUSD(cur.price)} ${cur.subtotal} ${cur.name}\n`
, 'Qty Price Subtotal Item\n')
let customer_mail_res; try {
console.log(`Emailing order ${order.id} confirmation to customer at ${order.contact.email}`)
customer_mail_res = await require('../lib/mailer').sendMail({
from: process.env.SALES_MAIL_FROM,
to: order.contact.email,
replyTo: process.env.SALES_EMAIL,
subject: `Order ${order.id} for ${formatUSD(order.total)} has been processed`,
text: `Dear ${order.contact.name},\n\nThank you for your payment. We will ship your order as soon as possible and send the tracking info to this email address. For now, keep this copy of your order details as proof of your payment. \n\n---\n\nITEMS: \n${customer_mail_items_string}\nTOTALS: \nSubtotal: ${formatUSD(order.subtotal)}\nTax: ${formatUSD(order.taxAmount)}\nShipping: ${formatUSD(order.shipping.amount)}\nProcessing: ${formatUSD(order.processing)}\n\nCONTACT: \n${order.contact.name}\n${order.contact.phone}\n${order.contact.email}\n\nSHIPPING ADDRESS:\n${order.shipping.address.name}\n${order.shipping.address.addr1}${(order.shipping.address.addr2)?'\n'+order.shipping.address.addr2:''}\n${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}\n\n---\n\nFor questions, comments, or to cancel this order, reply to this email or call 719-936-7778 x1. \n\nThank you!\n\n`,
})
} catch (err) { console.error(err) }
console.log(`Sent email ${customer_mail_res.messageId} to customer for order ${order.id}`)

View File

@ -0,0 +1,15 @@
'use strict'
require('dotenv').config()
const sales_mail_items_string = order.items.reduce((acc,cur) =>
acc += `${cur.pid}/${cur.sid} ${cur.qty} ${cur.weight} ${cur.volume}\n`
, 'PID/SID qty lbs in³\n')
let sales_mail_res; try {
console.log(`Emailing sales team at ${process.env.SALES_EMAIL} about order ${order.id}`)
sales_mail_res = await require('../lib/mailer').sendMail({
from: process.env.SALES_MAIL_FROM,
to: process.env.SALES_EMAIL,
subject: `Order for ${require('../lib/formatUSD')(order.total)} processed`,
text: `Order ${order.id} needs ${order.items.length} items shipped: \n\n${sales_mail_items_string}\nSHIP TO:\n${order.shipping.address.name}\n${order.shipping.address.addr1}${(order.shipping.address.addr2)?'\n'+order.shipping.address.addr2:''}\n${order.shipping.address.city}, ${order.shipping.address.state} ${order.shipping.address.zip}\n\nCONTACT:\n${order.contact.name}\n${order.contact.phone}\n${order.contact.email}\n`,
})
} catch (err) { console.error(err) }
console.log(`Sent email ${sales_mail_res.messageId} to sales team for order ${order.id}`)

58
stripe-webhook/index.js Normal file
View File

@ -0,0 +1,58 @@
'use strict'
require('dotenv').config()
const fs = require('fs').promises
const express = require('express')
const app = express()
const ORDERS_DIR = `${__dirname}/orders`
// Start webhook forwarder so we don't need a public endpoint
require('../lib/run.js')(`stripe listen --api-key '${process.env.STRIPE_SEC}' --forward-to 'http://localhost:${process.env.HOOK_PORT}/' --format 'JSON'`)
// Receive that webhook
app.listen(process.env.HOOK_PORT)
app.post('/', express.json(), async (req) => {
if (req.body.data.object.object==='charge') {
// Check if paid
if (!req.body.data.object.paid)
return console.log(`[${req.body.id}] Charge unpaid!`)
// Get order ID
const orderId = req.body.data.object.metadata.id
if (orderId == null)
return console.error(`[${req.body.id}] Charge has no metadata.id!`)
else
console.log(`[${req.body.id}] Charge paid for order ${orderId}`)
// Get order file
const orderFile = `${ORDERS_DIR}/${orderId}.json`
let order; try {
order = await JSON.parse(await fs.readFile(orderFile))
} catch (err) {
return console.error(`[${req.body.id}] Failed to retrieve order from ${orderFile}:\n${err}`)
}
// Save paidDate to order
try { order.paidDate = new Date()
fs.writeFile(orderFile, JSON.stringify(order,null,2))
} catch (err) {
console.error(`[${req.body.id}] Failed to write paidDate to ${orderFile}:\n${err}`)
}
// Email customer
require('./email-customer')()
// Ntfy sales team
require('./ntfy-sales')()
// Email sales team
require('./email-sales')()
// Remove single products from store
require('./remove-sids')()
}
})

View File

@ -0,0 +1,11 @@
'use strict'
require('dotenv').config()
const axios = require('axios')
axios.post(
`${process.env.NTFY_DOMAIN}/${process.env.NTFY_TOPIC}`,
`Order ${order.id} needs ${order.items.length} items shipped.`,
{ headers: {
Title: `Order for ${require('../lib/formatUSD')(order.total)} processed`,
Tags: 'moneybag,package'
} }
)

View File

@ -0,0 +1,25 @@
'use strict'
require('dotenv').config()
const run = require('../lib/run')
const SHOP_DIR = `${__dirname}/_src/shop`
const SOLD_DIR = `${__dirname}/sold`
const getSoldSids = new Promise((resolve) => {
order.items.forEach(async (item,i,arr) => {
let sids = []
if (item.sid) {
console.log(`Moving sid ${item.sid} from ${SHOP_DIR} to ${SOLD_DIR}`)
sids.push(item.sid)
await run(`find ${SHOP_DIR} -type d -name ${item.sid} -exec mv {} ${SOLD_DIR}/${item.sid} \\;`)
}
if (i===arr.length -1) resolve(sids)
})
})
let sold_sids; try {
sold_sids = await getSoldSids
if (sold_sids.length>0) {
run('npx @11ty/eleventy --quiet')
await run('git add _src/shop')
await run(`git commit -m 'Sold ${sold_sids.join()}'`)
run('git push')
}
} catch (err) { console.error(err) }