Online Course Registration using Asp.net, VoguePay and Mandrill API

In this Post, i’ll be explaining how to solve a little problem iv noticed  among some professional course providers. They simply put some links online where applicants can actually register but this  pages are just a simple contact page under the hood as it just sends a mail to the site admin/facilitator about a POTENTIAL applicant, therefore leaving the facilitator to follow up by calling/sending a mail to ALL  the potential applicants. while this can be good for situations where few  applicants are expected. In situations where the total number of seats to be filled is unlimited, then the course provider needs to know precisely, the total number of applicants coming for the program ,thereby making adequate provision for them. Example of such provision includes internet bandwidth, total  number of required machine(P.C) among others.

A simple solution would be to create a little app that accepts the users information, sends them a mail and sms informing them of their registration and that they’ve not paid!!…the  platform will also  make provision for online payment using their debit /credit card. With all this in place, the administrator now knows those that have the higher probability of attending the course because they’ve  paid ,likewise those that have only signified interest without payment. The Administrator will in turn put one or two calls to remind them of their expected payment while those in the first category would just get a reminder of the commencement  date. This simple workflow thou  still a bit crude will greatly reduce the total cost and time spent in the long run.

 

Furthermore, It allows the facilitator to easily enforce the allotment of seat strictly on a first come, first served basis. Once the total number of expected seats have been filled up, the app will simply decline subsequent registrations.

So let’s get our hands dirty and write some codes. For this simple project, I’ll be using the following technologies :

  • ASP.Net MVC
  • Microsoft SQL Server (MySql can also be used depending on the available resources)
  • VoguePay for the payment Gateway
  • Mandril  for sending mails(AmazonSES can also be used but i prefer mandril as it provides so many metrics without writing a single line of code .)
  • SmsLive247 API for sending sms(Nexmo too is a very good provider when it comes to reliability, it’s my favorite among the cloud sms providers and it’s relatively cheap compared to other good ones).

 

I’ll start by  creating a class library project within the visual studio solution, then defining the required entities which are :

public class Course
  {
      [Key]
      public int Id { get; set; }

      [Required(ErrorMessage = "Please enter a title")]
      public string Title { get; set; }

      public string SeoTitle { get; set; }

      [Required]
      public string Description { get; set; }

      public decimal Tuition { get; set; }

      [Display(Name = "Maximum Applicant Number")]
      public int MaxApplicantNumber { get; set; }

      [DefaultValue(true)]
      public bool Active { get; set; }
  }

 

public class SubmittedApplication
  {
      [Key]
      public int Id { get; set; }

      [Display(Name = "Seat Number")]
      public string SeatNumber { get; set; } //0001-01000

      [Display(Name = "Message For Facilitator")]
      public string MessageForFacilitator { get; set; }

      [DefaultValue(false)]
      public bool Paid { get; set; }

      public DateTime TimeStamp { get; set; }

      [Display(Name = "Payment Date")]
      public DateTime? PaymentDate { get; set; }

      [Required]
      public string UserId { get; set; }

      [Required]
      public virtual Course Course { get; set; }
  }
public class MemberProfile
   {
       [Key]
       public int Id { get; set; }

       [Required]
       [Display(Name = "First Name")]
       public string FirstName { get; set; }

       [Required]
       [Display(Name = "Last Name")]
       public string LastName { get; set; }

       [Display(Name = "Postal Address")]
       public string PostalAddress { get; set; }

       [Required]
       public virtual Member Member { get; set; }
   }

 

public class Member : IdentityUser
    {
        public virtual MemberProfile MemberProfile { get; set; }

        public async Task<ClaimsIdentity> 
         GenerateUserIdentityAsync(UserManager<Member> manager)
        {
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            return userIdentity;
        }
    }

and finally

 

public class GatewayTransaction
   {
       [Key]
       public int Id { get; set; }
       public string MerchantId { get; set; }
       public string TransactionId { get; set; }
       public string Email { get; set; }
       public string Total { get; set; }
       public string TotalPaidByBuyer { get; set; }
       public string TotalCreditedToMerchant { get; set; }
       public string ExtraChargesByMerchant { get; set; }
       public string MerchantRef { get; set; }
       public string Memo { get; set; }
       public string Status { get; set; }
       public string Date { get; set; }
       public string Referrer { get; set; }
       public string Method { get; set; }
       public string FundMaturity { get; set; }

       public virtual SubmittedApplication SubmittedApplication { get; set; }

       

   }

Then the interface

public interface ICourseRepository
   {
       IQueryable<Course> GetCourses { get; }
       IQueryable<SubmittedApplication> GetsSubmittedApplications { get; }
       Course CourseDetails(int? id);
       Course DeleteCourse(int courseId);
       void SaveCourse(Course c);
       void SaveApplication(SubmittedApplication sa);
       MemberProfile GetProfile(string username);
   }

and finally the implementation

public class EfCourseRepository : ICourseRepository
  {
      private readonly EfDbContext _context = new EfDbContext();

      public IQueryable<Course> GetCourses
      {
          get { return _context.Courses; }
      }

      public IQueryable<SubmittedApplication> GetsSubmittedApplications
      {
          get { return _context.SubmittedApplications.Include("Course"); }
      }

      public Course CourseDetails(int? id)
      {
          var coursedetails = _context.Courses.Find(id);
          return coursedetails;
      }

      public Course DeleteCourse(int courseId)
      {
          var entity = (from c in _context.Courses where c.Id == courseId select c).FirstOrDefault();
          if (entity != null)
          {
              _context.Courses.Remove(entity);
              _context.SaveChanges();
          }
          return entity;
      }

      public void SaveCourse(Course c)
      {
          if (c.Id == 0)
          {
              _context.Courses.Add(c);
          }
          else
          {
              var cEditor = _context.Courses.Find(c.Id);
              if (cEditor != null)
              {
                  cEditor.Title = c.Title;
                  cEditor.SeoTitle = c.Title.ToSeoUrl();
                  cEditor.Description = c.Description;
                  cEditor.Active = c.Active;
                  cEditor.MaxApplicantNumber = c.MaxApplicantNumber;
                  cEditor.Tuition = c.Tuition;
              }
          }
          _context.SaveChanges();
      }

      public void SaveApplication(SubmittedApplication sa)
      {
          if (sa.Id == 0)
          {
              _context.SubmittedApplications.Add(sa);
          }
          else
          {
              var saEditor = _context.SubmittedApplications.Find(sa.Id);
              if (saEditor != null)
              {
                  saEditor.Course = sa.Course;
                  saEditor.MessageForFacilitator = sa.MessageForFacilitator;
                  saEditor.Paid = sa.Paid;
                  saEditor.PaymentDate = sa.PaymentDate;
                  saEditor.SeatNumber = sa.SeatNumber;
                  saEditor.TimeStamp = sa.TimeStamp;
              }
          }
          _context.SaveChanges();
      }

      public MemberProfile GetProfile(string username)
      {
          var mp = _context.MemberProfiles.FirstOrDefault(x => x.Member.UserName == username);
          return mp;
      }

  }

 

public class EfDbContext : IdentityDbContext<Member>
   {
       public EfDbContext()
           : base("DefaultConnection", false)
       {
       }

       public DbSet<Course> Courses { get; set; }
       public DbSet<GatewayTransaction> GatewayTransactions { get; set; } 
       public DbSet<SubmittedApplication> SubmittedApplications { get; set; }
       public DbSet<MemberProfile> MemberProfiles { get; set; }
       public static EfDbContext Create()
       {
           return new EfDbContext();
       }
   }

There are some helper methods for sending mails and sms

public class MailPusher
   {
       public void SendMail(MailModel mm)
       {
           var api = new MandrillApi("xxxxxxxxxxxxxx");

           var recipients = new List<Mandrill.Messages.Recipient> {new Mandrill.Messages.Recipient(mm.To, mm.Name)};

           var message = new Mandrill.Messages.Message()
           {
               To = recipients.ToArray(),
               BccAddress = mm.Bcc,
               FromEmail = mm.From,
               Subject = mm.Subject,
               
               Html = mm.Message
           };

           api.Send(message);


           
       }
   }

 

public class SendSms
   {
       public void SendMessage(string mobile, string message, string sender)
       {
           PhoneNumberUtil instance = PhoneNumberUtil.GetInstance();
           PhoneNumber number = instance.Parse(mobile, "NG");
           var recipient = instance.Format(number, PhoneNumberFormat.INTERNATIONAL).Remove(0, 1);

           var msg = new ShortMessageService {Message = message, SendTo = recipient.Replace(" ", ""), Sender = sender};

           var slp = new SmsLiveProvider();
           slp.Send(msg);


       }

   }

 

 

The Database Schema of the solution is shown below.The schema does’nt capture all the asp.net identity 2 tables which manages authentication and authorization.

TrainingDbSchema

 

 

Now to our web project, I’ll highlight only some important methods in the controllers as a clear understanding of this methods will give you an idea of what the U.I looks like.

 

The first thing to do here is to inject the interface that was declared in the class library with some other helper methods. I used Ninject for this which can be freely accessed via nuget library

private readonly MailPusher _emailService = new MailPusher();

private readonly ICourseRepository _courseRepo;

public HomeController(ICourseRepository courseRepository){_courseRepo = courseRepository;}

 

I’ll start by highlighting the controller method responsible for displaying all available courses.

public ActionResult Index()
  {
      return View(_courseRepo.GetCourses.ToList());
  }

 

As soon as a course is selected,users are navigated to the “ApplicationDetails” Method which provides the details of the selected course in addition to a textarea(personal message to the facilitator) and button for initiating the actual registration process. This is achieved by the use of a ViewModel

[HttpGet]
public ActionResult ApplicationDetails(int courseId)
{

    var entity = new ApplicationVm {Course = _courseRepo.CourseDetails(courseId)};
    return View(entity);
}

Next, to one of the most important methods, it handles the actual registration and initiation of messages. The codes is self explanatory.

[HttpPost]
     [Authorize]
     public ActionResult ApplicationDetails(ApplicationVm avm)
     {
         //check if user has verfied phone number
         var me = _courseRepo.GetProfile(User.Identity.GetUserName());
         var course = _courseRepo.CourseDetails(avm.Course.Id);
         var submittedApplications =
          _courseRepo.GetsSubmittedApplications.Where
            (x => x.Course.Id == avm.Course.Id).ToList();
         if (me.Member.PhoneNumberConfirmed == false)
         {
             ModelState.AddModelError("","Please,Ensure you add a mobile 
               number to your profile AND  verify the submitted number ");
             var data = new ApplicationVm { Course = course };
             return View(data);
         }

         //check if max count for that course has not been reached
         if (submittedApplications.Count >= course.MaxApplicantNumber)
         {
             ModelState.AddModelError("", "The Total number of Applicants for 
        this course has already been reached,you may contact the Administrator 
          via the contact page for consideration");
             var data = new ApplicationVm { Course = course };
             return View(data);
         }

         //check if the user has previously applied for that course
         var checkPreviousRegistration = 
          submittedApplications.Where(x => x.UserId == me.Member.Id);
         if (checkPreviousRegistration.Any())
         {
             TempData["info"] = "You've previously registered;
             Please review your previous registration";
             return RedirectToAction("ApplicationReview", new { id = course.Id });

         }


         //create a course application object

         var applicationForm = new SubmittedApplication
         {
             Course = _courseRepo.CourseDetails(avm.Course.Id),
             MessageForFacilitator = avm.Message,
             Paid = false,
             SeatNumber = "NOT ASSIGNED",
             TimeStamp = DateTime.Now,
             UserId = User.Identity.GetUserId()
         };

       

         _courseRepo.SaveApplication(applicationForm);

         //send sms and email for registering and tell user to pay


    



         StringWriter writer = new StringWriter();
         HtmlTextWriter html = new HtmlTextWriter(writer);

         html.RenderBeginTag(HtmlTextWriterTag.H1);
         html.WriteEncodedText("Course Registration");
         html.RenderEndTag();

         html.WriteEncodedText("Dear " + me.FirstName);

         html.WriteBreak();
         html.RenderBeginTag(HtmlTextWriterTag.P);
         html.WriteEncodedText("Your registration for " + "'"
         + course.Title + "'"+" has been successfully recieved");
         html.WriteBreak();
         html.WriteEncodedText("Tuition : NOT PAID ");
         html.WriteBreak();
         html.WriteEncodedText("Seat Number : NOT ASSIGNED ");
         html.RenderEndTag();
         html.WriteBreak();
          
         html.WriteEncodedText("Thank You ");
         html.WriteBreak();
         html.WriteEncodedText("The Edu Centre");
         html.Flush();

         string emailString = writer.ToString();

         var mm = new MailModel
         {
             Message = emailString,
             Bcc = "app.logs@ourlearningcentre.com",
             From = "no-reply@ourlearningcentre.com",
             Name =me.FirstName + " " + me.LastName ,
             Subject = "New Course Registration",
             To = me.Member.Email
         };

         ThreadPool.QueueUserWorkItem(stateObject => _emailService.SendMail(mm));





         String smsNotification = "Course Registration" + Environment.NewLine;

         smsNotification = smsNotification + "Your registration for " + "'" 
        + course.Title + "'" + " has been successfully recieved" + Environment.NewLine;
         smsNotification = smsNotification + "Tuition : NOT PAID" + Environment.NewLine;
         smsNotification = smsNotification + 
          "Seat Number : NOT ASSIGNED" + Environment.NewLine;

         smsNotification = smsNotification + Environment.NewLine;
         smsNotification = smsNotification + "Thank you";
         smsNotification = smsNotification + Environment.NewLine;




         //send sms to applicant
         var msg = new SendSms();
         ThreadPool.QueueUserWorkItem(stateObject => 
          msg.SendMessage(me.Member.PhoneNumber, smsNotification, "EduCentre"));
 

         //redirect user to page that shows them there application

         return RedirectToAction("ApplicationReview", new {id = course.Id});

     }

At this point,the User has have successfully logged and the next thing would be to pay the tuition fee for the course in other to secure a seat. The gateway we are using accepts our parameters,processes the transaction and sends the transaction result to a controller method in our project just like most of other third party agregator does.

 

[Authorize]
public ActionResult MakePayment(int id)
{
    //get the course application in question
    var application = _courseRepo.GetsSubmittedApplications.FirstOrDefault(x => x.Id == id);
    if (application != null)
    {
        if (application.Paid)
        {
            TempData["info"] = "You've already made payment for this course,
             you do not need to pay again";
            return RedirectToAction("ApplicationReview", 
             new { id = application.Course.Id });
        }

        /////redirect to gateway
        #region GatewayRegion
        const string url = "https://voguepay.com/pay/";

        Response.Clear();
        Response.BufferOutput = true;
        var sb = new StringBuilder();
        sb.Append("<html>");
        sb.AppendFormat("<body onload='document.forms[0].submit()'>");
        sb.AppendFormat("<form action='{0}' method='post'>", url);
        sb.AppendFormat("<input type='hidden' name='v_merchant_id' value='{0}'>", 
        ConfigurationManager.AppSettings.Get("MerchantId"));
        sb.AppendFormat("<input type='hidden' name='merchant_ref' value='{0}'>", 
        application.Id.ToString());
        sb.AppendFormat("<input type='hidden' name='memo' value='{0}'>", "PAYMENT FOR "
         + application.Course.Title);
        sb.AppendFormat("<input type='hidden' name='total' value='{0}'>",
        application.Course.Tuition);
        sb.AppendFormat("<input type='hidden' name='notify_url' value='{0}'>",
        "https://www.ourlearningcentre.com/Home/GatewayNotification");
        sb.AppendFormat("<input type='hidden' name='success_url' value='{0}'>", 
        "https://www.ourlearningcentre.com/Home/PaymentSuccess");
        sb.AppendFormat("<input type='hidden' name='fail_url' value='{0}'>",
        "https://www.ourlearningcentre.com/Home/PaymentFailed");
        sb.Append("</form>");
        sb.Append("</body>");
        sb.Append("</html>");
        Response.Write(sb.ToString());
        Response.End();

        #endregion

        ViewBag.RedirectData = sb;


        return View("Completed");


        
    }
    TempData["info"] = "Ensure you select a course you've registered for";
    return RedirectToAction("MyCourses");

}

 

[HttpPost]
[AllowAnonymous]
public ActionResult GatewayNotification(string transaction_id)
{
    string uri = "https://voguepay.com/?v_transaction_id=" + transaction_id + "&" + "type=" + "json";

    WebClient client = new WebClient {Encoding = Encoding.UTF8};

    var response = client.DownloadString(uri);
    var j = JsonConvert.DeserializeObject<Transaction>(response);
    var refId = int.Parse(j.merchant_ref);
    var application = _courseRepo.GetsSubmittedApplications.
     FirstOrDefault(x => x.Id == refId);
    if (application != null)
    {
        if (!application.Paid && j.status == "Approved" &&
         application.Course.Tuition == Decimal.Parse(j.total))
        {
            //update to PAID
            //update Payment Time
            //Generate and Assign seat number
            //send email showing seat number and say thank u
            //send sms showing seat number and say thank u
            //save gateway information(deserialized data) 
             //to database for future refrence
           
        }
        
    }

    return null;
}

With all this put together, the amount of work required for a training program would be drastically reduced.  I may upload the entire source if time permits and i hope this helps someone someday.Please note that some of the above code blocks would need to be refactored in other be a bit more secured.

 

Leave a Reply

Be the First to Comment!

Notify of
avatar

wpDiscuz