Moved webhook listener outside docker
This commit is contained in:
parent
75ef9c8b6c
commit
568c17622b
|
@ -3,6 +3,7 @@ SITE_PORT="8080"
|
|||
SITE_DOMAIN="http://localhost:8080"
|
||||
API_PORT="8081"
|
||||
API_DOMAIN="http://localhost:8081"
|
||||
HOOK_PORT="8082"
|
||||
ADMIN_EMAIL="hostmaster@example.com"
|
||||
SALES_EMAIL="sales@example.com"
|
||||
STRIPE_PUB="pk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
|
@ -13,6 +14,7 @@ STRIPE_SEC="sk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|||
#SITE_DOMAIN="https://www.example.com"
|
||||
#API_PORT="8081"
|
||||
#API_DOMAIN="https://api.example.com"
|
||||
#HOOK_PORT="8082"
|
||||
#ADMIN_EMAIL="hostmaster@example.com"
|
||||
#SALES_EMAIL="sales@example.com"
|
||||
#STRIPE_PUB="pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
|
|
32
README.md
32
README.md
|
@ -1,4 +1,3 @@
|
|||
|
||||
# Shopity
|
||||
|
||||
Ecommerce template using 11ty, knockout, stripe, docker
|
||||
|
@ -7,3 +6,34 @@ Ecommerce template using 11ty, knockout, stripe, docker
|
|||
|
||||
- **zips-to-zones:** [USPS Zone chart by zip code](https://postcalc.usps.com/DomesticZoneChart)
|
||||
- **usps-shipping-rates:** [USPS Rates by zone/weight](https://pe.usps.com/text/dmm300/Notice123.htm#_c078)
|
||||
|
||||
## Website and API
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## Stripe hook
|
||||
|
||||
Runs on `HOOK_PORT`.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run stripe-webhook
|
||||
```
|
||||
|
||||
`/etc/systemd/system/www-stripe-hook.service`
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Stripe webhook
|
||||
Documentation=
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/usr/local/src/mysite
|
||||
ExecStart=npm run stripe-webhook.js
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
|
@ -1,8 +1,19 @@
|
|||
require('dotenv').config()
|
||||
module.exports = () => { return {
|
||||
STRIPE_PUB: JSON.parse(process.env.STRIPE_PUB),
|
||||
API_DOMAIN: JSON.parse(process.env.API_DOMAIN),
|
||||
SITE_DOMAIN: JSON.parse(process.env.SITE_DOMAIN),
|
||||
ADMIN_EMAIL: JSON.parse(process.env.ADMIN_EMAIL),
|
||||
SALES_EMAIL: JSON.parse(process.env.SALES_EMAIL),
|
||||
} }
|
||||
let obj = {}
|
||||
|
||||
try { obj.STRIPE_PUB = JSON.parse(process.env.STRIPE_PUB) }
|
||||
catch (err) { obj.STRIPE_PUB = process.env.STRIPE_PUB }
|
||||
|
||||
try { obj.API_DOMAIN = JSON.parse(process.env.API_DOMAIN) }
|
||||
catch (err) { obj.API_DOMAIN = process.env.API_DOMAIN }
|
||||
|
||||
try { obj.SITE_DOMAIN = JSON.parse(process.env.SITE_DOMAIN) }
|
||||
catch (err) { obj.SITE_DOMAIN = process.env.SITE_DOMAIN }
|
||||
|
||||
try { obj.ADMIN_EMAIL = JSON.parse(process.env.ADMIN_EMAIL) }
|
||||
catch (err) { obj.ADMIN_EMAIL = process.env.ADMIN_EMAIL }
|
||||
|
||||
try { obj.SALES_EMAIL = JSON.parse(process.env.SALES_EMAIL) }
|
||||
catch (err) { obj.SALES_EMAIL = process.env.SALES_EMAIL }
|
||||
|
||||
module.exports = () => obj
|
||||
|
|
100
api.js
100
api.js
|
@ -1,6 +1,5 @@
|
|||
'use strict'
|
||||
require('dotenv').config()
|
||||
const exec = require('child_process').exec
|
||||
const fs = require('fs').promises
|
||||
const stripe = require('stripe')(JSON.parse(process.env.STRIPE_SEC))
|
||||
const express = require('express')
|
||||
|
@ -14,13 +13,14 @@ const formatUSD = (v) => v.toLocaleString(undefined, {
|
|||
const jsonBodyParser = express.json()
|
||||
app.listen(process.env.API_PORT||80)
|
||||
const DATA_DIR = `${__dirname}/_src/_data`
|
||||
const SHOP_DIR = `${__dirname}/_src/shop`
|
||||
const SOLD_DIR = `${__dirname}/sold`
|
||||
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`
|
||||
|
||||
|
||||
// Run a command
|
||||
const run = (cmd) => {
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
require('child_process').exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) console.error(err.message)
|
||||
if (stderr) console.log(stderr)
|
||||
console.log(stdout)
|
||||
|
@ -82,7 +82,7 @@ app.options('/order', cors)
|
|||
const weightPromise = new Promise((resolve, reject) => {
|
||||
let sum = 0
|
||||
order.items.forEach((item) => {
|
||||
console.log(`Checking weight of products['${item.pid||''}/${item.sid||''}']`)
|
||||
//console.log(`Checking weight of products['${item.pid||''}/${item.sid||''}']`)
|
||||
if (products[`${item.pid}/${item.sid}`].weight.lbs !== parseFloat(item.weight))
|
||||
reject(`${item.pid}/${item.sid} weight of ${products[`${item.pid}/${item.sid}`].weight.lbs} lbs did not match the request, which gave it ${item.weight} lbs`)
|
||||
else sum += parseFloat(item.weight) * parseFloat(item.qty)
|
||||
|
@ -93,7 +93,7 @@ app.options('/order', cors)
|
|||
const volumePromise = new Promise((resolve, reject) => {
|
||||
let sum = 0
|
||||
order.items.forEach((item) => {
|
||||
console.log(`Checking volume of products['${item.pid||''}/${item.sid||''}']`)
|
||||
//console.log(`Checking volume of products['${item.pid||''}/${item.sid||''}']`)
|
||||
if (products[`${item.pid||''}/${item.sid||''}`].volume.in3 !== parseFloat(item.volume))
|
||||
reject(`${item.pid}/${item.sid} volume of ${products[`${item.pid||''}/${item.sid||''}`].volume.in3} in^3 did not match the request, which gave it ${item.volume} lbs`)
|
||||
else sum += parseFloat(item.volume) * parseFloat(item.qty)
|
||||
|
@ -206,33 +206,33 @@ app.options('/order', cors)
|
|||
// Save order to fs
|
||||
console.log(`[${ip}] Order ${order.id} for ${formatUSD(order.total)} verified.`)
|
||||
const orderFile = `${ORDERS_DIR}/${order.id}.json`
|
||||
try { fs.writeFile(orderFile,
|
||||
JSON.stringify({
|
||||
id: order.id,
|
||||
items: order.items,
|
||||
contactName: order.contact.name,
|
||||
contactPhone: order.contact.phone,
|
||||
contactEmail: order.contact.email,
|
||||
shipName: order.shippingAddress.name,
|
||||
shipAddr1: order.shippingAddress.addr1,
|
||||
shipAddr2: order.shippingAddress.addr2,
|
||||
shipCity: order.shippingAddress.city,
|
||||
shipState: order.shippingAddress.state,
|
||||
shipZip: order.shippingAddress.zip,
|
||||
subtotal: parseFloat(order.subtotal),
|
||||
shipCarrier: '',
|
||||
shipTracking: '',
|
||||
tax: parseFloat(order.tax),
|
||||
taxAmount: parseFloat(order.tax_amount),
|
||||
processing: parseFloat(order.processing),
|
||||
shipping: parseFloat(order.shipping),
|
||||
total: parseFloat(order.total),
|
||||
paymentMethod: 'USD',
|
||||
orderDate: new Date(),
|
||||
paidDate: null,
|
||||
shipDate: null,
|
||||
}, null, 2)
|
||||
) } catch (err) { console.error(`ERROR! Failed to save ${orderFile}:\n${err}`) }
|
||||
try {
|
||||
await fs.writeFile(orderFile,
|
||||
JSON.stringify({
|
||||
id: order.id,
|
||||
orderDate: new Date(),
|
||||
items: order.items,
|
||||
contact: order.contact,
|
||||
shippingAddress: order.shippingAddress,
|
||||
shipping: {
|
||||
date: null,
|
||||
address: order.shippingAddress,
|
||||
carrier: '',
|
||||
tracking: [],
|
||||
amount: parseFloat(order.shipping),
|
||||
},
|
||||
subtotal: parseFloat(order.subtotal),
|
||||
tax: parseFloat(order.tax),
|
||||
taxAmount: parseFloat(order.tax_amount),
|
||||
processing: parseFloat(order.processing),
|
||||
total: parseFloat(order.total),
|
||||
paymentMethod: 'USD',
|
||||
paidDate: null,
|
||||
}, null, 2),
|
||||
{mode:'666',owner:7000,group:7000}
|
||||
)
|
||||
fs.chown(orderFile,7000,7000)
|
||||
} catch (err) { console.error(`ERROR! Failed to save ${orderFile}:\n${err}`) }
|
||||
|
||||
}
|
||||
}).catch((err) => {
|
||||
|
@ -242,37 +242,3 @@ app.options('/order', cors)
|
|||
})
|
||||
|
||||
})
|
||||
|
||||
// Receive paymentIntent.success from stripe
|
||||
// Should only be available to stripe-hook container
|
||||
// No need to be world-visible!
|
||||
app.options('/paid', cors)
|
||||
.post('/paid', jsonBodyParser, cors, async (req, res) => {
|
||||
// Send an ok back to stripe immediately because we got it
|
||||
res.sendStatus(200)
|
||||
const orderId = req.body.data.object.metadata.id
|
||||
console.log(`Received stripe hook for order ${orderId}`)
|
||||
const orderFile = `${ORDERS_DIR}/${orderId}.json`
|
||||
// If we fail to save that fact, let it error out
|
||||
// and fix it by hand in the data later
|
||||
|
||||
// Save paidDate to filesystem
|
||||
// TODO: Catch errors for each awaited
|
||||
let order = await JSON.parse(await fs.readFile(orderFile))
|
||||
order.paidDate = new Date()
|
||||
await fs.writeFile(orderFile, JSON.stringify(order,null,2))
|
||||
|
||||
// Notify sales team
|
||||
run(`./hooks/ntfy ${order.id} ${order.items.length} ${formatUSD(order.total)}`)
|
||||
run(`./hooks/email-sales '${JSON.stringify(order)}'`)
|
||||
|
||||
// Email customer
|
||||
run(`./hooks/email-customer ${order.id} ${order.contactEmail} ${formatUSD(order.total)}`)
|
||||
|
||||
// Remove single products from store
|
||||
order.items.forEach((item) => {
|
||||
if (item.sid)
|
||||
run(`./hooks/remove_sid ${item.sid} ${SHOP_DIR} ${SOLD_DIR} ${__dirname}`)
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -44,9 +44,9 @@ services:
|
|||
- "./orders:/usr/local/src/orders"
|
||||
- "./sold:/usr/local/src/sold"
|
||||
|
||||
stripe-hook:
|
||||
restart: always
|
||||
image: stripe/stripe-cli:latest
|
||||
command: "listen --api-key ${STRIPE_SEC} --forward-to http://api:${API_PORT}/paid --events payment_intent.succeeded"
|
||||
volumes:
|
||||
- "/etc/timezone:/etc/timezone:ro"
|
||||
# stripe-hook:
|
||||
# restart: always
|
||||
# image: stripe/stripe-cli:latest
|
||||
# command: "listen --api-key ${STRIPE_SEC} --forward-to http://api:${API_PORT}/paid --events payment_intent.succeeded"
|
||||
# volumes:
|
||||
# - "/etc/timezone:/etc/timezone:ro"
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
#
|
||||
# USAGE: email-customer orderId contactEmail orderTotal
|
||||
|
||||
printf 'Emailing order %s to customer at %s' "${1}" "${2}"
|
||||
|
|
|
@ -2,4 +2,6 @@
|
|||
# email-sales
|
||||
#
|
||||
# USAGE: email-sales '{order}'
|
||||
source .env
|
||||
|
||||
printf 'Emailing order %s to sales at %s' "$(<<<${1} jq -r '.id')" "${SALES_EMAIL}"
|
||||
|
|
|
@ -12,4 +12,4 @@ curl --silent \
|
|||
-d "Order ${1} needs ${2} items shipped." \
|
||||
"${URL}" >/dev/null \
|
||||
&& printf 'Notified %s about order %s\n' "${URL}" "${1}" \
|
||||
| || printf 'FAILED to notify %s about order %s\n' "${URL}" "${1}"jq '.message'
|
||||
|| printf 'FAILED to notify %s about order %s\n' "${URL}" "${1}"
|
|
@ -3,5 +3,7 @@
|
|||
#
|
||||
# USAGE: remove_sid sid shop_dir sold_dir git_dir
|
||||
|
||||
printf 'Removing sid %s from website\n' "${1}" "${2}" "${3}" "${4}"
|
||||
find "${2}" -type d -name "${1}" -exec mv {} "${3}/${1}" \; 2>/dev/null && npm run build
|
||||
|
||||
find "${2}" -type d -name "${1}" -exec mv {} "${3}/${1}" \; 2>/dev/null
|
||||
printf 'Removed sid %s from website' "${1}"
|
||||
source .env && npx @11ty/eleventy --quiet
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
"build": "npx @11ty/eleventy --quiet",
|
||||
"serve": "npx @11ty/eleventy --quiet --serve",
|
||||
"json": "npx @11ty/eleventy --to=json",
|
||||
"api": "node api.js"
|
||||
"api": "node api.js",
|
||||
"stripe-webhook": "node stripe-webhook.js"
|
||||
},
|
||||
"author": {
|
||||
"name": "Keith Irwin",
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
'use strict'
|
||||
require('dotenv').config()
|
||||
const fs = require('fs').promises
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
|
||||
const ORDERS_DIR = `${__dirname}/orders`
|
||||
const SHOP_DIR = `${__dirname}/_src/shop`
|
||||
const SOLD_DIR = `${__dirname}/sold`
|
||||
|
||||
// Run a command
|
||||
const run = (cmd) => {
|
||||
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
|
||||
run(`./hooks/email-customer ${order.id} ${order.contact.email} ${formatUSD(order.total)}`)
|
||||
|
||||
// Notify sales team
|
||||
run(`./hooks/ntfy ${order.id} ${order.items.length} ${formatUSD(order.total)}`)
|
||||
run(`./hooks/email-sales '${JSON.stringify(order)}'`)
|
||||
|
||||
// Remove single products from store
|
||||
order.items.forEach((item) => {
|
||||
if (item.sid)
|
||||
run(`./hooks/remove_sid ${item.sid} ${SHOP_DIR} ${SOLD_DIR} ${__dirname}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
})
|
Loading…
Reference in New Issue