Access bus booking services using Django & AfricasTalking API

I am very certain that each one of us has, in the past took a long journey and you had to find a vehicle that goes express without dropping you midway. Chances are you went to their booking office, acquired a ticket and then waited for your bus to arrive ,board it then take off. Notice that you had to manually go to the station, queue then get your flight ticket.

This is all fine given the circumstances but what if I told you that we could digitize this process of acquiring bus ticket such that you can book a bus while seated in your house and only have to go and start the journey when the time for your bus has come? We are going to see how.

In this post we are going use Django to create a USSD bus booking system where:

  1. Users can query all buses in the system
  2. Users can book a bus
  3. Users can check the status of their flight
  4. Users can cancel their flight
  5. Users can report complains

The USSD service is made possible thanks to Africa'sTalking. We will use their sandbox environment to simulate a real life scenario of our application. Without further talk let's start!

PROJECT SETUP

Let's quicky create a new project:

mkdir django_ussd && cd django_ussd

virtualenv env

source env/bin/activate

pip install django

django-admin startproject mysite .

python manage.py startapp core

Next add core to the list of installed apps in settings.py.
Now let's create the following models to hold our data:

from django.db import models
# Create your models here.
class Bus(models.Model):
    number_plate=models.CharField(
        max_length=10
    )
    start=models.CharField(
        max_length=250
    )
    finish=models.CharField(
        max_length=250
    )
    price = models.FloatField()
    seats=models.IntegerField()
    is_available=models.BooleanField(
        default=True
    )
    def __str__(self) -> str:
        return self.number_plate

class Booking(models.Model):
    STATUS_CHOICES = [ 
    ("Awaiting departure", "Awaiting departure"),
    ("Journey complete", "Journey complete"),
    ("Cancelled", "Cancelled"),
    ]
    bus = models.ForeignKey(Bus, on_delete=models.CASCADE)
    customer = models.CharField(max_length = 150)
    seat=models.IntegerField(default=0)
    date = models.DateTimeField(auto_now_add=True)
    departure=models.DateTimeField(null=True, blank=True)
    status = models.CharField(
        max_length = 20,
        choices = STATUS_CHOICES,
        default = 'Awaiting departure'
        )
    def __str__(self) -> str:
        return self.customer

from django.contrib import admin
admin.site.register(Bus)
admin.site.register(Booking)

So our Bus class will have all details related to a given bus while Booking will store user tickets. Go ahead and makemigrations then migrate.
Create an admin user then go to the admin section and add a few buses that we will use for testing.
Next let's write our application logic. Open views.py and add the code below to it. If need be, by all means take a minute to go through it and understand. It is pretty straight forward.

import random
from datetime import datetime, timedelta
from core.models import Booking, Bus
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse

# Create your views here.
@csrf_exempt
def index(request):
    if request.method == 'POST':
        session_id = request.POST.get('sessionId')
        service_code = request.POST.get('serviceCode')
        phone_number = request.POST.get('phoneNumber')
        text = request.POST.get('text')

        response = ""

        if text == "":
            response = "CON Welcome! \n Which service would you like to access? \n"  
            response += "1. List all our buses  \n"
            response += "2. Check ticket status \n"
            response += "3. Book a bus seat \n"
            response += "4. Cancel a booking \n"
            response += "5. Report an issue"

         #User needs a list of all buses   
        elif text == "1":
            results=Bus.objects.all()
            for i in results:
                response += f"END {i}:{i.start}-{i.finish} @KSHS{i.price} \n \n"

        elif text == "2":
            response = "CON Choose an option \n"
            response += "1. All tickets \n"
            response += "2. Today active tickets"

         #Follow up
        elif text == '2*1':
            tickets=Booking.objects.filter(
                customer=phone_number
            )
            for tkt in tickets:
                response += f"END Ticket {tkt.id} on {tkt.departure:%Y-%m-%d %H:%M}"

         #Follow up
        elif text == '2*2':
            now = datetime.now()
            tickets=Booking.objects.filter(
                customer=phone_number,
                departure__date=now
            )
            if tickets:
                for tkt in tickets:
                    response += f"END Ticket {tkt.id} on {tkt.departure:%Y-%m-%d %H:%M}"
            response ='END No tickets found'

        #User wants to book a seat
        elif text == "3":
            response = "CON Okay, pick a route \n"
            response += "1. Kericho-Nairobi \n"
            response += "2. Kisumu-Eldoret \n"
            response += "3. Nakuru-Mombasa \n"
            response += "4. Narok-Naivasha "

        #Follow up
        elif text == '3*1' or '3*2' or '3*3' or '3*4':
            seat=random.randint(1,30)
            buses=Bus.objects.filter(is_available=True)
            buses=[bus for bus in buses]
            bus=random.choices(buses)
            for i in bus:
                bus=i
            departure=datetime.now() + timedelta(hours=1)
            new_booking=Booking.objects.create(
                bus=bus,
                customer=phone_number,
                seat=seat,
                departure=departure

            )
            response = f"END  Alright! Here is your booking info: \n TICKET NO {new_booking.id} \n Bus Number is {bus} \n Your seat number is {seat} \n Your bus leaves at {departure:%H:%M:%S}"  
        elif text == "4":
            response = "END Feature work in progress, check again soon"
        elif text == "5":
            response = "END Feature work in progress, check again soon"

        return HttpResponse(response)

With that figured out we can move on to create the route for it. Update the project urls.py like so:

from django.contrib import admin
from django.urls import path
from core import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index),
]

And that should be all we need. Now the next part is to expose our application to the internet because AfricasTalking cannot find our localhost. A great tool for such is Ngrok . Follow its instructions to get installed on your machine if you haven't already.
To tunnel our app:

ngrok http 8000

Result:
ngrok
Copy that https link we'll need it next up.

AFRICASTALKING SETUP

Log in to your AfricasTalking account then head over to the simulator section click on USSD part and click to create a new session. Now add the ngrok link that you created before to act as our callback. In case you need to know what a callback is, then here is a post. Now the form should be like shown below:
session
Save the form:
form
So for me, the app will be invoked by dialing *384*1827# yours may be a bit different but should work.

SIMULATING

We now have an application baked and we have configured our sandbox environment. Go ahead and launch the simulator then pick 'USSD' when the phone shows up. Enter your code, for me it is *384*1827#. The behavior should be:

WRAP UP

If you are still here then hats off to you. Congratulations. In this tutorial we were solving a transport problem. We built a solution from scratch to help passengers book buses and access a range of related services offline via USSD. Thus whether on featured phones(Kabambe) or smartphone, they can have at it with no problem.
That is all I had for you guys. Let me know if you encounter an issue or have a suggestion in the comments below. Also if possible show some love to this post.
As usual you can follow me here or on Twitter for more awesome content.
Till next time, cheers!

22