Created
June 19, 2021 18:05
-
-
Save JaniKibichi/d0e4050809af2f8b0509e26f6b4734b0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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