diff --git a/README.md b/README.md index 92e61a8..882b629 100644 --- a/README.md +++ b/README.md @@ -22,27 +22,23 @@ How to use: - Scope can be "Admin client" (not recommended) or "Manage" Cart discounts and Discount codes - Download the created API client's "Environment variables (.env)" file before closing the popup - Add the .env file to the project root -- Run `node get-cart-discounts.js`, copy the desired ID (UUID format, like `f9c4718e-0792-4f08-a802-70f81ef9d46d`) -- Run `node generate-discount-codes.js STUD20- f9c4718e-0792-4f08-a802-70f81ef9d46d 20 > newcodes.csv` -- Check `newcodes.csv` for the generated discount codes -- Sanity check the generated discount codes in Merchant Center > Discounts > Discount code list -- Test code behaviours in the cart without using them, or by removing the used up codes from the csv -- On https://my.sheerid.com/ - - Create a SheerID program, e.g. "Student discount" - - Upload `newcodes.csv` to the Codes step "Single-Use Codes" card - - Copy the Web URL from Publish step "New Page" - -- Add the copied Web URL to your website as a banner link or button -- Test linked banner on your website, fill the form, copy the code and test cart and checkout (the code will be invalidated in SheerID system immediately after successful verification) -- Don't forget to switch the program to Live mode on https://my.sheerid.com/ - -## Server for SheerID webhook - -As an example for running a server providing a webhook endpoint for [SheerID Settings](https://my.sheerid.com/settings) Webhook (coming), see `server.js`. - -To avoid the need of changing `.env` file `server.js` has several hardcoded entries that should be part of configuration, -like port. -The created cart rules are not "empty", but they need setting up in Merchant Center after the webhook. +- Log in to your SheerID Dashboard and + - Create a SheerID program, that you will use e.g. "Student discount" + - Configure your program with eligibility, theme etc + - Set Codes section to "No Code" + - In Program Settings + - set Webhook for eligible verification to `https:///api/success-webhook` + - add cartid as Campaign Metadata field + - Copy access token from Settings > Access Tokens page +- Edit the downloaded .env file, add +``` +SHEERID_TOKEN= +SHEERID_API_URL=https://services.sheerid.com/rest/v2/ +URL=https://sheeriddemo.gpmd.net/ +PORT=8080 +``` +- Run `node server.js` or `yarn server` to run the bridge application. +- Check that it's running by visiting the server URL indicated by the application. ## This repository diff --git a/generate-discount-codes.js b/generate-discount-codes.js deleted file mode 100644 index ba3f28f..0000000 --- a/generate-discount-codes.js +++ /dev/null @@ -1,63 +0,0 @@ -import config from './src/config.js'; -import fetch from 'node-fetch'; - -import { auth } from './src/auth'; -import { nano } from './src/nano'; -import { getCartDiscount } from './src/discount.js'; - -function makeid(length) { - var result = ''; - var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - var charactersLength = characters.length; - for (var i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - - return (result.substring(0, length / 2) + nano() + result.substring(length / 2, length)).toUpperCase(); -} - -const main = async () => { - if (process.argv.length < 5) { - console.log('Usage: node generate-discount-codes.js '); - return; - } - const token = await auth(); - if (token.error) { - console.log(token.error); - return; - } - - const prefix = process.argv[2]; - const cartDiscountId = process.argv[3]; - const numberOfCodes = process.argv[4]; - - const discount = await getCartDiscount(token.access_token, cartDiscountId); - console.log('Discount Codes'); - - for (let i = 0; i < numberOfCodes; i++) { - fetch( - `${config.CTP_API_URL}/${config.CTP_PROJECT_KEY}/discount-codes`, - { - 'method': 'POST', - 'headers': { - 'Authorization': 'Bearer ' + token.access_token, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - "code": prefix + makeid(6), - "name": discount.name, - "cartDiscounts": [ - { - "typeId": "cart-discount", - "id": cartDiscountId - } - ], - "isActive": true, - "cartPredicate": "1=1" - }) - }); - } -} - -main(); - diff --git a/get-cart-discounts.js b/get-cart-discounts.js deleted file mode 100644 index acd6050..0000000 --- a/get-cart-discounts.js +++ /dev/null @@ -1,29 +0,0 @@ -var request = require('request'); -var config = require('dotenv').config().parsed; - -const auth = require('./src/auth'); - -async function main() { - const token = await auth(); - if (token.error) { - console.log(token.error); - return; - } - - request({ - 'method': 'GET', - 'url': `${config.CTP_API_URL}/${config.CTP_PROJECT_KEY}/cart-discounts`, - 'headers': { - 'Authorization': 'Bearer '+token.access_token, - } - }, function (error, response) { - if (error) throw new Error(error); - const res = JSON.parse(response.body); - res.results.forEach(element => { - console.log(`${element.key}: ${element.id} - ${element.name.en}`); - }); - }); -} - -main(); - diff --git a/get-discount-codes.js b/get-discount-codes.js deleted file mode 100644 index 6d76190..0000000 --- a/get-discount-codes.js +++ /dev/null @@ -1,30 +0,0 @@ -var request = require('request'); -var config = require('dotenv').config().parsed; - -const auth = require('./src/auth'); - -async function main() { - const token = await auth(); - if (token.error) { - console.log(token.error); - return; - } - - request({ - 'method': 'GET', - 'url': `${config.CTP_API_URL}/${config.CTP_PROJECT_KEY}/discount-codes`, - 'headers': { - 'Authorization': 'Bearer '+token.access_token, - } - }, function (error, response) { - if (error) throw new Error(error); - const res = JSON.parse(response.body); - res.results.forEach(element => { - // console.log(element) - console.log(`${element.code}: ${element.id} - ${element.name.en} (${element.cartDiscounts[0].id})`); - }); - }); -} - -main(); - diff --git a/package.json b/package.json index c821b8f..e22d5b6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "dotenv": "^16.0.2", - "node-fetch": "^3.2.10" + "node-fetch": "^3.2.10", + "redis": "^4.3.1" }, "imports": { "@/*": "./src/*" diff --git a/server.js b/server.js index b15c812..0ef1830 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,25 @@ -import { config } from 'dotenv'; +import { config } from './src/config.js'; import http from 'http'; import { auth } from './src/auth.js'; +import url from 'url'; +import { createClient } from 'redis'; +import { createWebhook, getVerification } from './src/sheerid.js'; + +const redis = createClient(); +redis.on('error', (err) => console.log('Redis Client Error', err)); + +console.log('connecting to Redis...'); +await redis.connect(); + +const verificationStatus = async (verificationId) => { + return await getVerification(verificationId); +} + +const onSuccess = async (cartId, verificationData) => { + // create new discount code + // add directDiscount to cart + return await redis.set(`cart-${cartId}`, JSON.stringify(verificationData)) +} import { getCartDiscounts, createCartDiscount, createDiscountCode } from './src/discount.js'; @@ -8,9 +27,15 @@ const token = await auth(); http.createServer(async (req, res) => { if (req.url === '/' && req.method === 'GET') { + console.log('get /'); res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('SheerID - commercetools Demo server'); - } else if (req.url === '/cart-discounts' && req.method === 'GET') { + } else if (req.url.startsWith('/api/create-webhook')) { + const q = url.parse(req.url, true).query; + const r = await createWebhook(q.pid); + res.end(JSON.stringify(r)); + } else if (req.url === '/api/cart-discounts' && req.method === 'GET') { + console.log('get /api/cart-discounts'); res.writeHead(200, {'Content-Type': 'application/json'}); const discounts = await getCartDiscounts(token.access_token); console.log('number of discounts:', discounts.total); @@ -20,7 +45,17 @@ http.createServer(async (req, res) => { o[element.id] = element.name.en; }); res.end(JSON.stringify(o)); - } else if (req.url === '/success-webhook' && req.method === 'POST') { + } else if (req.url.startsWith('/api/verify') && req.method === 'GET') { + const q = url.parse(req.url, true).query; + console.log('get /api/verify', q.cid); + let cartJson = await redis.get(`cart-${q.cid}`) + if (cartJson === null) { + cartJson = `{}`; + } + res.writeHead(200, {'Content-Type': 'application/json'}); + res.end(cartJson); + } else if (req.url === '/api/success-webhook' && req.method === 'POST') { + console.log('post /api/success-webhook'); let body = []; req.on('data', (chunk) => { body.push(chunk); @@ -28,11 +63,25 @@ http.createServer(async (req, res) => { body = Buffer.concat(body).toString(); res.end('OK'); if (body != undefined && body.length > 0) { - const discount = JSON.parse(body); - createDiscountCode("SHEERID-", discount.name, cartDiscountId); + const res = JSON.parse(body); + verificationStatus(res.verificationId).then((r) => { + try { + if (r.personInfo?.metadata != undefined) { + const cartId = r.personInfo.metadata.cartid; + console.log(`saving ${cartId} cart id`); + onSuccess(cartId, r); + updateStatus(cartId, r); + } else { + console.log('no metadata', r); + } + } catch(err) { + console.log('error:', err); + } + }); } }); - } else if (req.url === '/webhook' && req.method === 'POST') { + } else if (req.url === '/api/webhook' && req.method === 'POST') { + console.log('post /api/cart-discounts'); let body = []; req.on('data', (chunk) => { body.push(chunk); @@ -46,8 +95,9 @@ http.createServer(async (req, res) => { } }); } else { + console.log('404', req); res.statusCode = 404; res.end('404: File Not Found'); } -}).listen(80); -console.log('server is running on http://localhost/'); +}).listen(config.PORT); +console.log(`server is running on http://localhost:${config.PORT}/`); diff --git a/src/sheerid.js b/src/sheerid.js index e08c3bf..2de0e48 100644 --- a/src/sheerid.js +++ b/src/sheerid.js @@ -1,21 +1,33 @@ import fetch from 'node-fetch'; import { config } from './config.js'; -const createWebhook = async (programID) => { +const createWebhook = async (programId) => { const res = await fetch( - `${config.SHEERID_API_URL}/rest/v2/program/${programID}/webhook`, { - 'method': 'post', - 'headers': { - 'Authorization': `Basic ${config.SHEERID_TOKEN}`, - 'Content-Type': 'application/json' - }, - data: { - 'scope': config.URL + '/success-webhook' - } + `${config.SHEERID_API_URL}/program/${programId}/webhook`, { + 'method': 'POST', + 'headers': { + 'Authorization': `Bearer ${config.SHEERID_TOKEN}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'callbackUri': `${config.URL}/api/success-webhook`, + }) }); - return await res.json(); + return res.json(); } -export { createWebhook }; +const getVerification = async (verificationId) => { + const res = await fetch( + `${config.SHEERID_API_URL}/verification/${verificationId}/details`, { + 'method': 'GET', + 'headers': { + 'Authorization': `Bearer ${config.SHEERID_TOKEN}`, + 'Content-Type': 'application/json' + }, + }); + return res.json(); +} + +export { createWebhook, getVerification };