diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking.sln b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking.sln
new file mode 100644
index 00000000..ecebceb6
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33516.290
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CallAutomation_AppointmentBooking", "CallAutomation_AppointmentBooking\CallAutomation_AppointmentBooking.csproj", "{E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E0A69614-B3D9-446A-B9BC-D7D21B4A2DF8}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {31E6D6F2-4041-4119-AFF5-F575C0D8C7F6}
+ EndGlobalSection
+EndGlobal
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/AppointmentBookingConfig.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/AppointmentBookingConfig.cs
new file mode 100644
index 00000000..1d7e1954
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/AppointmentBookingConfig.cs
@@ -0,0 +1,43 @@
+namespace CallAutomation_AppointmentBooking
+{
+ public class AppointmentBookingConfig
+ {
+ ///
+ /// public Callback URI that will be used to answer incoming call event,
+ /// or handle mid-call events, such as CallConnected.
+ /// See README file for details on how to setup tunnel on your localhost to handle this.
+ ///
+ public Uri CallbackUri { get; init; }
+
+ ///
+ /// DirectOffered phonenumber is can be aquired from Azure Communication Service portal.
+ /// In order to answer Incoming PSTN call or make an outbound call to PSTN number,
+ /// Call Automation needs Directly offered PSTN number to do these actions.
+ ///
+ public string DirectOfferedPhonenumber { get; init; }
+
+ ///
+ /// List of all prompts from this sample's business logic.
+ /// These recorded prompts must be uploaded to publicily available Uri endpoint.
+ /// See README for pre-generated samples that can be used for demo.
+ ///
+ public Prompts AllPrompts { get; init; }
+
+ public class Prompts
+ {
+ public Uri MainMenu { get; init; }
+
+ public Uri Retry { get; init; }
+
+ public Uri Choice1 { get; init; }
+
+ public Uri Choice2 { get; init; }
+
+ public Uri Choice3 { get; init; }
+
+ public Uri PlayRecordingStarted { get; init; }
+
+ public Uri Goodbye { get; init; }
+ }
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking.csproj b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking.csproj
new file mode 100644
index 00000000..e8a912e5
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net7.0
+ enable
+ enable
+ d7d2fc43-754d-4dba-87ed-832e765c7a4d
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/CallingModules.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/CallingModules.cs
new file mode 100644
index 00000000..34a5910d
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/CallingModules.cs
@@ -0,0 +1,88 @@
+using Azure.Communication;
+using Azure.Communication.CallAutomation;
+using CallAutomation_AppointmentBooking.Interfaces;
+
+namespace CallAutomation_AppointmentBooking
+{
+ ///
+ /// Reusuable common calling actions for business needs
+ ///
+ public class CallingModules : ICallingModules
+ {
+ private readonly CallConnection _callConnection;
+ private readonly AppointmentBookingConfig _appointmentBookingConfig;
+
+ public CallingModules(
+ CallConnection callConnection,
+ AppointmentBookingConfig appointmentBookingConfig)
+ {
+ _callConnection = callConnection;
+ _appointmentBookingConfig = appointmentBookingConfig;
+ }
+
+ public async Task RecognizeTonesAsync(
+ CommunicationIdentifier targetToRecognize,
+ int minDigitToCollect,
+ int maxDigitToCollect,
+ Uri askPrompt,
+ Uri retryPrompt)
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ // prepare recognize tones
+ CallMediaRecognizeDtmfOptions callMediaRecognizeDtmfOptions = new CallMediaRecognizeDtmfOptions(targetToRecognize, maxDigitToCollect);
+ callMediaRecognizeDtmfOptions.Prompt = new FileSource(askPrompt);
+ callMediaRecognizeDtmfOptions.InterruptPrompt = true;
+ callMediaRecognizeDtmfOptions.InitialSilenceTimeout = TimeSpan.FromSeconds(10);
+ callMediaRecognizeDtmfOptions.InterToneTimeout = TimeSpan.FromSeconds(10);
+ callMediaRecognizeDtmfOptions.StopTones = new List { DtmfTone.Pound, DtmfTone.Asterisk };
+
+ // Send request to recognize tones
+ StartRecognizingCallMediaResult startRecognizingResult = await _callConnection.GetCallMedia().StartRecognizingAsync(callMediaRecognizeDtmfOptions);
+
+ // Wait for recognize related event...
+ StartRecognizingEventResult recognizeEventResult = await startRecognizingResult.WaitForEventProcessorAsync();
+
+ if (recognizeEventResult.IsSuccess)
+ {
+ // success recognition - return the tones detected.
+ RecognizeCompleted recognizeCompleted = recognizeEventResult.SuccessResult;
+ string dtmfTones = ((DtmfResult)recognizeCompleted.RecognizeResult).ConvertToString();
+
+ // check if it collected the minimum digit it collected
+ if (dtmfTones.Length >= minDigitToCollect)
+ {
+ return dtmfTones;
+ }
+ }
+ else
+ {
+ // failed recognition - likely timeout
+ _ = recognizeEventResult.FailureResult;
+ }
+
+ // play retry prompt and retry again
+ await PlayMessageThenWaitUntilItEndsAsync(retryPrompt);
+ }
+
+ throw new Exception("Retried 3 times, Failed to get tones.");
+ }
+
+
+ public async Task PlayMessageThenWaitUntilItEndsAsync(Uri playPrompt)
+ {
+ // Play failure prompt and retry.
+ FileSource fileSource = new FileSource(playPrompt);
+ PlayResult playResult = await _callConnection.GetCallMedia().PlayToAllAsync(fileSource);
+
+ // ... wait for play to complete, then return
+ await playResult.WaitForEventProcessorAsync();
+ }
+
+ public async Task TerminateCallAsync()
+ {
+ // Terminate the call
+ await _callConnection.HangUpAsync(true);
+ }
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Controllers/EventController.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Controllers/EventController.cs
new file mode 100644
index 00000000..982b42d7
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Controllers/EventController.cs
@@ -0,0 +1,41 @@
+using Azure.Communication.CallAutomation;
+using Azure.Messaging;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CallAutomation_AppointmentBooking.Controllers
+{
+ ///
+ /// This is controller where it will recieve interim events from Call automation service.
+ /// We are utilizing event processor, this will handle events and relay to our business logic.
+ ///
+ [Route("api/[controller]")]
+ [ApiController]
+ public class EventController : ControllerBase
+ {
+ private readonly ILogger _logger;
+ private readonly CallAutomationEventProcessor _eventProcessor;
+
+ public EventController(
+ ILogger logger,
+ CallAutomationClient callAutomationClient)
+ {
+ _logger = logger;
+ _eventProcessor = callAutomationClient.GetEventProcessor();
+ }
+
+ [HttpPost]
+ public IActionResult CallbackEvent([FromBody] CloudEvent[] cloudEvents)
+ {
+ // Prase incoming event into solid base class of CallAutomationEvent.
+ // This is useful when we want to access the properties of the event easily, such as CallConnectionId.
+ // We are using this parsed event to log CallconnectionId of the event here.
+ CallAutomationEventBase? parsedBaseEvent = CallAutomationEventParser.ParseMany(cloudEvents).FirstOrDefault();
+ _logger.LogInformation($"Event Recieved. CallConnectionId[{parsedBaseEvent?.CallConnectionId}], Type Name[{parsedBaseEvent?.GetType().Name}]");
+
+ // Utilizing evnetProcessor here to easily handle mid-call call automation events.
+ // process event into processor, so events could be handled in CallingModule.
+ _eventProcessor.ProcessEvents(cloudEvents);
+ return Ok();
+ }
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Controllers/IncomingCallController.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Controllers/IncomingCallController.cs
new file mode 100644
index 00000000..dd7e2e9a
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Controllers/IncomingCallController.cs
@@ -0,0 +1,106 @@
+using Azure.Communication;
+using Azure.Communication.CallAutomation;
+using Azure.Messaging.EventGrid;
+using Azure.Messaging.EventGrid.SystemEvents;
+using CallAutomation_AppointmentBooking.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CallAutomation_AppointmentBooking.Controllers
+{
+ ///
+ /// This is the controller for recieving an inbound call.
+ /// See README files how to setup incoming call and its incoming call event
+ ///
+ [Route("api/[controller]")]
+ [ApiController]
+ public class IncomingCallController : ControllerBase
+ {
+ private readonly ILogger _logger;
+ private readonly CallAutomationClient _callAutomationClient;
+ private readonly AppointmentBookingConfig _appointmentBookingConfig;
+ private readonly ITopLevelMenuService _topLevelMenuService;
+ private readonly IOngoingEventHandler _ongoingEventHandler;
+
+ public IncomingCallController(
+ ILogger logger,
+ CallAutomationClient callAutomationClient,
+ AppointmentBookingConfig appointmentBookingConfig,
+ ITopLevelMenuService topLevelMenuService,
+ IOngoingEventHandler ongoingEventHandler)
+ {
+ _logger = logger;
+ _callAutomationClient = callAutomationClient;
+ _appointmentBookingConfig = appointmentBookingConfig;
+ _topLevelMenuService = topLevelMenuService;
+ _ongoingEventHandler = ongoingEventHandler;
+ }
+
+ [HttpPost]
+ public async Task IncomingCall([FromBody] object request)
+ {
+ string callConnectionId = string.Empty;
+ try
+ {
+ // Parse incoming call event using eventgrid parser
+ var httpContent = new BinaryData(request.ToString());
+ EventGridEvent cloudEvent = EventGridEvent.ParseMany(httpContent).First();
+
+ if (cloudEvent.EventType == SystemEventNames.EventGridSubscriptionValidation)
+ {
+ // this section is for handling initial handshaking with Event webhook registration
+ var eventData = cloudEvent.Data.ToObjectFromJson();
+ var responseData = new SubscriptionValidationResponse
+ {
+ ValidationResponse = eventData.ValidationCode
+ };
+
+ if (responseData.ValidationResponse != null)
+ {
+ _logger.LogInformation($"Incoming EventGrid event: Handshake Successful.");
+ return Ok(responseData);
+ }
+ }
+ else if (cloudEvent.EventType == SystemEventNames.AcsIncomingCall)
+ {
+ // parse again the data into ACS incomingCall event
+ AcsIncomingCallEventData incomingCallEventData = cloudEvent.Data.ToObjectFromJson();
+
+ // Answer Incoming call with incoming call event data
+ // IncomingCallContext can be used to answer the call
+ AnswerCallResult answerCallResult = await _callAutomationClient.AnswerCallAsync(incomingCallEventData.IncomingCallContext, _appointmentBookingConfig.CallbackUri);
+ callConnectionId = answerCallResult.CallConnectionProperties.CallConnectionId;
+
+ _ = Task.Run(async () =>
+ {
+ // attaching ongoing event handler for specific events
+ // This is useful for handling unexpected events could happen anytime (such as participants leaves the call and cal is disconnected)
+ _ongoingEventHandler.AttachCountParticipantsInTheCall(callConnectionId);
+ _ongoingEventHandler.AttachDisconnectedWrapup(callConnectionId);
+
+ // Wait for call to be connected.
+ // Wait for 40 seconds before throwing timeout error.
+ var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(40));
+ AnswerCallEventResult eventResult = await answerCallResult.WaitForEventProcessorAsync(tokenSource.Token);
+
+ if (eventResult.IsSuccess)
+ {
+ // call connected returned! Call is now established.
+ // invoke top level menu now the call is connected;
+ await _topLevelMenuService.InvokeTopLevelMenu(
+ CommunicationIdentifier.FromRawId(incomingCallEventData.FromCommunicationIdentifier.RawId),
+ answerCallResult.CallConnection,
+ eventResult.SuccessResult.ServerCallId);
+ }
+ });
+ }
+ }
+ catch (Exception e)
+ {
+ // Exception! Failed to answer the call.
+ _logger.LogError($"Exception while answer the call. CallConnectionId[{callConnectionId}], Exception[{e}]");
+ }
+
+ return Ok();
+ }
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/ICallingModules.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/ICallingModules.cs
new file mode 100644
index 00000000..0632fb9c
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/ICallingModules.cs
@@ -0,0 +1,14 @@
+using Azure.Communication.CallAutomation;
+using Azure.Communication;
+
+namespace CallAutomation_AppointmentBooking.Interfaces
+{
+ public interface ICallingModules
+ {
+ Task RecognizeTonesAsync(CommunicationIdentifier targetToRecognize, int minDigitToCollect, int maxDigitToCollect, Uri askPrompt, Uri retryPrompt);
+
+ Task PlayMessageThenWaitUntilItEndsAsync(Uri playPrompt);
+
+ Task TerminateCallAsync();
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/IOngoingEventHandler.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/IOngoingEventHandler.cs
new file mode 100644
index 00000000..18f18153
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/IOngoingEventHandler.cs
@@ -0,0 +1,12 @@
+using Azure.Communication;
+using Azure.Communication.CallAutomation;
+
+namespace CallAutomation_AppointmentBooking.Interfaces
+{
+ public interface IOngoingEventHandler
+ {
+ void AttachCountParticipantsInTheCall(string callConnectionId);
+
+ void AttachDisconnectedWrapup(string callConnectionId);
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/ITopLevelMenuService.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/ITopLevelMenuService.cs
new file mode 100644
index 00000000..5c76eca7
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Interfaces/ITopLevelMenuService.cs
@@ -0,0 +1,10 @@
+using Azure.Communication;
+using Azure.Communication.CallAutomation;
+
+namespace CallAutomation_AppointmentBooking.Interfaces
+{
+ public interface ITopLevelMenuService
+ {
+ Task InvokeTopLevelMenu(CommunicationIdentifier originalTarget, CallConnection callConnection, string serverCallId);
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/OngoingEventHandler.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/OngoingEventHandler.cs
new file mode 100644
index 00000000..dd61ca72
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/OngoingEventHandler.cs
@@ -0,0 +1,46 @@
+using Azure.Communication;
+using Azure.Communication.CallAutomation;
+using CallAutomation_AppointmentBooking.Interfaces;
+
+namespace CallAutomation_AppointmentBooking
+{
+ ///
+ /// This demonstrate how to attach ongoing event handler callback for Call Automation
+ /// In below example, adding callback function that will execute whenever specific event type is recieved for that call
+ /// OngoingEventProcessor could be also very useful for callback design pattern
+ ///
+ public class OngoingEventHandler : IOngoingEventHandler
+ {
+ private readonly ILogger _logger;
+ private readonly CallAutomationEventProcessor _eventProcessor;
+
+ public OngoingEventHandler(
+ ILogger logger,
+ CallAutomationClient callAutomation)
+ {
+ _logger = logger;
+ _eventProcessor = callAutomation.GetEventProcessor();
+ }
+
+ ///
+ /// Update and write whenever participant number is updated.
+ ///
+ public void AttachCountParticipantsInTheCall(string callConnectionId)
+ {
+ _eventProcessor.AttachOngoingEventProcessor(callConnectionId, recievedEvent => {
+ _logger.LogInformation($"Number of participants in this Call: [{callConnectionId}], Number Of Participants[{recievedEvent.Participants.Count}]");
+ });
+ }
+
+ ///
+ /// Whenever the call ends (i.e. Call Automation leaves the call or Call is terminated),
+ /// Notify that Call automation has lost the control of the call because of it.
+ ///
+ public void AttachDisconnectedWrapup(string callConnectionId)
+ {
+ _eventProcessor.AttachOngoingEventProcessor(callConnectionId, recievedEvent => {
+ _logger.LogInformation($"Call is disconnected!: [{callConnectionId}]");
+ });
+ }
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Program.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Program.cs
new file mode 100644
index 00000000..9b0177c8
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Program.cs
@@ -0,0 +1,74 @@
+// This is the start of the program.
+// Define any dependencies or configs here.
+using Azure.Communication.CallAutomation;
+using CallAutomation_AppointmentBooking;
+using CallAutomation_AppointmentBooking.Interfaces;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// CallAutomation client: Add with given Azure Communication Service's connection string
+CallAutomationClient callAutomationClient = new CallAutomationClient(ReadingConfigs(builder, "COMMUNICATION_CONNECTION_STRING"));
+builder.Services.AddSingleton(callAutomationClient);
+
+// This is our main Top Level Menu service, which will include our business logic of IVR.
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+// setting up callback endpoint
+// Note: we are using VS tunnel feature for hosting callback webhook
+// update this if it were to use other 3rd party tunnel program, such as ngrok.
+var callbackUriHost = builder.Configuration["VS_TUNNEL_URL"]?.TrimEnd('/');
+
+if (string.IsNullOrEmpty(callbackUriHost))
+{
+ callbackUriHost = ReadingConfigs(builder, "BASE_URI");
+}
+
+// Get all configs, such as callback url and prompts url
+AppointmentBookingConfig appointmentBookingConfig = new AppointmentBookingConfig
+{
+ CallbackUri = new Uri(callbackUriHost + "/api/event"),
+ DirectOfferedPhonenumber = ReadingConfigs(builder, "DIRECT_OFFERED_PHONE_NUMBER"),
+ AllPrompts = new AppointmentBookingConfig.Prompts
+ {
+ MainMenu = new Uri(ReadingConfigs(builder, "PROMPT_MAIN_MENU")),
+ Retry = new Uri(ReadingConfigs(builder, "PROMPT_RETRY")),
+ Choice1 = new Uri(ReadingConfigs(builder, "PROMPT_CHOICE1")),
+ Choice2 = new Uri(ReadingConfigs(builder, "PROMPT_CHOICE2")),
+ Choice3 = new Uri(ReadingConfigs(builder, "PROMPT_CHOICE3")),
+ PlayRecordingStarted = new Uri(ReadingConfigs(builder, "PROMPT_PLAY_RECORDING_STARTED")),
+ Goodbye = new Uri(ReadingConfigs(builder, "PROMPT_GOODBYE")),
+ }
+};
+builder.Services.AddSingleton(appointmentBookingConfig);
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
+
+static string ReadingConfigs(WebApplicationBuilder builder, string configKey)
+{
+ string? returnedValue = builder.Configuration.GetSection("AppointmentBookingConfigs")[configKey];
+ if (returnedValue == null)
+ {
+ throw new NullReferenceException($"{configKey} is not setup. README has details on how to set these variables.");
+ }
+ return returnedValue;
+}
\ No newline at end of file
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Properties/launchSettings.json b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Properties/launchSettings.json
new file mode 100644
index 00000000..41171e03
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Properties/launchSettings.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "CallAutomation_AppointmentBooking": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7173;http://localhost:5021",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Tools.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Tools.cs
new file mode 100644
index 00000000..12ac0990
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/Tools.cs
@@ -0,0 +1,37 @@
+namespace CallAutomation_AppointmentBooking
+{
+ public static class Tools
+ {
+ public static string FormatPhoneNumbers(string phoneNumber)
+ {
+ // calculate E.164 format phonenumber.
+ // +1 xxx-xxx-xxxx
+ // update this tools as your need.
+ if (phoneNumber == null)
+ {
+ throw new ArgumentNullException(nameof(phoneNumber));
+ }
+
+ // Remove all non-digit characters from the phone number
+ phoneNumber = new string(phoneNumber.Where(char.IsDigit).ToArray());
+
+ if (phoneNumber.Length == 10)
+ {
+ return "+1" + phoneNumber;
+ }
+ else if (phoneNumber.Length == 11 && phoneNumber.StartsWith("1"))
+ {
+ return "+" + phoneNumber;
+ }
+ else if (phoneNumber.Length == 12 && phoneNumber.StartsWith("+1"))
+ {
+ return phoneNumber;
+ }
+ else
+ {
+ throw new ArgumentException("Invalid phone number");
+ }
+ }
+
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/TopLevelMenuService.cs b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/TopLevelMenuService.cs
new file mode 100644
index 00000000..90967862
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/TopLevelMenuService.cs
@@ -0,0 +1,103 @@
+using Azure.Communication;
+using Azure.Communication.CallAutomation;
+using CallAutomation_AppointmentBooking.Controllers;
+using CallAutomation_AppointmentBooking.Interfaces;
+
+namespace CallAutomation_AppointmentBooking
+{
+ ///
+ /// This is our top level menu that will have our greetings menu.
+ ///
+ public class TopLevelMenuService : ITopLevelMenuService
+ {
+ private readonly ILogger _logger;
+ private readonly CallAutomationClient _callAutomation;
+ private readonly AppointmentBookingConfig _appointmentBookingConfig;
+
+ public TopLevelMenuService(
+ ILogger logger,
+ CallAutomationClient callAutomation,
+ AppointmentBookingConfig appointmentBookingConfig)
+ {
+ _logger = logger;
+ _callAutomation = callAutomation;
+ _appointmentBookingConfig = appointmentBookingConfig;
+ }
+
+ public async Task InvokeTopLevelMenu(
+ CommunicationIdentifier originalTarget,
+ CallConnection callConnection,
+ string serverCallId)
+ {
+ _logger.LogInformation($"Invoking top level menu, with CallConnectionId[{callConnection.CallConnectionId}]");
+
+ // prepare calling modules to interact with this established call
+ ICallingModules callingModule = new CallingModules(callConnection, _appointmentBookingConfig);
+
+ try
+ {
+ // ... then Start Recording
+ // this will accept serverCallId and uses main service client
+ _logger.LogInformation($"Start Recording...");
+ CallLocator callLocator = new ServerCallLocator(serverCallId);
+ StartRecordingOptions startRecordingOptions = new StartRecordingOptions(callLocator);
+ _ = await _callAutomation.GetCallRecording().StartAsync(startRecordingOptions);
+
+ // Play message of start of recording
+ await callingModule.PlayMessageThenWaitUntilItEndsAsync(_appointmentBookingConfig.AllPrompts.PlayRecordingStarted);
+
+ while (true)
+ {
+ // Top Level DTMF Menu, ask for which menu to be selected
+ string selectedTone = await callingModule.RecognizeTonesAsync(
+ originalTarget,
+ 1,
+ 1,
+ _appointmentBookingConfig.AllPrompts.MainMenu,
+ _appointmentBookingConfig.AllPrompts.Retry);
+
+ _logger.LogInformation($"Caller selected DTMF Tone[{selectedTone}]");
+
+ switch (selectedTone)
+ {
+ // Option 1: Play Message and terminate the call.
+ case "1":
+ await callingModule.PlayMessageThenWaitUntilItEndsAsync(_appointmentBookingConfig.AllPrompts.Choice1);
+ await callingModule.TerminateCallAsync();
+ return;
+
+ // Option 2: Play Message and terminate the call.
+ case "2":
+ await callingModule.PlayMessageThenWaitUntilItEndsAsync(_appointmentBookingConfig.AllPrompts.Choice2);
+ await callingModule.TerminateCallAsync();
+ return;
+
+ // Option 3: Play Message and terminate the call.
+ case "3":
+ await callingModule.PlayMessageThenWaitUntilItEndsAsync(_appointmentBookingConfig.AllPrompts.Choice3);
+ await callingModule.TerminateCallAsync();
+ return;
+
+ default:
+ // Wrong input!
+ // play message then retry this toplevel menu.
+ _logger.LogInformation($"Wrong Input! selectedTone[{selectedTone}]");
+ await callingModule.PlayMessageThenWaitUntilItEndsAsync(_appointmentBookingConfig.AllPrompts.Retry);
+ break;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogWarning($"Exception during Top Level Menu! [{e}]");
+ }
+
+ // wrong input too many times, exception happened, or user requested termination.
+ // good bye and hangup
+ _logger.LogInformation($"Terminating Call. Due to wrong input too many times, exception happened, or user requested termination.");
+ await callingModule.PlayMessageThenWaitUntilItEndsAsync(_appointmentBookingConfig.AllPrompts.Goodbye);
+ await callingModule.TerminateCallAsync();
+ return;
+ }
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/appsettings.json b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/appsettings.json
new file mode 100644
index 00000000..ded6f111
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/CallAutomation_AppointmentBooking/appsettings.json
@@ -0,0 +1,21 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "AppointmentBookingConfigs": {
+ "COMMUNICATION_CONNECTION_STRING": "",
+ "DIRECT_OFFERED_PHONE_NUMBER": "",
+ "BASE_URI": "%BASE_URI%",
+ "PROMPT_MAIN_MENU": "https://github.com/Azure-Samples/communication-services-dotnet-quickstarts/raw/callautomation/appointmentBooking/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_MAIN_MENU.wav",
+ "PROMPT_RETRY": "https://github.com/Azure-Samples/communication-services-dotnet-quickstarts/raw/callautomation/appointmentBooking/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_RETRY.wav",
+ "PROMPT_CHOICE1": "https://github.com/Azure-Samples/communication-services-dotnet-quickstarts/raw/callautomation/appointmentBooking/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE1.wav",
+ "PROMPT_CHOICE2": "https://github.com/Azure-Samples/communication-services-dotnet-quickstarts/raw/callautomation/appointmentBooking/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE2.wav",
+ "PROMPT_CHOICE3": "https://github.com/Azure-Samples/communication-services-dotnet-quickstarts/raw/callautomation/appointmentBooking/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE3.wav",
+ "PROMPT_PLAY_RECORDING_STARTED": "https://github.com/Azure-Samples/communication-services-dotnet-quickstarts/raw/callautomation/appointmentBooking/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_PLAY_RECORDING_STARTED.wav",
+ "PROMPT_GOODBYE": "https://github.com/Azure-Samples/communication-services-dotnet-quickstarts/raw/callautomation/appointmentBooking/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_GOODBYE.wav"
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/Data/AddAccountForTunnel.png b/CallAutomation_AppointmentBooking/Data/AddAccountForTunnel.png
new file mode 100644
index 00000000..2c58aa1c
Binary files /dev/null and b/CallAutomation_AppointmentBooking/Data/AddAccountForTunnel.png differ
diff --git a/CallAutomation_AppointmentBooking/Data/AppointmentReminderDesign.png b/CallAutomation_AppointmentBooking/Data/AppointmentReminderDesign.png
new file mode 100644
index 00000000..c750ab72
Binary files /dev/null and b/CallAutomation_AppointmentBooking/Data/AppointmentReminderDesign.png differ
diff --git a/CallAutomation_AppointmentBooking/Data/CreateDevTunnel.png b/CallAutomation_AppointmentBooking/Data/CreateDevTunnel.png
new file mode 100644
index 00000000..dfafb29e
Binary files /dev/null and b/CallAutomation_AppointmentBooking/Data/CreateDevTunnel.png differ
diff --git a/CallAutomation_AppointmentBooking/Data/EnableDevTunnel.png b/CallAutomation_AppointmentBooking/Data/EnableDevTunnel.png
new file mode 100644
index 00000000..8fc6fd24
Binary files /dev/null and b/CallAutomation_AppointmentBooking/Data/EnableDevTunnel.png differ
diff --git a/CallAutomation_AppointmentBooking/Data/EventgridSubscription-IncomingCall.png b/CallAutomation_AppointmentBooking/Data/EventgridSubscription-IncomingCall.png
new file mode 100644
index 00000000..7983b61b
Binary files /dev/null and b/CallAutomation_AppointmentBooking/Data/EventgridSubscription-IncomingCall.png differ
diff --git a/CallAutomation_AppointmentBooking/Data/ViewDevTunnel.png b/CallAutomation_AppointmentBooking/Data/ViewDevTunnel.png
new file mode 100644
index 00000000..fc138256
Binary files /dev/null and b/CallAutomation_AppointmentBooking/Data/ViewDevTunnel.png differ
diff --git a/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE1.wav b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE1.wav
new file mode 100644
index 00000000..3f10514c
Binary files /dev/null and b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE1.wav differ
diff --git a/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE2.wav b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE2.wav
new file mode 100644
index 00000000..2f83bb64
Binary files /dev/null and b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE2.wav differ
diff --git a/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE3.wav b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE3.wav
new file mode 100644
index 00000000..943cd8ab
Binary files /dev/null and b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_CHOICE3.wav differ
diff --git a/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_GOODBYE.wav b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_GOODBYE.wav
new file mode 100644
index 00000000..f7642f78
Binary files /dev/null and b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_GOODBYE.wav differ
diff --git a/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_MAIN_MENU.wav b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_MAIN_MENU.wav
new file mode 100644
index 00000000..ca5e7983
Binary files /dev/null and b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_MAIN_MENU.wav differ
diff --git a/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_PLAY_RECORDING_STARTED.wav b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_PLAY_RECORDING_STARTED.wav
new file mode 100644
index 00000000..0f1d6c86
Binary files /dev/null and b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_PLAY_RECORDING_STARTED.wav differ
diff --git a/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_RETRY.wav b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_RETRY.wav
new file mode 100644
index 00000000..0c7b706d
Binary files /dev/null and b/CallAutomation_AppointmentBooking/MediaFiles/PROMPT_RETRY.wav differ
diff --git a/CallAutomation_AppointmentBooking/README.md b/CallAutomation_AppointmentBooking/README.md
new file mode 100644
index 00000000..81bf14ea
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/README.md
@@ -0,0 +1,89 @@
+---
+page_type: sample
+languages:
+- csharp
+products:
+- azure
+- azure-communication-services
+- azure-communication-callautomation
+---
+# Call Automation - Appointment Booking Sample
+
+This sample application shows how the Azure Communication Services - Call Automation SDK can be used to build appointment booking solutions.
+The application accepts an incoming call when an callee dialed in to either ACS Communication Identifier or ACS acquired phone number.
+Application start recording and prompt the Dual-Tone Multi-Frequency (DTMF) tones to select, and then plays the appropriate audio file based on the key pressed by the callee.
+The application has been configured to accept tone-1 through tone-3, and if any other key is pressed, the callee will hear an invalid tone prompt and retry.
+Upon success the call will be disconnected. This sample has been developed as an app service application using .Net7 framework.
+
+# Design
+
+![design](./Data/AppointmentReminderDesign.png)
+
+## Prerequisites
+
+- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/)
+- [Visual Studio (2022 v17.4.0 and above)](https://visualstudio.microsoft.com/vs/)
+- [.NET7 Framework](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) (Make sure to install version that corresponds with your visual studio instance, 32 vs 64 bit)
+- Create an Azure Communication Services resource. For details, see [Create an Azure Communication Resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). You will need to record your resource **connection string** for this sample.
+- Get a phone number for your new Azure Communication Services resource. For details, see [Get a phone number](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number?tabs=windows&pivots=programming-language-csharp)
+- Enable Visual studio dev tunneling for local development. For details, see [Enable dev tunnel] (https://learn.microsoft.com/en-us/connectors/custom-connectors/port-tunneling)
+ - To enable dev tunneling, Click `Tools` -> `Options` in Visual Studio 2022. In the search bar type tunnel, Click the checkbox under `Environment` -> `Preview Features` called `Enable dev tunnels for Web Application`
+ ![EnableDevTunnel](./Data/EnableDevTunnel.png)
+ - Create `Dev Tunnels`, for more details about [Dev Tunnels.](https://learn.microsoft.com/en-us/aspnet/core/test/dev-tunnels?view=aspnetcore-7.0)
+ ![ViewDevTunnels](./Data/ViewDevTunnel.png)
+ ![CreateDevTunnels](./Data/CreateDevTunnel.png)
+
+## Before running the sample for the first time
+
+1. Open an instance of PowerShell, Windows Terminal, Command Prompt or equivalent and navigate to the directory that you would like to clone the sample to.
+2. git clone `https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git`.
+
+### Locally running the Call Automation Appointment Booking app
+1. Go to CallAutomation_AppointmentBooking folder and open `CallAutomation_AppointmentBooking.sln` solution in Visual Studio.
+2. Visual studio dev tunnel url - Run the solution once and check for dev tunnels being created, select to continue on security prompt.
+
+### Publish the Call Automation Appointment Booking to Azure WebApp
+
+1. Right click the `CallAutomation_AppointmentBooking` project and select Publish.
+2. Create a new publish profile and select your app name, Azure subscription, resource group etc. (choose any unique name, as this URL needed for `BASE_URI` configuration settings)
+3. After publishing, add the following configurations on azure portal (under app service's configuration section).
+
+ - COMMUNICATION_CONNECTION_STRING: Azure Communication Service resource's connection string.
+ - DIRECT_OFFERED_PHONE_NUMBER: Azure Communication Service acquired phone number.
+ - BASE_URI: Url of the deployed app service.
+
+### Create Webhook for Microsoft.Communication.IncomingCall event
+IncomingCall is an Azure Event Grid event for notifying incoming calls to your Communication Services resource. To learn more about it, see [this guide](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/incoming-call-notification).
+1. Navigate to your resource on Azure portal and select `Events` from the left side menu.
+1. Select `+ Event Subscription` to create a new subscription.
+1. Filter for Incoming Call event.
+1. Choose endpoint type as web hook and provide the public url generated for your application by Dev Tunnels. Make sure to provide the exact api route that you programmed to receive the event previously. In this case, it would be /api/incomingCall.
+
+ ![Event Grid Subscription for Incoming Call](./Data/EventgridSubscription-IncomingCall.png)
+
+1. Select create to start the creation of subscription and validation of your endpoint as mentioned previously. The subscription is ready when the provisioning status is marked as succeeded.
+
+
+This subscription currently has no filters and hence all incoming calls will be sent to your application. To filter for specific phone number or a communication user, use the Filters tab.
+
+
+
+4. Detailed instructions on publishing the app to Azure are available at [Publish a Web app](https://docs.microsoft.com/visualstudio/deployment/quickstart-deploy-to-azure?view=vs-2019).
+
+**Note**: While you may use http://localhost for local testing, the sample when deployed will only work when served over https. The SDK [does not support http](https://docs.microsoft.com/azure/communication-services/concepts/voice-video-calling/calling-sdk-features#user-webrtc-over-https).
+
+### Troubleshooting
+
+1. Solution doesn't build, it throws errors during build
+
+ Clean/rebuild the C# solution
+
+## Resources
+- [Call Automation Overview](https://learn.microsoft.com/azure/communication-services/concepts/voice-video-calling/call-automation)
+- [Incoming Call Concept](https://learn.microsoft.com/azure/communication-services/concepts/voice-video-calling/incoming-call-notification)
+- [Build a customer interaction workflow using Call Automation](https://learn.microsoft.com/azure/communication-services/quickstarts/voice-video-calling/callflows-for-customer-interactions?pivots=programming-language-csha)
+- [Redirect inbound telephony calls with Call Automation](https://learn.microsoft.com/azure/communication-services/how-tos/call-automation-sdk/redirect-inbound-telephony-calls?pivots=programming-language-csharp)
+- [Quickstart: Play action](https://learn.microsoft.com/azure/communication-services/quickstarts/voice-video-calling/play-action?pivots=programming-language-csharp)
+- [Quickstart: Recognize action](https://learn.microsoft.com/azure/communication-services/quickstarts/voice-video-calling/recognize-action?pivots=programming-language-csharp)
+- [Read more about Call Recording in Azure Communication Services](https://learn.microsoft.com/azure/communication-services/concepts/voice-video-calling/call-recording)
+- [Record and download calls with Event Grid](https://learn.microsoft.com/azure/communication-services/quickstarts/voice-video-calling/get-started-call-recording?pivots=programming-language-csharp)
\ No newline at end of file
diff --git a/CallAutomation_AppointmentBooking/azuredeploy.json b/CallAutomation_AppointmentBooking/azuredeploy.json
new file mode 100644
index 00000000..e69de29b