diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..495ea0076 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# dependencies +node_modules + +# misc +.env +*.log diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..1ad991772 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": false, + "trailingComma": "none" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..eafe7efd0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2025 UNIQUELY PARTICULAR LLC and Adam Grohs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..ce7650c71 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# @particular./sync-shippo-to-moltin + +> Update order shipping status when delivered in Shippo + +Asynchronous microservice that is triggered by [Shippo](https://goshippo.com) webhooks to update Order data inside of [moltin](https://moltin.com). + +Built with [Micro](https://github.com/zeit/micro)! 🤩 + +## 🛠 Setup + +Both a [moltin](https://moltin.com) _and_ [Shippo](https://goshippo.com) account are needed for this to function. + +Create a `.env` at the project root with the following credentials: + +```dosini +MOLTIN_CLIENT_ID= +MOLTIN_CLIENT_SECRET= +``` + +Find your `MOLTIN_CLIENT_ID` and `MOLTIN_CLIENT_SECRET` inside of your [moltin Dashboard](https://dashboard.moltin.com)'s API keys. + +## 📦 Package + +Run the following command to build the app + +```bash +yarn install +``` + +Start the development server + +```bash +yarn dev +``` + +The server will typically start on PORT `3000`, if not, make a note for the next step. + +Start ngrok (change ngrok port below from 3000 if yarn dev deployed locally on different port above) + +```bash +ngrok http 3000 +``` + +Make a note of the https 'URL' ngrok provides. + +## ⛽️ Usage + +Next head over to the Shippo [API Settings](https://app.goshippo.com/settings/api) area, add a new webhook with the following details: + +| Event Type | Mode | URL | +| ------------- | ------ | ------------------- | +| Track Updated | `Test` | _`ngrok URL` above_ | + +⚠️ Each time a `charge` is `refunded` this function will be called, but it will only call moltin to update order if 'fully refunded' in Stripe (TODO: if Moltin add support for order.payment = partial_refund then can update to handle). + +## 🚀 Deploy + +You can easily deploy this function to [now](https://now.sh). diff --git a/index.js b/index.js new file mode 100644 index 000000000..ff5ae51af --- /dev/null +++ b/index.js @@ -0,0 +1,111 @@ +const { buffer, send } = require('micro') +const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY) +const { MemoryStorageFactory } = require('@moltin/sdk') +const moltinGateway = require('@moltin/sdk').gateway +const moltin = moltinGateway({ + client_id: process.env.MOLTIN_CLIENT_ID, + client_secret: process.env.MOLTIN_CLIENT_SECRET, + storage: new MemoryStorageFactory(), + application: 'demo-sync-shippo-to-moltin' +}) +const cors = require('micro-cors')({ + allowMethods: ['POST'], + exposeHeaders: ['stripe-signature'], + allowHeaders: [ + 'stripe-signature', + 'user-agent', + 'x-forwarded-proto', + 'X-Requested-With', + 'Access-Control-Allow-Origin', + 'X-HTTP-Method-Override', + 'Content-Type', + 'Authorization', + 'Accept' + ] +}) + +const _toJSON = error => { + return !error + ? '' + : Object.getOwnPropertyNames(error).reduce( + (jsonError, key) => { + return { ...jsonError, [key]: error[key] } + }, + { type: 'error' } + ) +} + +const handleError = (res, error) => { + console.error(error) + const jsonError = _toJSON(error) + return send( + res, + jsonError.type === 'StripeSignatureVerificationError' ? 401 : 500, + jsonError + ) +} + +process.on('unhandledRejection', (reason, p) => { + console.error( + 'Promise unhandledRejection: ', + p, + ', reason:', + JSON.stringify(reason) + ) +}) + +module.exports = cors(async (req, res) => { + if (req.method === 'OPTIONS') { + return send(res, 200, 'ok!') + } + + try { + const sig = await req.headers['stripe-signature'] + const body = await buffer(req) + + // NOTE: expects metadata field to be populated w/ moltin order data when charges were first created, email is automatically there + const { + type, + data: { + object: { + id: reference, + status, + refunded, + metadata: { email, order_id } + } + } + } = await stripe.webhooks.constructEvent( + body, + sig, + process.env.STRIPE_WEBHOOK_SECRET + ) + + if ( + type === 'charge.refunded' && + status === 'succeeded' && + refunded === true + ) { + // if refunded !== true, then only partial (moltin Order.Payment does not support partial_refund status) + if (order_id) { + moltin.Transactions.All({ order: order_id }) + .then(transactions => { + const moltinTransaction = transactions.data.find( + transaction => transaction.reference === reference + ) + + moltin.Transactions.Refund({ + order: order_id, + transaction: moltinTransaction.id + }) + .then(moltinRefund => { + return send(res, 200, JSON.stringify({ received: true })) + }) + .catch(error => handleError(error)) + }) + .catch(error => handleError(error)) + } + } + } catch (error) { + handleError(error) + } +}) diff --git a/now.json b/now.json new file mode 100644 index 000000000..211ecfed4 --- /dev/null +++ b/now.json @@ -0,0 +1,15 @@ +{ + "name": "demo-sync-shippo-to-moltin", + "alias": "particular-demo-sync-shippo-to-moltin.now.sh", + "env": { + "NODE_ENV": "production", + "MOLTIN_CLIENT_ID": "@demo-moltin-client-id", + "MOLTIN_CLIENT_SECRET": "@demo-moltin-client-secret" + }, + "builds": [ + { + "src": "*.js", + "use": "@now/node" + } + ] +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..684a2bd98 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@particular./sync-shippo-to-moltin", + "version": "1.0.0", + "author": "Adam Grohs (https://uniquelyparticular.com)", + "owner": "Particular.", + "description": "Please contact Particular. via info@uniquelyparticular.com for any questions", + "keywords": [ + "moltin", + "shippo", + "webhooks", + "microservices", + "shipping", + "ecommerce", + "integration", + "particular", + "particular." + ], + "license": "MIT", + "homepage": "https://github.com/uniquelyparticular/sync-shippo-to-moltin#readme", + "repository": "github:uniquelyparticular/sync-shippo-to-moltin", + "main": "index.js", + "scripts": { + "dev": "micro-dev", + "precommit": "pretty-quick --staged", + "start": "micro" + }, + "dependencies": { + "@moltin/sdk": "^3.19.0", + "micro": "^9.3.3", + "micro-cors": "^0.1.1" + }, + "devDependencies": { + "husky": "1.3.1", + "micro-dev": "3.0.0", + "prettier": "1.16.4", + "pretty-quick": "1.10.0" + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..cf13e9be4 --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "extends": ["config:base", "docker:disable", ":skipStatusChecks"], + "automerge": true, + "major": { + "automerge": false + } +}