Skip to content

Instantly share code, notes, and snippets.

@JaniKibichi
Created June 19, 2021 18:05
Show Gist options
  • Save JaniKibichi/d0e4050809af2f8b0509e26f6b4734b0 to your computer and use it in GitHub Desktop.
Save JaniKibichi/d0e4050809af2f8b0509e26f6b4734b0 to your computer and use it in GitHub Desktop.
package com.janikibichi.models.africastalking.ussd
import akka.actor._
import com.janikibichi.models.africastalking.ussd.USSDFSMProtocol._
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt
object USSDFSMProtocol {
def props(sessionId: String): Props = Props(new USSDFSMActor(sessionId: String))
final case class USSDRequest(sessionId: String, phoneNumber: String, networkCode: String, serviceCode: String, text: String)
final case class USSDMenu(title: String, body: String, options: String)
//FSM STATE
sealed trait USSDState
final case object CurrentSession extends USSDState
//FSM DATA
sealed trait USSDData
final case object NoSession extends USSDData
}
class USSDFSMActor(sessionId: String) extends FSM[USSDState, USSDData] {
startWith(CurrentSession, NoSession)
when(stateName = CurrentSession, stateTimeout = 360.seconds) {
case Event(ussdRequest: USSDRequest, NoSession) =>
// 1. CHECK USER ACCOUNT
var acc: AccountData = ???
// FETCH USER PROFILE FROM DATABASE
val userprofile:List[Any] = Await.result(fetchUserProfile(userByCountry.drop(1)), 30.seconds)
userprofile match {
case Nil =>
// CALL BRAND FOR USER DATA
val acc: AccountData = ???
// UPDATE UNREGISTERED USER
updateUnregisteredUser(phoneNumber = ussdRequest.phoneNumber.drop(1))
case ::(head, tl) =>
log.info(s"User found as $head")
}
// 2. CHECK TEXT, ONGOING SESSION
val request = InputUtils.ussd.cleanUpResponse(ussdRequest.text)
request.length match {
case 0 => // THIS IS A NEW SESSION
if(acc.isEmpty){
// SEND MENU
val menuContent = Await.result(fetchMenuContent(state = "NoAccount", language = UtilityUSSDConfig.language.default(ussdRequest.phoneNumber)), 30.seconds)
val menuOption = Await.result(fetchMenuOptions(state = "NoAccount", language = UtilityUSSDConfig.language.default(ussdRequest.phoneNumber)), 30.seconds)
menuContent match {
case Nil =>
MenuSetup.startUpMenus()
case ::(menuContHd, menuContTl) =>
menuOption match {
case Nil =>
case ::(menuOptHd, menuOptTl) =>
// RETURN RESPONSE
val responseMenu = USSDMenu(
title = menuContHd.title,
body = menuContHd.body,
options = menuOptHd.option1 + menuOptHd.option2 + menuOptHd.option3 + menuOptHd.option4 + menuOptHd.option5 + menuOptHd.option6
)
sender() ! responseMenu
}
}
}else{
// USER HAS AN A/C, UPDATE THE USER PROFILE
updateUserProfile(userProfile =
UserProfile(
fullName = acc.objects.head.customer.full_name,
idNumber = acc.objects.head.customer.identity_document_number,
idType = acc.objects.head.customer.identity_document_type,
accOne = acc.objects.head.account_id,
accTwo = "",
accThree = "",
phoneOne = userByCountry.drop(1),
phoneTwo = acc.objects.head.customer.phones.get.head.e164_format.drop(1),
language = acc.objects.head.customer.language.toUpperCase
)
)
// UPDATE ONGOING SESSION, NEW SESSIONS START AT 0
updateOngoingSession(
OngoingSession(
"ChooseAccount",
sessionId = sessionId,
language = acc.objects.head.customer.language.toUpperCase,
fullName = acc.objects.head.customer.full_name,
idDocument = acc.objects.head.customer.identity_document_number,
chosenAc = "",
amount = "",
chosenPhone = userByCountry.drop(1),
day1Amt = "",
day7Amt = "",
day30Amt = "",
day60Amt = "",
currency = "",
day1Code = "",
day7Code = "",
day30Code = "",
day60Code = "",
optionCode = ""
)
)
// SEND MENU
val menuContent = Await.result(fetchMenuContent(state = "ChooseAccount", language = acc.objects.head.customer.language.toUpperCase), 30.seconds)
val menuOption = Await.result(fetchMenuOptions(state = "ChooseAccount", language = acc.objects.head.customer.language.toUpperCase), 30.seconds)
menuContent match {
case Nil =>
MenuSetup.startUpMenus()
case ::(menuContHd, menuContTl) =>
menuOption match {
case Nil =>
case ::(menuOptHd, menuOptTl) =>
// RETURN RESPONSE
val responseMenu = USSDMenu(
title = menuContHd.title + acc.objects.head.customer.full_name + ".",
body = menuContHd.body,
options = "\n1. " + acc.objects.head.account_id + menuOptHd.option2 + menuOptHd.option3 + menuOptHd.option4 + menuOptHd.option5 + menuOptHd.option6
)
sender() ! responseMenu
}
}
}
case _ => // THIS IS AN ONGOING SESSION
// RETRIEVE ONGOING SESSION
val oldSession:List[Any] = Await.result(fetchOngoingSession(sessionId), 30.seconds)
oldSession match {
case Nil => // NO SESSION, THERES AN ISSUE, LOG ERROR
case ::(sessionHd, sessionTl) =>
// RETRIEVE MENU ANSWER FROM DB. WE CAN TELL, FROM STATE & LANGUAGE, HOW TO HANDLE RESPONSE
val menuAnswer:List[Any] = Await.result(fetchMenuAnswers(state = sessionHd.state), 30.seconds)
menuAnswer match {
case Nil => // THE MENU HAS NOT BEEN UPDATED, STORE DEFAULT MENU
MenuSetup.startUpMenus()
case ::(ansHd, ansTl) =>
// GET RESPONSE FROM USER, REMEMBER WE MUST HANDLE OPTION 1 THROUGH 6, FROM MENU OPTIONS TYPE
// THERE ARE TWO TYPES OF RESPONSES, CHOSEN OPTION 1-6 OR USER INPUT LIKE NATIONAL ID
log.info(s"Old Session Received is $sessionHd and answer is ${ansHd.answer.getOrDefault(request, "1")} and language as ${sessionHd.language}")
var menuChosen = ""
if (request.equals("0") | request.equals("1") | request.equals("2") | request.equals("3") | request.equals("4") | request.equals("5")) {
menuChosen = request
} else {
menuChosen = "UserInput"
}
// FETCH THE RETURNING USER PROFILE
val userprofile:List[Any] = Await.result(fetchUserProfile(userByCountry.drop(1)), 30.seconds)
userprofile match {
case Nil => // NO USER, THERES AN ISSUE, LOG ERROR
case ::(userHd, userTl) =>
// GET THE CURRENT STATE, AND THE USER'S RESPONSE TO MENU JUST SERVED IN CURRENT STATE
sessionHd.state match {
case _ =>
// SESSION DATA KEY ENABLES US TO EMBED TRANSIENT DATA IN A USSD MENU FOR INSTANCE THE NAME OF THE USER IN A WELCOME GREETING - VERY IMPORTANT
val args = Map("1" -> "", "2" -> "", "3" -> "", "4" -> "", "5" -> "", "0" -> "", sessionHd.dataKey -> menuChosen)
generateResponse(state = ansHd.answer.get(menuChosen), menuChosen = menuChosen, menu = ansHd, ongoingSession = sessionHd, options = args)
}
}
}
}
}
stay()
}
initialize()
whenUnhandled {
case Event(event, data) =>
stay()
}
onTermination {
case _ =>
removeOngoingSession(sessionId)
}
// RETURN RESPONSE
def generateResponse(state: String, menuChosen: String, menu: MenuAnswer, ongoingSession: OngoingSession, options: Map[String, String]): Unit = {
log.info(s"Received Data in $state State with Menu Chosen as : $menuChosen")
// UPDATE ONGOING SESSION
updateOngoingSession(ongoingSession = ongoingSession.copy(state = menu.answer.get(menuChosen)))
// MENU CONTENT
assembleMenu(state = menu.answer.get(menuChosen), language = ongoingSession.language, args = options)
}
// USSD MENU
def assembleMenu(currentState: String, currentLanguage: String, args: Map[String, String]): Unit = {
// MENU CONTENT
val menuContent = Await.result(fetchMenuContent(state = currentState, language = currentLanguage), 30.seconds)
val menuOption = Await.result(fetchMenuOptions(state = currentState, language = currentLanguage), 30.seconds)
menuContent match {
case Nil =>
MenuSetup.startUpMenus()
case ::(menuContHd, menuContTl) =>
menuOption match {
case Nil =>
case ::(menuOptHd, menuOptTl) =>
// RETURN RESPONSE
val responseMenu = USSDMenu(
title = menuContHd.title,
body = menuContHd.body,
options = menuOptHd.option1 +
menuOptHd.option2 +
menuOptHd.option3 +
menuOptHd.option4 +
menuOptHd.option5 +
menuOptHd.option6
)
sender() ! responseMenu
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment