├── CONTRIBUTING.md ├── package.json ├── LICENSE ├── src ├── keyGenerator.js ├── encryption.js ├── server.js └── endpoint.js ├── README.md └── CODE_OF_CONDUCT.md /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to WhatsApp Checkout Button Template Endpoint 2 | We are not accepting contributions to this repo right now. Contributions will not be addressed. 3 | 4 | ## Issues 5 | We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-checkout-button-template-endpoint", 3 | "version": "1.0.0", 4 | "description": "An example endpoint server for WhatsApp Checkout https://developers.facebook.com/docs/whatsapp/cloud-api/payments-api/payments-in/checkout-button-templates", 5 | "main": "src/server.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node src/server.js" 9 | }, 10 | "author": "WhatsApp Checkout & Payments Team", 11 | "license": "MIT", 12 | "dependencies": { 13 | "express": "^4.21.2", 14 | "cors": "^2.8.5" 15 | }, 16 | "engines": { 17 | "node": ">=16.0.0" 18 | }, 19 | "keywords": [] 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/keyGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /* The script will generate a public and private key pair and log the same in the console. 9 | * Copy paste the private key into your /.env file and public key should be added to your account. 10 | * For more details visit: https://developers.facebook.com/docs/whatsapp/cloud-api/payments-api/payments-in/checkout-button-templates 11 | * 12 | * Run this script using command below: 13 | * 14 | * node src/keyGenerator.js {passphrase} 15 | * 16 | */ 17 | 18 | import crypto from "crypto"; 19 | 20 | const passphrase = process.argv[2]; 21 | if (!passphrase) { 22 | throw new Error( 23 | "Passphrase is empty. Please include passphrase argument to generate the keys like: node src/keyGenerator.js {passphrase}" 24 | ); 25 | } 26 | 27 | try { 28 | const keyPair = crypto.generateKeyPairSync("rsa", { 29 | modulusLength: 2048, 30 | publicKeyEncoding: { 31 | type: "spki", 32 | format: "pem", 33 | }, 34 | privateKeyEncoding: { 35 | type: "pkcs1", 36 | format: "pem", 37 | cipher: "des-ede3-cbc", 38 | passphrase, 39 | }, 40 | }); 41 | 42 | console.log(`Successfully created your public private key pair. Please copy the below values into your /.env file 43 | ************* COPY PASSPHRASE & PRIVATE KEY BELOW TO .env FILE ************* 44 | PASSPHRASE="${passphrase}" 45 | 46 | PRIVATE_KEY="${keyPair.privateKey}" 47 | ************* COPY PASSPHRASE & PRIVATE KEY ABOVE TO .env FILE ************* 48 | 49 | ************* COPY PUBLIC KEY BELOW ************* 50 | ${keyPair.publicKey} 51 | ************* COPY PUBLIC KEY ABOVE ************* 52 | `); 53 | } catch (err) { 54 | console.error("Error while creating public private key pair:", err); 55 | } 56 | -------------------------------------------------------------------------------- /src/encryption.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import crypto from "crypto"; 9 | import { CheckoutButtonTemplateEndpointException } from "./endpoint.js"; 10 | 11 | export const decryptRequest = (body, privatePem, passphrase) => { 12 | const { encrypted_aes_key, encrypted_flow_data, initial_vector } = body; 13 | 14 | const privateKey = crypto.createPrivateKey({ key: privatePem, passphrase }); 15 | let decryptedAesKey = null; 16 | 17 | try { 18 | // decrypt AES key created by client 19 | decryptedAesKey = crypto.privateDecrypt( 20 | { 21 | key: privateKey, 22 | padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, 23 | oaepHash: "sha256", 24 | }, 25 | Buffer.from(encrypted_aes_key, "base64") 26 | ); 27 | } catch (error) { 28 | console.error(error); 29 | /* 30 | Failed to decrypt. Please verify your private key. 31 | If you change your public key. You need to return HTTP status code 421 to refresh the public key on the client 32 | */ 33 | throw new CheckoutButtonTemplateEndpointException(421, { 34 | error_msg: `Failed to decrypt the request. Please verify your private key.`, 35 | }); 36 | } 37 | 38 | // decrypt checkout data 39 | const checkoutDataBuffer = Buffer.from(encrypted_flow_data, "base64"); 40 | const initialVectorBuffer = Buffer.from(initial_vector, "base64"); 41 | 42 | const TAG_LENGTH = 16; 43 | const encrypted_flow_data_body = checkoutDataBuffer.subarray(0, -TAG_LENGTH); 44 | const encrypted_flow_data_tag = checkoutDataBuffer.subarray(-TAG_LENGTH); 45 | 46 | const decipher = crypto.createDecipheriv( 47 | "aes-128-gcm", 48 | decryptedAesKey, 49 | initialVectorBuffer 50 | ); 51 | decipher.setAuthTag(encrypted_flow_data_tag); 52 | 53 | console.log("encrypted_flow_data_tag", encrypted_flow_data_tag); 54 | 55 | const decryptedJSONString = Buffer.concat([ 56 | decipher.update(encrypted_flow_data_body), 57 | decipher.final(), 58 | ]).toString("utf-8"); 59 | 60 | return { 61 | decryptedBody: JSON.parse(decryptedJSONString), 62 | aesKeyBuffer: decryptedAesKey, 63 | initialVectorBuffer, 64 | }; 65 | }; 66 | 67 | export const encryptResponse = ( 68 | response, 69 | aesKeyBuffer, 70 | initialVectorBuffer 71 | ) => { 72 | // flip initial vector 73 | const flipped_iv = []; 74 | for (const pair of initialVectorBuffer.entries()) { 75 | flipped_iv.push(~pair[1]); 76 | } 77 | 78 | // encrypt response data 79 | const cipher = crypto.createCipheriv( 80 | "aes-128-gcm", 81 | aesKeyBuffer, 82 | Buffer.from(flipped_iv) 83 | ); 84 | return Buffer.concat([ 85 | cipher.update(JSON.stringify(response), "utf-8"), 86 | cipher.final(), 87 | cipher.getAuthTag(), 88 | ]).toString("base64"); 89 | }; 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WhatsApp Checkout Button Template Endpoint 2 | 3 | This endpoint example is designed to be used with the [checkout button template](https://developers.facebook.com/docs/whatsapp/cloud-api/payments-api/payments-in/checkout-button-templates) to enable coupons, shipping address and inventory handling at real-time. 4 | 5 | ## Checkout Button Template Endpoint Docs 6 | 7 | Refer to the [docs here for implementing your checkout button template endpoint](https://developers.facebook.com/docs/whatsapp/cloud-api/payments-api/payments-in/checkout-button-templates#enabling_coupons_inventory) 8 | 9 | ## ⚠️ WARNING ⚠️ 10 | 11 | - This project is meant to be an example for prototyping only. It's not production ready. 12 | - When you remix (fork) this project on Glitch, your code is public by default, unless you choose to make it private (requires paid subscription to Glitch). Do not use this for any proprietary/private code. 13 | - Env variables are stored & managed by Glitch. Never use the private keys for your production accounts here. Create a temporary private key for testing on Glitch only and replace it with your production key in your own infrastructure. 14 | - Running this endpoint example on Glitch is completely optional and is not required to use WhatsApp Flows. You can run this code in any other environment you prefer. 15 | 16 | ## Glitch Setup 17 | 18 | 1. Create an account on Glitch to have access to all features mentioned here. 19 | 2. Remix this project on Glitch. 20 | 3. Create a private & public key pair for testing, if you haven't already, using the included script `src/keyGenerator.js`. Run the below command in the terminal to generate a key pair, then follow [these steps to upload the key pair](https://developers.facebook.com/docs/whatsapp/flows/guides/implementingyourflowendpoint#upload_public_key) to your business phone number. 21 | 22 | ``` 23 | node src/keyGenerator.js {passphrase} 24 | ``` 25 | 26 | 4. Click on the file ".env" on the left sidebar, **then click on `✏️ Plain text` on top. Do not edit it directly from UI as it will break your key formatting.** 27 | 5. Edit it with your private key and passphrase. Make sure a multiline key has the same line breaks like below. Env variables are only visible to the owner of the Glitch project. **Use a separate private key for testing only, and not your production key.** 28 | 29 | ``` 30 | PASSPHRASE="my-secret" 31 | 32 | PRIVATE_KEY="-----[REPLACE THIS] BEGIN RSA PRIVATE KEY----- 33 | MIIE... 34 | ... 35 | ...xyz 36 | -----[REPLACE THIS] END RSA PRIVATE KEY-----" 37 | ``` 38 | 39 | 6. Use the new Glitch URL as your checkout button template endpoint URL, eg: `https://project-name.glitch.me`. You can find this URL by clicking on `Share` on top right, then copy the `Live Site` URL. 40 | 7. Edit `src/endpoint.js` with your logic to provide response to actions like applying coupon or shipping etc. 41 | 8. Click on the `Logs` tab at the bottom to view server logs. The logs section also has a button to attach a debugger via Chrome devtools. 42 | 43 | ## License 44 | WhatsApp Checkout API is [MIT licensed, as found in the LICENSE file](./LICENSE). 45 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import express from "express"; 9 | import cors from "cors"; 10 | import { decryptRequest, encryptResponse } from "./encryption.js"; 11 | import { 12 | getResponse, 13 | CheckoutButtonTemplateEndpointException, 14 | } from "./endpoint.js"; 15 | import crypto from "crypto"; 16 | 17 | const app = express(); 18 | app.use(cors()); 19 | 20 | app.use(express.static("public")); 21 | 22 | app.use( 23 | express.json({ 24 | // store the raw request body to use it for signature verification 25 | verify: (req, res, buf, encoding) => { 26 | req.rawBody = buf?.toString(encoding || "utf8"); 27 | }, 28 | }) 29 | ); 30 | 31 | const { APP_SECRET, PRIVATE_KEY, PASSPHRASE = "", PORT = "3000" } = process.env; 32 | 33 | app.post("/", async (req, res) => { 34 | if (!PRIVATE_KEY) { 35 | throw new Error( 36 | 'Private key is empty. Please check your env variable "PRIVATE_KEY".' 37 | ); 38 | } 39 | 40 | if (!isRequestSignatureValid(req)) { 41 | // Return status code 432 if request signature does not match. 42 | // To learn more about return error codes visit: https://developers.facebook.com/docs/whatsapp/flows/reference/error-codes#endpoint_error_codes 43 | return res.status(432).send(); 44 | } 45 | 46 | let decryptedRequest = null; 47 | try { 48 | decryptedRequest = decryptRequest(req.body, PRIVATE_KEY, PASSPHRASE); 49 | } catch (err) { 50 | throw new Error( 51 | 'Unable to decrypt the request. Please check your env variable "PRIVATE_KEY" is valid' 52 | ); 53 | return; 54 | } 55 | 56 | const { aesKeyBuffer, initialVectorBuffer, decryptedBody } = decryptedRequest; 57 | console.log("💬 Decrypted Request:", decryptedBody); 58 | 59 | // TODO: Uncomment this block and add your flow token validation logic. 60 | // If the flow token becomes invalid, return HTTP code 427 to disable the flow and show the message in `error_msg` to the user 61 | // Refer to the docs for details https://developers.facebook.com/docs/whatsapp/flows/reference/error-codes#endpoint_error_codes 62 | if (!isValidEndpointToken(decryptedBody.flow_token)) { 63 | const error_response = { 64 | error_msg: `This is an invalid data exchange request message from endpoint`, 65 | }; 66 | return res 67 | .status(427) 68 | .send(encryptResponse(error_response, aesKeyBuffer, initialVectorBuffer)); 69 | } 70 | 71 | try { 72 | const screenResponse = await getResponse(decryptedBody); 73 | console.log("👉 Response to Encrypt:", screenResponse); 74 | res.send( 75 | encryptResponse(screenResponse, aesKeyBuffer, initialVectorBuffer) 76 | ); 77 | } catch (err) { 78 | sendErrorResponse(res, err, aesKeyBuffer, initialVectorBuffer); 79 | return; 80 | } 81 | }); 82 | 83 | app.get("/", (req, res) => { 84 | res.send(`
Nothing to see here. Checkout README.md to start.
`); 85 | }); 86 | 87 | app.listen(PORT, () => { 88 | console.log(`Server is listening on port: ${PORT}`); 89 | }); 90 | 91 | function isValidEndpointToken(flowToken) { 92 | return true; 93 | } 94 | 95 | function sendErrorResponse(res, err, aesKeyBuffer, initialVectorBuffer) { 96 | console.error(err); 97 | if (err instanceof CheckoutButtonTemplateEndpointException) { 98 | return res 99 | .status(err.statusCode) 100 | .send(encryptResponse(err.response, aesKeyBuffer, initialVectorBuffer)); 101 | } 102 | return res.status(500).send(); 103 | } 104 | 105 | function isRequestSignatureValid(req) { 106 | if (!APP_SECRET) { 107 | console.warn( 108 | "App Secret is not set up. Please Add your app secret in /.env file to check for request validation" 109 | ); 110 | return true; 111 | } 112 | 113 | const signatureHeader = req.get("x-hub-signature-256"); 114 | const signatureBuffer = Buffer.from( 115 | signatureHeader.replace("sha256=", ""), 116 | "utf-8" 117 | ); 118 | 119 | const hmac = crypto.createHmac("sha256", APP_SECRET); 120 | const digestString = hmac.update(req.rawBody).digest("hex"); 121 | const digestBuffer = Buffer.from(digestString, "utf-8"); 122 | 123 | if (!crypto.timingSafeEqual(digestBuffer, signatureBuffer)) { 124 | console.error("Error: Request Signature did not match"); 125 | return false; 126 | } 127 | return true; 128 | } 129 | -------------------------------------------------------------------------------- /src/endpoint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const COUPONS_RESPONSE = { 9 | GET_COUPONS: { 10 | version: "replace_version", 11 | sub_action: "replace_sub_action", 12 | data: { 13 | coupons: [ 14 | { 15 | description: "Get 10% off on your 1st order above ₹100.", 16 | code: "TRYNEW10", 17 | id: "trynew10_ref_id", 18 | }, 19 | { 20 | description: "15% off up to ₹75", 21 | code: "NEWEYE15", 22 | id: "neweye15_ref_id", 23 | }, 24 | { 25 | description: 26 | "Get 50% off on your 1st order above ₹80. Maximum Discount: ₹50", 27 | code: "WELCOME50", 28 | id: "welcome50_ref_id", 29 | }, 30 | ], 31 | }, 32 | }, 33 | }; 34 | 35 | const EMPTY_COUPONS_RESPONSE = { 36 | GET_COUPONS: { 37 | version: "replace_version", 38 | sub_action: "replace_sub_action", 39 | data: { 40 | coupons: [], 41 | }, 42 | }, 43 | }; 44 | 45 | const NO_COUPONS_RESPONSE = { 46 | GET_COUPONS: { 47 | version: "replace_version", 48 | sub_action: "replace_sub_action", 49 | data: {}, 50 | }, 51 | }; 52 | 53 | const COUPON_DISCOUNT = { 54 | TRYNEW10: 10, 55 | NEWEYE15: 15, 56 | WELCOME50: 50, 57 | }; 58 | 59 | const DEFAULT_SHIPPING_COST = 100; 60 | 61 | export const getResponse = async (decryptedBody) => { 62 | const { screen, data, version, action, sub_action, flow_token } = 63 | decryptedBody || {}; 64 | 65 | // handle health check request 66 | if (action === "ping") { 67 | return { 68 | version, 69 | data: { 70 | status: "active", 71 | }, 72 | }; 73 | } 74 | 75 | // handle error notification 76 | if (data?.error) { 77 | console.warn("Received client error:", data); 78 | return { 79 | version, 80 | data: { 81 | acknowledged: true, 82 | }, 83 | }; 84 | } 85 | 86 | if (action === "data_exchange") { 87 | switch (sub_action) { 88 | case "get_coupons": 89 | return { 90 | ...COUPONS_RESPONSE.GET_COUPONS, 91 | version: version, 92 | sub_action: sub_action, 93 | }; 94 | 95 | case "apply_coupon": { 96 | const order_details = data?.order_details; 97 | const couponCode = data?.input?.coupon?.code; 98 | const couponId = data?.input?.coupon?.id; 99 | 100 | if (!order_details || !couponCode || !couponId) { 101 | console.error("Order details, coupon code or id is missing."); 102 | return { 103 | version: version, 104 | sub_action: sub_action, 105 | data: { error: "Invalid order details, coupon code or id." }, 106 | }; 107 | } 108 | 109 | const order = order_details?.order; 110 | const items = order?.items; 111 | if (couponCode === "TRYNEW10" && items?.[0]?.sale_amount?.value) { 112 | const itemDiscount = 113 | (items[0].sale_amount.value * COUPON_DISCOUNT[couponCode]) / 100; 114 | items[0].sale_amount.value -= itemDiscount; 115 | order.subtotal.value -= itemDiscount; 116 | order_details.total_amount.value -= itemDiscount; 117 | 118 | order_details.coupon = { 119 | code: couponCode, 120 | id: couponId, 121 | discount: { 122 | value: itemDiscount, 123 | offset: 100, 124 | }, 125 | }; 126 | } else if (couponCode.toUpperCase() === "code5".toUpperCase()) { 127 | // 5% discount on the manually enter discount code "CODE" 128 | const discount = (order_details.total_amount?.value * 5) / 100; 129 | if (discount) { 130 | order_details.total_amount.value -= discount; 131 | order_details.coupon = { 132 | code: couponCode, 133 | id: couponId, 134 | discount: { 135 | value: discount, 136 | offset: 100, 137 | }, 138 | }; 139 | } 140 | } else if (couponCode === "NEWEYE15" || couponCode === "WELCOME50") { 141 | const discount = 142 | (order_details.total_amount?.value * COUPON_DISCOUNT[couponCode]) / 143 | 100; 144 | if (discount) { 145 | order_details.total_amount.value -= discount; 146 | order_details.coupon = { 147 | code: couponCode, 148 | id: couponId, 149 | discount: { 150 | value: discount, 151 | offset: 100, 152 | }, 153 | }; 154 | } 155 | } else { 156 | throw new CheckoutButtonTemplateEndpointException(427, { 157 | error_msg: `The offer code you have entered is not valid.`, 158 | }); 159 | } 160 | 161 | return { 162 | version: version, 163 | sub_action: sub_action, 164 | data: { 165 | order_details: order_details, 166 | }, 167 | }; 168 | } 169 | 170 | case "remove_coupon": { 171 | const order_details = data?.order_details; 172 | const coupon = order_details?.coupon; 173 | const couponCode = coupon?.code; 174 | const couponId = coupon?.id; 175 | const discount = coupon?.discount?.value; 176 | 177 | if (!order_details || !couponCode || !couponId || !discount) { 178 | throw new CheckoutButtonTemplateEndpointException(421, { 179 | error_msg: `Invalid Request - Order details, coupon id, coupon code, or discount missing.`, 180 | }); 181 | } 182 | 183 | if (couponCode === "TRYNEW10") { 184 | const order = order_details?.order; 185 | const items = order?.items; 186 | 187 | if (items?.[0]?.sale_amount?.value) { 188 | items[0].sale_amount.value += discount; 189 | order.subtotal.value += discount; 190 | order_details.total_amount.value += discount; 191 | } 192 | order_details.coupon = {}; 193 | } else { 194 | order_details.total_amount.value += discount; 195 | delete order_details.coupon; 196 | } 197 | 198 | return { 199 | version: version, 200 | sub_action: sub_action, 201 | data: { 202 | order_details: order_details, 203 | }, 204 | }; 205 | } 206 | 207 | case "apply_shipping": { 208 | const order_details = data?.order_details; 209 | const selectedAddress = data?.input?.selected_address; 210 | 211 | if (!order_details || !selectedAddress) { 212 | throw new CheckoutButtonTemplateEndpointException(421, { 213 | error_msg: `Invalid Request - Order details or selected address missing.`, 214 | }); 215 | } 216 | 217 | if (selectedAddress.in_pin_code !== "400051") { 218 | throw new CheckoutButtonTemplateEndpointException(427, { 219 | error_msg: `Currently we operate only in Mumbai area (400051)`, 220 | }); 221 | } 222 | 223 | const shipping_info = order_details?.shipping_info; 224 | if (shipping_info) { 225 | if (shipping_info.selected_address) { 226 | shipping_info.selected_address = selectedAddress; 227 | } else { 228 | order_details.shipping_info.selected_address = selectedAddress; 229 | const order = order_details?.order; 230 | 231 | if (order?.shipping?.value != null) { 232 | order.shipping.value += DEFAULT_SHIPPING_COST; 233 | order_details.total_amount.value += DEFAULT_SHIPPING_COST; 234 | } 235 | } 236 | } 237 | 238 | return { 239 | version: version, 240 | sub_action: sub_action, 241 | data: { 242 | order_details: order_details, 243 | }, 244 | }; 245 | } 246 | 247 | default: 248 | throw new CheckoutButtonTemplateEndpointException(421, { 249 | error_msg: `Invalid Request - Unsupported sub action`, 250 | }); 251 | } 252 | } 253 | 254 | throw new CheckoutButtonTemplateEndpointException(421, { 255 | error_msg: `Invalid Request - Unsupported action`, 256 | }); 257 | }; 258 | 259 | export const CheckoutButtonTemplateEndpointException = class CheckoutButtonTemplateEndpointException extends Error { 260 | constructor(statusCode, response) { 261 | super(response.error_msg); 262 | 263 | this.name = this.constructor.name; 264 | this.statusCode = statusCode; 265 | this.response = response; 266 | } 267 | }; 268 | --------------------------------------------------------------------------------