Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added:Control Certer Backend APIs #45

Merged
merged 15 commits into from
Apr 3, 2024
3 changes: 2 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ SERVER_PORT=3001
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_NUMBER=
TEST_RECEPIENT_NUMBER=
TEST_RECEPIENT_NUMBER=
STRAPI_TOURISM_TOKEN=
1 change: 1 addition & 0 deletions .github/workflows/api_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
echo "TWILIO_AUTH_TOKEN=${{secrets.TWILIO_AUTH_TOKEN}}" >> .env
echo "TWILIO_NUMBER=${{secrets.TWILIO_NUMBER}}" >> .env
echo "TEST_RECEPIENT_NUMBER=${{secrets.TEST_RECEPIENT_NUMBER}}" >> .env
echo "STRAPI_TOURISM_TOKEN=${{secrets.STRAPI_TOURISM_TOKEN}}" >> .env

- name: Set up Node.js
uses: actions/setup-node@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
echo "TWILIO_AUTH_TOKEN=${{secrets.TWILIO_AUTH_TOKEN}}" >> .env
echo "TWILIO_NUMBER=${{secrets.TWILIO_NUMBER}}" >> .env
echo "TEST_RECEPIENT_NUMBER=${{secrets.TEST_RECEPIENT_NUMBER}}" >> .env

echo "STRAPI_TOURISM_TOKEN=${{secrets.STRAPI_TOURISM_TOKEN}}" >> .env
- name: Create SSH key file
run: echo -e "${{ secrets.EC2_SSH_KEY }}" > ~/ec2_key
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
echo "TWILIO_AUTH_TOKEN=${{secrets.TWILIO_AUTH_TOKEN}}" >> .env
echo "TWILIO_NUMBER=${{secrets.TWILIO_NUMBER}}" >> .env
echo "TEST_RECEPIENT_NUMBER=${{secrets.TEST_RECEPIENT_NUMBER}}" >> .env

echo "STRAPI_TOURISM_TOKEN=${{secrets.STRAPI_TOURISM_TOKEN}}" >> .env
- name: Set up Node.js
uses: actions/setup-node@v2
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
echo "TWILIO_AUTH_TOKEN=${{secrets.TWILIO_AUTH_TOKEN}}" >> .env
echo "TWILIO_NUMBER=${{secrets.TWILIO_NUMBER}}" >> .env
echo "TEST_RECEPIENT_NUMBER=${{secrets.TEST_RECEPIENT_NUMBER}}" >> .env

echo "STRAPI_TOURISM_TOKEN=${{secrets.STRAPI_TOURISM_TOKEN}}" >> .env
- name: Set up Node.js
uses: actions/setup-node@v2
with:
Expand Down
97 changes: 97 additions & 0 deletions controllers/ControlCenter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Actions from '../services/Actions.js'
import logger from '../utils/logger.js'
import {
ITEM_ID,
ITEM_NAME,
CAT_ATTR_TAG_RELATIONS,
NEW_CATALOG_AVAILABLE,
TRIGGER_BLIZZARD_MESSAGE,
CANCEL_BOOKING_MESSAGE,
TOURISM_STRAPI_URL
} from '../utils/constants.js'

const action = new Actions()

const TWILIO_RECEPIENT_NUMBER = process.env.TEST_RECEPIENT_NUMBER
export const cancelBooking = async (req, res) => {
try {
const { orderId } = req.body
if(!orderId){
return res.status(400).json({message:"Order Id is Required", status:false})
}

const validOrderId = await action.call_api(`${TOURISM_STRAPI_URL}/orders/${orderId}`,'GET',{},{ Authorization: `Bearer ${process.env.STRAPI_TOURISM_TOKEN}`})
logger.info(`OrderDetails: ${JSON.stringify(validOrderId)}`)
if(!validOrderId.status){
return res.status(400).send({ message: `Invalid Order Id`, status:false })
}
const messageBody = CANCEL_BOOKING_MESSAGE;
const getOrderAddressDetails = await action.call_api(`${TOURISM_STRAPI_URL}/order-addresses?order_id=${orderId}`,'GET',{},{ Authorization: `Bearer ${process.env.STRAPI_TOURISM_TOKEN}`})

const getOrderFulfillmentDetails = await action.call_api(`${TOURISM_STRAPI_URL}/order-fulfillments?order_id=${orderId}`,'GET',{},{ Authorization: `Bearer ${process.env.STRAPI_TOURISM_TOKEN}`})
if (getOrderFulfillmentDetails.data.data.length) {
await action.call_api(`${TOURISM_STRAPI_URL}/order-fulfillments/${getOrderFulfillmentDetails.data.data[0].id}`,'PUT',{
data: {
state_code: 'CANCELLED',
state_value: 'CANCELLED BY HOTEL',
},
},{ Authorization: `Bearer ${process.env.STRAPI_TOURISM_TOKEN}`})
let statusMessage = "";

if(getOrderAddressDetails.data.data[0].attributes.phone){
statusMessage = (await action.send_message(`+91${getOrderAddressDetails.data.data[0].attributes.phone}`, messageBody)).deliveryStatus
}
else{
statusMessage = (await action.send_message(TWILIO_RECEPIENT_NUMBER, messageBody)).deliveryStatus
}
return res.status(200).send({ message: `Notification ${statusMessage}`, status:true })
}

return res.status(200).send({ message: 'Cancel Booking Failed', status:false })
} catch (error) {
logger.error(error.message)
return res.status(400).send({ message: error.message, status:false })
}
}

export const updateCatalog = async (req, res) => {
try {
const { userNo = TWILIO_RECEPIENT_NUMBER } = req.body;
const messageBody = NEW_CATALOG_AVAILABLE;
await action.call_api(`${TOURISM_STRAPI_URL}/items/${ITEM_ID}`,'PUT',{
data: {
name: ITEM_NAME,
cat_attr_tag_relations: CAT_ATTR_TAG_RELATIONS,
},
},{ Authorization: `Bearer ${process.env.STRAPI_TOURISM_TOKEN}`})
const notifyResponse = await action.send_message(userNo, messageBody)

if(!notifyResponse || notifyResponse.deliveryStatus === "failed"){
throw new Error('Notification Failed')
}
return res.status(200).send({ message: 'Catalog Updated', status:true })
} catch (error) {
logger.error(error.message)
return res.status(400).send({ message: error.message, status:false })
}
}


export const notify = async (req, res) => {
try {
const { userNo = TWILIO_RECEPIENT_NUMBER } = req.body;
const messageBody = TRIGGER_BLIZZARD_MESSAGE;
const sendWhatsappNotificationResponse = await action.send_message(
userNo,
messageBody
)
if(sendWhatsappNotificationResponse.deliveryStatus === "failed"){
return res.status(400).json({...sendWhatsappNotificationResponse, status:false})
}
sendWhatsappNotificationResponse.deliveryStatus = 'delivered'
return res.status(200).json({...sendWhatsappNotificationResponse, status:true})
} catch (error) {
logger.error(error.message)
return res.status(400).send({ message: error.message, status:false })
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"body-parser": "^1.20.2",
"chai": "^5.0.0",
"config": "^3.3.11",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"js-yaml": "^4.1.0",
Expand Down
19 changes: 13 additions & 6 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import dotenv from 'dotenv'
import cors from 'cors'
dotenv.config()
import express from 'express'
import bodyParser from 'body-parser'
import logger from './utils/logger.js'
import messageController from './controllers/Bot.js'
import DBService from './services/DBService.js'

import {
cancelBooking,
updateCatalog,
notify
} from './controllers/ControlCenter.js'
const app = express()

app.use(cors())
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))

Expand All @@ -16,11 +21,13 @@ app.use(bodyParser.json())

// Define endpoints here
// app.post('/act', actions.act)
app.post('/webhook', messageController.process_text)

app.post('/webhook', messageController.process_wa_webhook)
app.post('/notify', notify)
app.post('/cancel-booking', cancelBooking)
app.post('/update-catalog', updateCatalog)
// Reset all sessions
const db = new DBService();
await db.clear_all_sessions();
const db = new DBService()
await db.clear_all_sessions()

// Start the Express server
app.listen(process.env.SERVER_PORT, () => {
Expand Down
9 changes: 4 additions & 5 deletions services/Actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Actions {
// optimise search results.
// This code will ensure that for search resylts, only the responses with catalog providers are returned and out of them we only take the first resopnse to further reduce the token size.
// This should be imlemented by different baps based on their requirements.
if(request.data.context.action==='search'){
if(request.data.context && request.data.context.action==='search'){
response.data.responses = response.data.responses.filter(res => res.message && res.message.catalog && res.message.catalog.providers && res.message.catalog.providers.length > 0)
if(response.data.responses.length > 0)
response.data.responses = response.data.responses.slice(0, 1);
Expand Down Expand Up @@ -106,14 +106,13 @@ class Actions {

async send_message(recipient, message) {
try {

const response = await client.messages.create({
const data = await client.messages.create({
body: message,
from: `whatsapp:${twilioNumber}`,
to: recipient.includes('whatsapp:') ? recipient : `whatsapp:${recipient}`,
})
logger.info(`Message sent: ${JSON.stringify(response)}`)
return true;
const status = await client.messages(data.sid).fetch()
return { deliveryStatus: status.status }
} catch (error) {
logger.error(`Error sending message: ${error.message}`)
return false;
Expand Down
95 changes: 95 additions & 0 deletions tests/unit/controllers/controlCenter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, it} from 'mocha'
import app from '../../../server.js'
import request from 'supertest'
import * as chai from 'chai'
const expect = chai.expect


describe('API tests for /notify endpoint for an end to end Notify Request', () => {
it('Should test unsuccess response for invalid whatsapp number.', async () => {
const response = await request(app).post('/notify').send({
"userNo":"INVALID_NUMBER"
})
expect(response.status).to.equal(400)
})

it('Should test success response for no whatsapp number provided in the payload and will sent to TEST_RECEPIENT_NUMBER', async () => {
const response = await request(app).post('/notify').send({})

expect(response.status).to.equal(200)
expect(response._body.status).to.equal(true)
expect(response._body.deliveryStatus).to.not.equal('failed')
})

it('Should test success response for valid whatsapp number', async () => {
const response = await request(app).post('/notify').send({
"userNo":process.env.TEST_RECEPIENT_NUMBER
})
expect(response.status).to.equal(200)
expect(response._body.status).to.equal(true)
expect(response._body.deliveryStatus).to.not.equal('failed')
})


})



describe('API tests for /cancel-booking endpoint for an end to end Notify Message', () => {
it('Should test unsuccess response for invalid order Id.', async () => {
const response = await request(app).post('/cancel-booking').send({
"orderId":"Abcd"
})
expect(response.status).equal(400)
expect(response._body.status).equal(false)
expect(response._body.status).equal(false)
})

it('Should test unsuccess response for no order Id.', async () => {
const response = await request(app).post('/cancel-booking').send({})
expect(response.status).equal(400)
expect(response._body.status).equal(false)
expect(response._body.status).equal(false)
})


it('Should test success response for valid order Id.', async () => {
const response = await request(app).post('/cancel-booking').send({
"orderId":"1"
})

expect(response.status).equal(200)
expect(response._body.status).equal(true)
expect(response._body.message).to.not.equal('Notification failed')
})


})

describe('API tests for /update-catalog endpoint for an end to end Notify Message', () => {
mayurvir marked this conversation as resolved.
Show resolved Hide resolved
it('Should test success response for invalid whatsapp No.', async () => {
const response = await request(app).post('/update-catalog').send({
"userNo":"INVALID_NUMBER"
})

expect(response.status).equal(400)
expect(response._body.status).equal(false)
expect(response._body.message).equal('Notification Failed')
})

it('Should test success response for no whatsapp number provided in the payload and will sent to TEST_RECEPIENT_NUMBER', async () => {
const response = await request(app).post('/update-catalog').send({})
expect(response.status).equal(200)
expect(response._body.status).equal(true)
expect(response._body.message).equal('Catalog Updated')
})

it('Should test success response for valid whatsapp number', async () => {
const response = await request(app).post('/update-catalog').send({
"userNo":process.env.TEST_RECEPIENT_NUMBER
})
expect(response.status).equal(200)
expect(response._body.status).equal(true)
expect(response._body.message).equal('Catalog Updated')
})
})
9 changes: 3 additions & 6 deletions tests/unit/services/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,40 +52,37 @@ describe('should test send_message()', () => {
const message = "hi, this is a test message";

let status = await actionsService.send_message(recipient, message);
expect(status).to.be.true;

expect(status.deliveryStatus).to.not.equal('failed')
});

it('should test send a message via Twilio with a whatsapp prefix', async () => {
const recipient = `whatsapp:${process.env.TEST_RECEPIENT_NUMBER}`;
const message = "hi, this is a test message";

let status = await actionsService.send_message(recipient, message);
expect(status).to.be.true;
expect(status.deliveryStatus).to.not.equal('failed')

});

it('should throw an error for invalid recipient', async () => {
const recipient = '';
const message = 'Test message';

try {
await actionsService.send_message(recipient, message);
throw new Error('Expected an error to be thrown');
} catch (error) {

expect(error).to.be.an.instanceOf(Error);
}
});

it('should throw an error for empty message', async () => {
const recipient = process.env.TEST_RECEPIENT_NUMBER;
const message = '';

try {
await actionsService.send_message(recipient, message);
throw new Error('Expected an error to be thrown');
} catch (error) {

mayurvir marked this conversation as resolved.
Show resolved Hide resolved
expect(error).to.be.an.instanceOf(Error);
}
});
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/services/ai.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('Test cases for get_context_by_instruction()', async () => {
expect(config.bap_url).to.equal(registry_config[0].bpp_subscriber_uri);
})

it('Should return right config for search action in retail contect', async () => {
it('Should return right config for search action in retail context', async () => {
ai.action = {action: 'search'};
const config = await ai.get_context_by_instruction("I'm looking for some pet food");;
expect(config).to.have.property('action')
Expand Down
8 changes: 8 additions & 0 deletions utils/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const ITEM_ID = '4'
export const ITEM_NAME = 'Ticket Pass-Mueseum'

export const CAT_ATTR_TAG_RELATIONS = [2, 3, 4, 5]
export const TRIGGER_BLIZZARD_MESSAGE = "Hey, Triggering a Blizzard";
export const CANCEL_BOOKING_MESSAGE = `Dear Guest,\n\nApologies, but your hotel booking with us has been canceled due to unforeseen circumstances. \nWe understand the inconvenience and are here to assist you with any alternative arrangements needed. \n\nPlease contact us for further assistance.`;
export const NEW_CATALOG_AVAILABLE = `Dear Guest,\n\n Checkout this new place to visit.`
export const TOURISM_STRAPI_URL = "https://mit-bpp-tourism.becknprotocol.io/api"
Loading