Masking phone numbers of couriers and customers using a key-value store
Tags: showdev, javascript, programming, tutorial
Published: 2021-09-17 15:40:44

The pandemic has greatly affected delivery services, and they have become more in demand than ever before. As we all know, a courier and a customer need each other's phone numbers to make a call and discuss the order details. **But what about privacy?** Many delivery services have already thought this question through, and each uses its own number masking solution. I, in turn, want to tell you how to mask phone numbers with the [Voximplant](https://voximplant.com/?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs) key-value store. So let’s get the party started!

## How it works
We will create a scenario that allows a customer and a courier to make calls **without knowing each other’s phone numbers**.

Meanwhile, we’ll have only one so-called “neutral” phone number to call for both a courier and a customer. We’ll rent this number in the Voximplant panel. Then, we’ll create some data structure to connect a courier and a customer with each other by an order number (a **key** if we are referring to a key-value store).

When calling a rented number, a caller enters their order number. If such an order exists in the database, our scenario checks the numbers attached to it. Then, if it identifies a phone number as a customer’s, we put them through to the courier responsible for the order, and vice versa.

For example, a call from a courier to a customer looks like this:
![Scheme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3d4kwcl4a770wypqulem.png)

If it doesn’t find the caller’s phone number in the database, we suggest they call again from the number they used when placing the order. Or we simply transfer them to an operator.

Let’s go straight to the implementation.

## What you need
- A Voximplant account which you can [create here](https://manage.voximplant.com/auth/sign_up?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs);
- [A Voximplant application with a scenario and a rule for this scenario](https://voximplant.com/docs/introduction/introduction_to_voximplant/basic_concepts?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs) (we’ll create it all together);
- Test phone numbers: a number rented from Voximplant, a courier’s, a customer’s, and an operator’s numbers. In the test version, we can omit an operator’s number.

1. To begin, log into your Voximplant account: [manage.voximplant.com/auth](https://manage.voximplant.com/auth?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs). In the menu on the left, click [Applications](https://manage.voximplant.com/applications?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs) and then click **Create application** in the upper right corner. Give it a name (for example, **numberMasking**) and click **Create**.

2. Open a newly created app and go to **Scenarios**. Create a scenario by clicking the “+” button and name it **kvs-scenario**. You will work with the code here, but everything is not quite ready yet; wait until we add code to the scenario.
 
3. Go to the **Routing** tab and create a rule for the scenario. Leave the pattern (regular expression) “.*” as default. This way your rule will work for all phone numbers.
![Rule](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6iakgye8k0aon5ayzplb.png)

4. Rent a real phone number. To do that, go to the [Numbers](https://manage.voximplant.com/numbers?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs) section, select one and purchase it. This number will be called by a courier and a customer and will be displayed to them instead of their personal numbers.

    > Please note that phone numbers are offered on-demand, except in countries where regulations require additional steps. You will find more information when renting a phone number.

    ![Phone number purchase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mlgya5qkw1ldblterd49.png)

    In Voximplant, you can also rent test numbers to see how everything works. In our case, we need a real phone number to [initiate a call from the platform](https://voximplant.com/docs/howtos/calls/pstn?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs).

5. The last thing is to attach the phone number to your application. To do that, open your application, go to the **Numbers** → **Available** and click **Attach**. Here you can also attach your rule so it works for incoming calls, and all other rules are ignored.

Great! The structure is ready, now you just need to fill the key-value store and add some code to the scenario.

## Key-value store
The scenario works properly if the key-value store isn’t empty. To fill it up, use the [Voximplant Management API](https://voximplant.com/docs/references/httpapi?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs). I use the [Python API client](https://github.com/voximplant/apiclient-python). It requires Python 2.x or 3.x with pip and setuptools> = 18.5 installed.

1. Go to your project folder and install the SDK using `pip`:
`python -m pip install --user voximplant-apiclient`

2. Create a .py file and write the code that adds the order details to the key-value store. The [set_key_value_item](https://voximplant.com/docs/references/httpapi/keyvaluestorage#setkeyvalueitem) will help you do that:

    ```python
    from voximplant.apiclient import VoximplantAPI, 
    VoximplantException

    if __name__ == "__main__":
        voxapi = VoximplantAPI("credentials.json")
    
        # SetKeyValueItem example

        KEY = 12345
        VALUE = '{"courier": "12222222222", "client": 
    "13333333333"}'
        APPLICATION_ID = 1
        TTL = 864000
    
        try:
            res = voxapi.set_key_value_item(KEY,
                VALUE,
                APPLICATION_ID,
                ttl=TTL)
            print(res)
        except VoximplantException as e:
            print("Error: {}".format(e.message))
    ```

    You can generate a **credentials.json** file yourself when creating a service account in the [Service accounts](https://manage.voximplant.com/settings/service_accounts?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs) section. Don’t forget to choose a role that will allow you to call the `set_key_value_item` method. **Owner**, for example, will do well.

    Find the **APPLICATION_ID** in the address bar when navigating to your app.

    We use a five-digit order number as a key (**KEY**) and phone numbers as values. **TTL** here is to specify the values' storage period.

3. Finally, run the file to save the order details:

    `python kvs.py`

    > In case you no longer want a client and a courier to disturb each other, you can [delete](https://voximplant.com/docs/references/httpapi/keyvaluestorage#delkeyvalueitem) the order details from the store. Find all the available key-value store methods in our documentation: [management API](https://voximplant.com/docs/references/httpapi/keyvaluestorage?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs) and [VoxEngine](https://voximplant.com/docs/references/voxengine/applicationstorage?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs).

## Scenario code

The **kvs-scenario** code is below, you can copy it as is. You just need to do one more thing – specify the number you rented in the Voximplant control panel as **callid** in the form of  "10000000000":

{% details Full scenario code %} ```javascript
require(Modules.ApplicationStorage);

/**
 * @param {boolean} repeatAskForInput - whether the input request was repeated
 * @param longInputTimerId - timer for the absence of input
 * @param shortInputTimerId - timer for triggering the phrase about contacting an operator
 * @param {boolean} firstTimeout - indicator of the first timeout
 * @param {boolean} wrongPhone - indicator that the caller's number matches the number from storage
 * @param {boolean} inputRecieved - whether the input was received from the user
 * 
 */

let repeatAskForInput;
let longInputTimerId;
let shortInputTimerId;
let firstTimeout = true;
let wrongPhone;
let inputRecieved;

const store = {
    call: null,
    caller: '',
    callee: '',
    callid: 'phone number rented in the panel',
    operator_call: null,
    operatorNumber: '',
    input: '',
    data: {
        call_operator: '',
        order_number: '',
        order_search: '',
        phone_search: '',
        sub_status: '',
        sub_available: '',
        need_operator: '',
        call_record: ''
    }
}

const phrases = {
    start: 'Hello. Please -- enter the five-digit order number in tone mode.',
    repeat: 'Please -- enter the five-digit order number in tone mode, or press pound to contact an operator.',
    noInputGoodbye: 'You have not chosen anything. You can look up the order number in the text message and call us again. Goodbye, have a nice day!',
    connectToOpearator: 'To contact an operator, press pound.',
    connectingToOpearator: 'Stay on the line, putting you through to an operator.',
    operatorUnavailable: 'Unfortunately, all operators are busy.. Please,,, call back later. Goodbye, have a nice day!',
    wrongOrder: 'Order number is not found. Look up the order number in the text message and enter it in tone mode. Or contact an operator by pressing pound.',
    wrongOrderGoodbye: 'You didn’t choose anything.. Goodbye, have a nice day!',
    wrongPhone: 'Phone number is not found. If you are a customer, please call back from the number you used to place your order. If you are a courier, please call back from the number that is registered in our system. Or contact an operator by pressing pound.',
    wrongPhoneGoodbye: 'You didn’t choose anything.. Goodbye, have a nice day!',
    courierIsCalling: `A courier is calling you about the order delivery, - - ${store.data.order_number}`,
    clientIsCalling: `A customer is calling you about the order delivery, - - ${store.data.order_number}`,
    courierUnavailable: 'It seems like the courier is unavailable at the moment. Please call back in a couple of minutes. Goodbye, have a nice day!',
    clientUnavailable: 'It seems like the customer is unavailable at the moment. Please call back in a couple of minutes. Goodbye, have a nice day!',
    waitForCourier: 'Stay on the line, putting you through to the courier.',
    waitForClient: 'Stay on the line, putting you through to the customer.'
}


VoxEngine.addEventListener(AppEvents.Started, async e => {
    VoxEngine.addEventListener(AppEvents.CallAlerting, callAlertingHandler);
})

async function callAlertingHandler(e) {
    store.call = e.call;
    store.caller = e.callerid;
    store.call.addEventListener(CallEvents.Connected, callConnectedHandler);
    store.call.addEventListener(CallEvents.Disconnected, callDisconnectedHandler);
    store.call.answer();
}

async function callDisconnectedHandler(e) {
    await sendResultToDb();
    VoxEngine.terminate();
}

async function callConnectedHandler() {
    store.call.handleTones(true);
    store.call.addEventListener(CallEvents.RecordStarted, (e) => {
        store.data.call_record = e.url;
    });
    store.call.record();
    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
    await say(phrases.start);
    addInputTimeouts();
}

function dtmfHandler(e) {
    clearInputTimeouts();
    store.input += e.tone;
    Logger.write('Entered digit is ' + e.tone)
    Logger.write('Full number ' + store.input)
    if (e.tone === '#') {
        store.data.need_operator = "Yes";
        store.call.removeEventListener(CallEvents.ToneReceived);
        store.call.handleTones(false);
        callOperator();
        return;
    }

    if (!wrongPhone) {
        if (store.input.length >= 5) {
            repeatAskForInput = true;
            Logger.write(`Received number is ${store.input}. `);
            store.call.handleTones(false);
            store.call.removeEventListener(CallEvents.ToneReceived);
            handleInput(store.input);
            return;
        }
    }
    addInputTimeouts();
}

function addInputTimeouts() {
    clearInputTimeouts();
    if (firstTimeout) {
        Logger.write('Timer for the phrase about contacting an operator is triggered');
        shortInputTimerId = setTimeout(async () => {
            await say(phrases.connectToOpearator);
        }, 1500);
        firstTimeout = false;
    }

    longInputTimerId = setTimeout(async () => {
        Logger.write('Timer for no input from the user is triggered ' + longInputTimerId);
        store.call.removeEventListener(CallEvents.ToneReceived);
        store.call.handleTones(false);
        if (store.input) {
            handleInput(store.input);
            return;
        }
        if (!repeatAskForInput) {
            Logger.write('Asking the caller to re-enter the number');
            store.call.handleTones(true);
            store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
            await say(phrases.repeat);
            addInputTimeouts();
            repeatAskForInput = true;
        } else {
            Logger.write('Number is not entered. Ending the call');
            await say(inputRecieved ? phrases.wrongOrderGoodbye : phrases.noInputGoodbye);
            store.call.hangup();
        }

    }, 8000);
    Logger.write('Timer for no input from the user is triggered ' + longInputTimerId);
}

function clearInputTimeouts() {
    Logger.write(`Clearing the timer ${longInputTimerId}. `);
    if (longInputTimerId) clearTimeout(longInputTimerId);
    if (shortInputTimerId) clearTimeout(shortInputTimerId);
}

async function handleInput() {
    store.data.order_number = store.input;
    Logger.write('Looking for a match in the key-value store by the entered number: ' + store.input)
    inputRecieved = true;
    let kvsAnswer = await ApplicationStorage.get(store.input);
    if (kvsAnswer) {
        store.data.order_search = 'Order is found';
        Logger.write('Received response from kvs: ' + kvsAnswer.value)
        let { courier, client } = JSON.parse(kvsAnswer.value);

        if (store.caller == courier) {
            Logger.write('Courier is calling')
            store.callee = client;
            store.data.sub_status = 'Courier';
            store.data.phone_search = 'Phone number is found';
            callCourierOrClient();
        } else if (store.caller == client) {
            Logger.write('Customer is calling')
            store.callee = courier;
            store.data.sub_status = 'Customer';
            store.data.phone_search = 'Phone number is found';
            callCourierOrClient();
        } else {
            Logger.write('Number of the caller does not match the numbers received from kvs');
            wrongPhone = true;
            store.data.phone_search = 'Phone number is not found';
            store.input = '';
            store.call.handleTones(true);
            store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
            await say(phrases.wrongPhone);
            addInputTimeouts();
        }

    } else {
        Logger.write('No match in kvs for the entered number');
        store.data.order_search = 'Order is not found';
        store.input = '';
        store.call.handleTones(true);
        store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
        await say(phrases.wrongOrder);
        Logger.write(`Clearing the timer ${longInputTimerId}. `);
        addInputTimeouts();

    }

}

async function callCourierOrClient() {
    clearInputTimeouts();
    Logger.write('Starting a call to the courier/customer');
    await say(store.data.sub_status === 'Courier' ? phrases.waitForClient : phrases.waitForCourier, store.call);
    const secondCall = VoxEngine.callPSTN(store.callee, store.callid);
    store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');
    secondCall.addEventListener(CallEvents.Connected, async () => {
        store.data.sub_available = 'Yes';
        await say(store.data.sub_status === 'Courier' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);
        store.call.stopPlayback();
        VoxEngine.sendMediaBetween(store.call, secondCall);
    });
    secondCall.addEventListener(CallEvents.Disconnected, () => {
        store.call.hangup();
    });
    secondCall.addEventListener(CallEvents.Failed, async () => {
        store.data.sub_available = 'No';
        store.call.stopPlayback();
        await say(store.data.sub_status === 'Courier' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);
        store.call.hangup();
    });
}

async function callOperator() {
    Logger.write('Starting a call to an operator');
    await say(phrases.connectingToOpearator, store.call);
    store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');
    store.operator_call = VoxEngine.callPSTN(store.operatorNumber, store.callid);
    store.operator_call.addEventListener(CallEvents.Connected, async () => {
        store.data.call_operator = 'Operator is free';
        VoxEngine.sendMediaBetween(store.call, store.operator_call);
    });
    store.operator_call.addEventListener(CallEvents.Disconnected, () => {
        store.call.hangup();
    });
    store.operator_call.addEventListener(CallEvents.Failed, async () => {
        store.data.call_operator = 'Operator is busy';
        await say(phrases.operatorUnavailable, store.call);
        store.call.hangup();
    });
}


async function sendResultToDb() {
    Logger.write('Data to be sent to the database');
    Logger.write(JSON.stringify(store.data));
    const options = new Net.HttpRequestOptions();
    options.headers = ['Content-Type: application/json'];
    options.method = 'POST';
    options.postData = JSON.stringify(store.data);
    await Net.httpRequestAsync('https://voximplant.com/', options);
}


function say(text, call = store.call, lang = VoiceList.Amazon.
en_AU_Nicole) {
    return new Promise((resolve) => {
        call.say(text, lang);
        call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {
            resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));
        });
    });
};
```
{% enddetails %}

The code is thoroughly commented on but let's go into more detail on some points.

### Enter the order number

The first thing we do when a call arrives is ask a caller to enter the order number and handle it using the `dtmfHandler` function.
```javascript
store.input += e.tone;
```

If the caller enters #, put them through to the operator:
```javascript
if (e.tone === '#') {
    store.data.need_operator = "Yes";
    store.call.removeEventListener(CallEvents.ToneReceived);
    store.call.handleTones(false);
    callOperator();
    return;
}
```

If they enter a 5-digit number, call the `handleInput` method:
```javascript
if (store.input.length >= 5) {
    repeatAskForInput = true;
    Logger.write('Received number is ${store.input}. ');
    store.call.handleTones(false);
    store.call.removeEventListener(CallEvents.ToneReceived);
    handleInput(store.input);
    return;
}
```

### Search the order
It’s time to compare the entered number with order numbers in the store using the [ApplicationStorage.get()](https://voximplant.com/docs/references/voxengine/applicationstorage#get) method and the entered number as a key here:
```javascript
store.data.order_number = store.input;
Logger.write('Looking for a match in the key-value store by the entered number: ' + store.input)
inputRecieved = true;
let kvsAnswer = await ApplicationStorage.get(store.input);
```

If the order is found, get the courier and customer phone numbers connected with it:
```javascript
if (kvsAnswer) {
    store.data.order_search = 'Order is found';
    Logger.write('Received response from kvs: ' + kvsAnswer.value)
    let { courier, client } = JSON.parse(kvsAnswer.value);
```

Now we need to figure out to whom to call. If the caller’s number is the courier’s, forward a call to the customer, if it’s the customer’s – to the courier. The `callCourierOrClient` function is intended for this:
```javascript
if (store.caller == courier) {
    Logger.write('Courier is calling')
    store.callee = client;
    store.data.sub_status = 'Courier';
    store.data.phone_search = 'Phone number is found';
    callCourierOrClient();
} else if (store.caller == client) {
    Logger.write('Customer is calling')
    store.callee = courier;
    store.data.sub_status = 'Customer';
    store.data.phone_search = 'Phone number is found';
    callCourierOrClient();
}
``` 

If the number is not in the store, ask a caller to call again from the number they used when placing the order:
```javascript
else {
    Logger.write('Number of the caller does not match the numbers received from kvs');
    wrongPhone = true;
    store.data.phone_search = 'Phone number is not found';
    store.input = '';
    store.call.handleTones(true);
    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
    await say(phrases.wrongPhone);
    addInputTimeouts();
}
```

Finally, handle what happens when the order number is not in the store. In such a case, ask the caller to make sure the number is correct and enter it again:
```javascript
else {
    Logger.write('No match in kvs for the entered number');
    store.data.order_search = 'Order is not found';
    store.input = '';
    store.call.handleTones(true);
    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
    await say(phrases.wrongOrder);
    Logger.write(`Clearing the timer ${longInputTimerId}. `);
    addInputTimeouts();
}
```

### Call the customer/courier
Let's go directly to the call, i.e. to the `callCourierOrClient` function. Here we tell the caller that we're transferring their call to the courier/client and play music on hold. We use the [callPSTN](https://voximplant.com/docs/references/voxengine/voxengine/callpstn?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs) method to call the client or the courier (depending on whose number was previously identified as the caller's number):
```javascript
await say(store.data.sub_status === 'Courier' ? phrases.waitForClient : phrases.waitForCourier, store.call);
const secondCall = VoxEngine.callPSTN(store.callee, store.callid);
store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');
```

At the same time, we tell the callee that the call is about clarification of information on the order:
```javascript
secondCall.addEventListener(CallEvents.Connected, async () => {
    store.data.sub_available = 'Yes';
    await say(store.data.sub_status === 'Courier' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);
    store.call.stopPlayback();
    VoxEngine.sendMediaBetween(store.call, secondCall);
});
```

Then, handle the disconnect event:
```javascript
secondCall.addEventListener(CallEvents.Disconnected, () => {
    store.call.hangup();
});
```

Notify the caller if the callee is unavailable:
```javascript
secondCall.addEventListener(CallEvents.Failed, async () => {
    store.data.sub_available = 'No';
    store.call.stopPlayback();
    await say(store.data.sub_status === 'Courier' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);
    store.call.hangup();
});
```

The `say` method is responsible for all the phrases the robot utters. The phrases themselves are in the **phrases** associative array. We use [Amazon as a TTS provider](https://voximplant.com/docs/references/voxengine/voicelist/amazon?utm_source=devto&utm_medium=techarticle&utm_campaign=vox_kvs), the voice of Nicole:
```javascript
function say(text, call = store.call, lang = VoiceList.Amazon.
en_AU_Nicole) {
    return new Promise((resolve) => {
        call.say(text, lang);
        call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {
            resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));
        });
    });
};
```

Among other things, our scenario records the calls using the [record](https://voximplant.com/docs/references/voxengine/call#record) method and enables you to save statistics to the database. In our code, the `sendResultToDb` function handles this. This is very important for business because it allows you to analyze statistics, provide quality control and quickly resolve any issues that might arise during the delivery process.

# Test the app
When you add the full code to the scenario and the order details to the storage, feel free to start testing.

Let's call from the customer's or courier's phone number to the number rented in the panel. Then enter the order number (in our case, it is 12345) and wait for the connection with the other party.

If we do everything correctly, the customer and the courier will be able to call each other and discuss the details of the order without knowing each other's personal numbers, and thus without any privacy issues.
 
I’m glad you read all the way to the end of this article. It means it was gripping, right? :) Anyway, I wish you successful development and trouble-free order delivery! Stay tuned for more helpful articles in the future.