1. Code
  2. Coding Fundamentals
  3. Design Patterns

Building a Note-Taking SAAS Using ASP.NET MVC 5

Scroll to top
Final product imageFinal product imageFinal product image
What You'll Be Creating

1. Introduction

In this tutorial, I'm going to show you how to build a Software-as-a-Service (SaaS) minimum viable product (MVP). To keep things simple, the software is going to allow our customers to save a list of notes. 

I am going to offer three subscription plans: the Basic plan will have a limit of 100 notes per user, the Professional plan will allow customers to save up to 10,000 notes, and the Business plan will allow a million notes. The plans are going to cost $10, $20 and $30 per month respectively. In order to receive payment from our customers, I'm going to use Stripe as a payment gateway, and the website is going to be deployed to Azure.

2. Setup 

2.1 Stripe

In a very short time Stripe has become a very well known Payment Gateway, mainly because of their developer-friendly approach, with simple and well-documented APIs. Their pricing is also very clear: 2.9% per transaction + 30 cents. No setup fees or hidden charges. 

Credit card data is also very sensitive data, and in order to be allowed to receive and store that data in my server, I need to be PCI compliant. Because that's not an easy or quick task for most small companies, the approach that many payment gateways take is: You display the order details, and when the customer agrees to purchase, you redirect the customer to a page hosted by the payment gateway (bank, PayPal, etc), and then they redirect the customer back.

Stripe has a nicer approach to this problem. They offer a JavaScript API, so we can send the credit card number directly from the front-end to Stripe's servers. They return a one-time use token that we can save to our database. Now, we only need an SSL certificate for our website that we can quickly purchase from about $5 per year.

Now, sign up for a Stripe account, as you'll need it to charge your customers.

2.2 Azure

As a developer I don't want to be dealing with dev-ops tasks and managing servers if I don't have to. Azure websites is my choice for hosting, because it's a fully managed Platform-as-a-Service. It allows me to deploy from Visual Studio or Git, I can scale it easily if my service is successful, and I can focus on improving my application. They offer $200 to spend on all Azure services in the first month to new customers. That's enough to pay for the services that I am using for this MVP. Sign up for Azure.

2.3 Mandrill and Mailchimp: Transactional Email

Sending emails from our application might not seem like a very complex task, but I would like to monitor how many emails are delivered successfully, and also design responsive templates easily. This is what Mandrill offers, and they also let us send up to 12,000 emails per month for free. Mandrill is built by MailChimp, so they know about the business of sending emails. Also, we can create our templates from MailChimp, export them to Mandrill, and send emails from our app using our templates. Sign up for Mandrill, and sign up for MailChimp.

2.4 Visual Studio 2013 Community Edition

Last but not least, we need Visual Studio to write our application. This edition, which was launched only a few months ago, is completely free and is pretty much equivalent to Visual Studio Professional. You can download it here, and this is all we need, so now we can focus on the development.

3. Creating the Website

The first thing that we need to do is open Visual Studio 2013. Create a new ASP.NET Web Application:

  • Go to File > New Project and choose ASP.NET Web Application.
  • On the ASP.NET template dialog, choose the MVC template and select Individual User Accounts.
New ASPNET MVC ProjectNew ASPNET MVC ProjectNew ASPNET MVC Project

This project creates an application where a user can login by registering an account with the website. The website is styled using Bootstrap, and I'll continue building the rest of the app with Bootstrap. If you hit F5 in Visual Studio to run the application, this is what you will see:

Example HomepageExample HomepageExample Homepage

This is the default landing page, and this page is one of the most important steps to convert our visitors into customers. We need to explain the product, show the price for each plan, and offer them the chance to sign up for a free trial. For this application I am creating three different subscription plans:

  • Basic: $10 per month
  • Professional: $20 per month
  • Business: $30 per month

3.1 Landing Page

For some help creating a landing page, you can visit ThemeForest and purchase a template. For this sample, I am using a free template, and you can see the final result in the photo below.

Landing PageLanding PageLanding Page

3.2 Registration Page

In the website that we created in the previous step, we also get a Registration form template. From the landing page, when you navigate to Prices, and click on Free Trial, you navigate to the registration page. This is the default design:

Registration PageRegistration PageRegistration Page

We only need one extra field here to identify the subscription plan that the user is joining. If you can see in the navigation bar of the photo, I am passing that as a GET parameter. In order to do that, I generate the markup for the links in the landing page using this line of code:

1
<a href="@Url.Action("Register", "Account", new { plan = "business" })">
2
    Free Trial
3
</a>

To bind the Subscription Plan to the back-end, I need to modify the class RegisterViewModel and add the new property.

1
public class RegisterViewModel
2
{
3
    [Required]
4
    [EmailAddress]
5
    [Display(Name = "Email")]
6
    public string Email { get; set; }
7
8
    [Required]
9
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
10
    [DataType(DataType.Password)]
11
    [Display(Name = "Password")]
12
    public string Password { get; set; }
13
14
    [DataType(DataType.Password)]
15
    [Display(Name = "Confirm password")]
16
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
17
    public string ConfirmPassword { get; set; }
18
19
    public string SubscriptionPlan { get; set; }
20
}

I also have to edit AccountController.cs, and modify the Action Register to receive the plan:

1
[AllowAnonymous]
2
public ActionResult Register(string plan)
3
{
4
    return View(new RegisterViewModel
5
    {
6
        SubscriptionPlan = plan
7
    });
8
}

Now, I have to render the Plan Identifier in a hidden field in the Register form:

1
@Html.HiddenFor(m => m.SubscriptionPlan)

The last step will be to subscribe the user to the plan, but we'll get to that a bit later. I also update the design of the registration form.

Updated Registration FormUpdated Registration FormUpdated Registration Form

3.3 Login Page

Login PageLogin PageLogin Page

In the template we also get a login page and action controllers implemented. The only thing I need to do is to make it look prettier.

Updated Login PageUpdated Login PageUpdated Login Page

3.4 Forgot Password

Take a second look at the previous screenshot, and you'll notice that I added a "Forgot your Password?" link. This is already implemented in the template, but it's commented out by default. I don't like the default behaviour, where the user needs to have the email address confirmed to be able to reset the password. Let's remove that restriction. In the file AccountController.cs edit the action ForgotPassword:

1
[HttpPost]
2
[AllowAnonymous]
3
[ValidateAntiForgeryToken]
4
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
5
{
6
    if (ModelState.IsValid)
7
    {
8
        var user = await UserManager.FindByNameAsync(model.Email);
9
        if (user == null)
10
        {
11
            // Don't reveal that the user does not exist or is not confirmed

12
            return View("ForgotPasswordConfirmation");
13
        }
14
15
        // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771

16
        // Send an email with this link

17
        // string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);

18
        // var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);    	

19
        // await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>");

20
        // return RedirectToAction("ForgotPasswordConfirmation", "Account");

21
    }
22
23
    // If we got this far, something failed, redisplay form

24
    return View(model);
25
}

The code to send the email with the link to reset the password is commented out. I'll show how to implement that part a bit later. The only thing left for now is to update the design of the pages:

  • ForgotPassword.cshtml: Form that is displayed to the user to enter his or her email.
  • ForgotPasswordConfirmation.cshtml: Confirmation message after the reset link has been emailed to the user.
  • ResetPassword.cshtml: Form to reset the password after navigating to the reset link from the email.
  • ResetPasswordConfirmation.cshtml: Confirmation message after the password has been reset.
Forgot Password PageForgot Password PageForgot Password Page

4. ASP.NET Identity 2.0

ASP.NET Identity is a fairly new library that has been built based on the assumption that users will no longer log in by using only a username and password. OAuth integration to allow users to log in through social channels such as Facebook, Twitter, and others is very easy now. Also, this library can be used with Web API, and SignalR. 

On the other hand, the persistence layer can be replaced, and it's easy to plug in different storage mechanisms such as NoSQL databases. For the purposes of this application, I will use Entity Framework and SQL Server.

The project that we just created contains the following three NuGet packages for ASP.NET Identity:

  • Microsoft.AspNet.Identity.Core: This package contains the core interfaces for ASP.NET Identity.
  • Microsoft.AspNet.Identity.EntityFramework: This package has the Entity Framework implementation of the previous library. It will persist the data to SQL Server.
  • Microsoft.AspNet.Identity.Owin: This package plugs the middle-ware OWIN authentication with ASP.NET Identity.

The main configuration for Identity is in App_Start/IdentityConfig.cs. This is the code that initializes Identity.

1
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
2
{
3
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
4
    // Configure validation logic for usernames

5
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
6
    {
7
        AllowOnlyAlphanumericUserNames = false,
8
        RequireUniqueEmail = true
9
    };
10
11
    // Configure validation logic for passwords

12
    manager.PasswordValidator = new PasswordValidator
13
    {
14
        RequiredLength = 6,
15
        RequireNonLetterOrDigit = true,
16
        RequireDigit = true,
17
        RequireLowercase = true,
18
        RequireUppercase = true,
19
    };
20
21
    // Configure user lockout defaults

22
    manager.UserLockoutEnabledByDefault = true;
23
    manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
24
    manager.MaxFailedAccessAttemptsBeforeLockout = 5;
25
26
    // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user

27
    // You can write your own provider and plug it in here.

28
    manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser>
29
    {
30
        MessageFormat = "Your security code is {0}"
31
    });
32
    manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
33
    {
34
        Subject = "Security Code",
35
        BodyFormat = "Your security code is {0}"
36
    });
37
    manager.EmailService = new EmailService();
38
    manager.SmsService = new SmsService();
39
    var dataProtectionProvider = options.DataProtectionProvider;
40
    if (dataProtectionProvider != null)
41
    {
42
        manager.UserTokenProvider = 
43
            new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
44
    }
45
    return manager;
46
}

As you can see in the code, it's pretty easy to configure users' validators and password validators, and two factor authentication can also be enabled. For this application, I use cookie-based authentication. The cookie is generated by the framework and is encrypted. This way, we can scale horizontally, adding more servers if our application needs it.

5. Sending Emails With Mandrill

You can use MailChimp to design email templates, and Mandrill to send emails from your application. In the first place you need to link your Mandrill account to your MailChimp account:

  • Log in to MailChimp, click your username in the right-hand panel, and select Account from the drop-down.
  • Click on Integrations and find the Mandrill option in the list of integrations.
  • Click on it to see the integration details, and click the Authorize Connection button. You will be redirected to Mandrill. Allow the connection, and the integration will be completed.
Setting Up MandrillSetting Up MandrillSetting Up Mandrill

5.1 Creating the "Welcome to My Notes" Email Template

Navigate to Templates in MailChimp, and click on Create Template.

Creating a TemplateCreating a TemplateCreating a Template

Now, select one of the templates offered by MailChimp. I selected the first one:

Choosing a TemplateChoosing a TemplateChoosing a Template

In the template editor, we modify the content as we like. One thing to note, as you can see below, is that we can use variables. The format is *|VARIABLE_NAME|*. From the code, we'll set those for each customer. When you are ready, click on Save and Exit at the bottom right.

Populating the TemplatePopulating the TemplatePopulating the Template

In the Templates list, click on Edit, on the right side, and select Send To Mandrill. After a few seconds you will get a confirmation message.

Sending To MandrillSending To MandrillSending To Mandrill

To confirm that the template has been exported, navigate to Mandrill and log in. Select Outbound from the left menu, and then Templates from the top menu. In the image below you can see that the template has been exported.

Outbound DataOutbound DataOutbound Data

If you click on the name of the template, you'll see more information about the template. The field "Template Slug" is the text identifier that we will use in our application to let Mandrill API know which template we want to use for the email that we are sending.

Template MarkupTemplate MarkupTemplate Markup

I leave it as an exercise for you to create a "Reset Password" template.

Reset Password TemplateReset Password TemplateReset Password Template

5.2 Sending Emails From My Notes

Sending EmailsSending EmailsSending Emails

In the first place, install Mandrill from NuGet. After that, add your Mandrill API Key to Web.config App Settings. Now, open App_Start/IdentityConfig.cs and you'll see the class EmailService skeleton pending implementation:

1
public class EmailService : IIdentityMessageService
2
{
3
    public Task SendAsync(IdentityMessage message)
4
    {
5
        // Plug in your email service here to send an email.

6
        return Task.FromResult(0);
7
    }
8
}

Although this class has only the method SendAsync, because we have two different templates (Welcome Email Template and Reset Password Template), we will implement new methods. The final implementation will look like this.

1
public class EmailService : IIdentityMessageService
2
{
3
    private readonly MandrillApi _mandrill;
4
    private const string EmailFromAddress = "no-reply@mynotes.com";
5
    private const string EmailFromName = "My Notes";
6
7
    public EmailService()
8
    {
9
        _mandrill = new MandrillApi(ConfigurationManager.AppSettings["MandrillApiKey"]);
10
    }
11
12
    public Task SendAsync(IdentityMessage message)
13
    {
14
        var task = _mandrill.SendMessageAsync(new EmailMessage
15
        {
16
            from_email = EmailFromAddress,
17
            from_name = EmailFromName,
18
            subject = message.Subject,
19
            to = new List<Mandrill.EmailAddress> { new EmailAddress(message.Destination) },
20
            html = message.Body
21
        });
22
23
        return task;
24
    }
25
26
    public Task SendWelcomeEmail(string firstName, string email)
27
    {
28
        const string subject = "Welcome to My Notes";
29
30
        var emailMessage = new EmailMessage
31
        {
32
            from_email = EmailFromAddress,
33
            from_name = EmailFromName,
34
            subject = subject,
35
            to = new List<Mandrill.EmailAddress> { new EmailAddress(email) },
36
            merge = true,
37
        };
38
39
        emailMessage.AddGlobalVariable("subject", subject);
40
        emailMessage.AddGlobalVariable("first_name", firstName);
41
42
        var task = _mandrill.SendMessageAsync(emailMessage, "welcome-my-notes-saas", null);
43
44
        task.Wait();
45
46
        return task;
47
    }
48
49
    public Task SendResetPasswordEmail(string firstName, string email, string resetLink)
50
    {
51
        const string subject = "Reset My Notes Password Request";
52
53
        var emailMessage = new EmailMessage
54
        {
55
            from_email = EmailFromAddress,
56
            from_name = EmailFromName,
57
            subject = subject,
58
            to = new List<Mandrill.EmailAddress> { new EmailAddress(email) }
59
        };
60
        emailMessage.AddGlobalVariable("subject", subject);
61
        emailMessage.AddGlobalVariable("FIRST_NAME", firstName);
62
        emailMessage.AddGlobalVariable("RESET_PASSWORD_LINK", resetLink);
63
64
        var task = _mandrill.SendMessageAsync(emailMessage, "reset-password-my-notes-saas", null);
65
66
        return task;
67
    }
68
}

To send an email through Mandrill API:

  1. Create email message.
  2. Set message variables' values.
  3. Send email specifying the template slug.

In AccountController -> Register action, this is the code snippet to send the welcome email:

1
await _userManager.EmailService.SendWelcomeEmail(user.UserName, user.Email);

In AccountController -> ForgotPassword action, this is the code to send the email:

1
// Send an email to reset password

2
string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
3
var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
4
await UserManager.EmailService.SendResetPasswordEmail(user.UserName, user.Email, callbackUrl);

6. Integrating SAAS Ecom for Billing

One important thing in SAAS applications is billing. We need to have a way to charge our customers periodically, monthly in this example. Because this part is something that requires a lot of work, but doesn't add anything valuable to the product that we are selling, we are going to use the open source library SAAS Ecom that was created for this purpose. 

6.1 Data Model: Entity Framework Code First

SAAS Ecom has a dependency on Entity Framework Code First. For those of you that are not familiar with it, Entity Framework Code First allows you to focus on creating C# POCO classes, letting Entity Framework map the classes to database tables. It follows the idea of convention over configuration, but you can still specify mappings, foreign keys and so on, if needed.

To add SAAS Ecom to our project, just install the dependency using NuGet. The library is split in two packages: SaasEcom.Core that contains the business logic, and SaasEcom.FrontEnd that contains some view helpers to use in an MVC application. Go ahead and install SaasEcom.FrontEnd.

Installing SaasEcomInstalling SaasEcomInstalling SaasEcom

You can see that some files have been added to your solution:

  • Content/card-icons: Credit card icons to display in the billing area
  • Controllers/BillingController: Main controller
  • Controllers/StripeWebhooksController: Stripe Webhooks
  • Scripts/saasecom.card.form.js: Script to add credit card to Stripe
  • Views/Billing: Views and view partials
Files Added To The ProjectFiles Added To The ProjectFiles Added To The Project

There are still a few steps left to integrate SAAS Ecom, so get your Stripe API Keys and add them to Web.config.

1
<appSettings>
2
  <add key="StripeApiSecretKey" value="your_key_here" />
3
  <add key="StripeApiPublishableKey" value="your_key_here" />
4
</appSettings>

If you try to compile, you'll see errors:

Open the file Models/IdentityModels.cs, and then make the class ApplicationUser inherit from SaasEcomUser

1
ApplicationUser : SaasEcomUser { /* your class methods*/ }

Open the file Models/IdentityModels.cs, and then your class ApplicationDbContext should inherit from SaasEcomDbContext<ApplicationUser>

1
ApplicationDbContext : SaasEcomDbContext<ApplicationUser> 
2
{ /* Your Db context properties */ }

Because ApplicationUser is inheriting from SaasEcomUser, the default behaviour for Entity Framework would be to create two tables in the database. Because we don't need that in this case, we need to add this method to the class ApplicationDbContext to specify that it should use only one table:

1
protected override void OnModelCreating(DbModelBuilder modelBuilder)
2
{
3
    modelBuilder.Entity<ApplicationUser>().Map(m => m.MapInheritedProperties());
4
    base.OnModelCreating(modelBuilder);
5
}

As we just updated the DbContext, to make it inherit from SaasEcomDbContext, the database has to be updated too. In order to do that, enable code migrations and update the database opening NuGet Package Manager from the menu Tools > NuGet Package Manager > Package Manager Console:

1
PM > enable-migrations

2
PM > add-migration Initial

3
PM > update-database

If you get an error when you run update-database, the database (SQL Compact) is inside your AppData folder, so open the database, delete all the tables in it, and then run update-database again.

6.2 Creating the Subscription Plans in Stripe and Database

The next step in the project is to integrate Stripe to charge our customers monthly, and for that we need to create the subscription plans and pricing in Stripe. So sign in to your Stripe dashboard, and create your subscription plans as you can see in the pictures.

StripeStripeStripe
Stripe PlansStripe PlansStripe Plans
Stripe Test PlansStripe Test PlansStripe Test Plans

Once we have created the Subscription Plans in Stripe, let's add them to the database. We do this so that we don't have to query Stripe API each time that we need any information related to subscription plans. 

Also, we can store specific properties related to each plan. In this example, I'm saving as a property of each plan the number of notes that a user can save: 100 notes for the basic plan, 10,000 for the professional, and 1 million for the business plan. We add that information to the Seed method that is executed each time that the database is updated when we run update-database from NuGet Package Manager console. 

Open the file Migrations/Configuration.cs and add this method: 

1
protected override void Seed(MyNotes.Models.ApplicationDbContext context)
2
{
3
    //  This method will be called after migrating to the latest version.

4
5
    var basicMonthly = new SubscriptionPlan
6
    {
7
        Id = "basic_monthly",
8
        Name = "Basic",
9
        Interval = SubscriptionPlan.SubscriptionInterval.Monthly,
10
        TrialPeriodInDays = 30,
11
        Price = 10.00,
12
        Currency = "USD"
13
    };
14
    basicMonthly.Properties.Add(new SubscriptionPlanProperty { Key = "MaxNotes", Value = "100" });
15
16
    var professionalMonthly = new SubscriptionPlan
17
    {
18
        Id = "professional_monthly",
19
        Name = "Professional",
20
        Interval = SubscriptionPlan.SubscriptionInterval.Monthly,
21
        TrialPeriodInDays = 30,
22
        Price = 20.00,
23
        Currency = "USD"
24
    };
25
    professionalMonthly.Properties.Add(new SubscriptionPlanProperty
26
    {
27
        Key = "MaxNotes",
28
        Value = "10000"
29
    });
30
31
    var businessMonthly = new SubscriptionPlan
32
    {
33
        Id = "business_monthly",
34
        Name = "Business",
35
        Interval = SubscriptionPlan.SubscriptionInterval.Monthly,
36
        TrialPeriodInDays = 30,
37
        Price = 30.00,
38
        Currency = "USD"
39
    };
40
    businessMonthly.Properties.Add(new SubscriptionPlanProperty
41
    {
42
        Key = "MaxNotes",
43
        Value = "1000000"
44
    });
45
46
    context.SubscriptionPlans.AddOrUpdate(
47
        sp => sp.Id,
48
        basicMonthly,
49
        professionalMonthly,
50
        businessMonthly);
51
}

6.3 Subscribe a Customer to a Plan on Sign-Up

The next thing that we need to do is to ensure that each time a user registers for our app, we also create the user in Stripe using their API. To do that we use SAAS Ecom API, and we just need to edit the action Register on AccountController and add these lines after creating the user in the database:

1
// Create Stripe user

2
await SubscriptionsFacade.SubscribeUserAsync(user, model.SubscriptionPlan);
3
await UserManager.UpdateAsync(user);

The method SubscribeUserAsync subscribes the user to the plan in Stripe, and if the user doesn't exist already in Stripe it is created too. This is useful if you have a freemium SAAS and you only create users in Stripe once they are on a paid plan. Another small change in the Register action from AccountController is to save the RegistrationDate and LastLoginTime when you create the user:

1
var user = new ApplicationUser
2
{
3
    UserName = model.Email, 
4
    Email = model.Email, 
5
    RegistrationDate = DateTime.UtcNow,
6
    LastLoginTime = DateTime.UtcNow
7
};
8
var result = await UserManager.CreateAsync(user, model.Password);

As we need the dependency SubscriptionsFacade from SAAS Ecom, add it as a property to Account Controller:

1
private SubscriptionsFacade _subscriptionsFacade;
2
private SubscriptionsFacade SubscriptionsFacade
3
{
4
    get
5
    {
6
        return _subscriptionsFacade ?? (_subscriptionsFacade = new SubscriptionsFacade(
7
            new SubscriptionDataService<ApplicationDbContext, ApplicationUser>
8
                (HttpContext.GetOwinContext().Get<ApplicationDbContext>()),
9
            new SubscriptionProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]),
10
            new CardProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"],
11
                new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>())),
12
            new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()),
13
            new CustomerProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]),
14
            new SubscriptionPlanDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()),
15
            new ChargeProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"])));
16
    }
17
}

You can simplify the way that this is instantiated using dependency injection, but this is something that can be covered in another article.

6.4 Integrate Billing Views

When we added SAAS Ecom to the project, some view partials were added too. They use the main _Layout.cshtml, but that layout is the one being used by the landing page. We need to add a different layout for the web application area or customer dashboard.

I have created a very similar version to the _Layout.cshtml that is created when you add a new MVC project in Visual Studio—you can see the _DashboardLayout.cshtml in GitHub.

The main differences are that I have added font-awesome and an area to display Bootstrap notifications if they're present:

1
<div id="bootstrap_alerts">
2
    @if (TempData.ContainsKey("flash"))
3
    {
4
        @Html.Partial("_Alert", TempData["flash"]);
5
    }
6
</div>

For the views in the folder Views/Billing, set the layout to _DashboardLayout, otherwise it would use the default one that is _Layout.cshtml. Do the same thing for views on the folder Views/Manage:

1
Layout = "~/Views/Shared/_DashboardLayout.cshtml";

I have slightly modified "DashboardLayout" to use some styles from the main website, and it looks like this after signing up and navigating to the Billing section:

Billing TemplateBilling TemplateBilling Template

In the billing area a customer can Cancel or Upgrade / Downgrade a subscription. Add payment details, using Stripe JavaScript API, so we don't need to be PCI compliant and only need SSL in the server to take payments from our customers.

Payment TemplatePayment TemplatePayment Template

To properly test your new application, you can use several credit card numbers provided by Stripe.

Testing PaymentTesting PaymentTesting Payment

The last thing that you might want to do is set up Stripe Webhooks. This is used to let Stripe notify you about events that happen in your billing, like payment successful, payment overdue, trial about to expire, and so on—you can get a full list from the Stripe documentation. The Stripe event is sent as JSON to a public facing URL. To test this locally you probably want to use Ngrok.

When SAAS Ecom was installed, a new controller was added to handle the webhooks from Stripe: StripeWebhooksController.cs. You can see there how the invoice created event is handled:

1
case "invoice.payment_succeeded": // Occurs whenever an invoice attempts to be paid, and the payment succeeds.

2
    StripeInvoice stripeInvoice = Stripe.Mapper<StripeInvoice>.MapFromJson(stripeEvent.Data.Object.ToString());
3
    Invoice invoice = SaasEcom.Core.Infrastructure.Mappers.Map(stripeInvoice);
4
    if (invoice != null && invoice.Total > 0)
5
    {
6
        // TODO get the customer billing address, we still have to instantiate the address on the invoice

7
        invoice.BillingAddress = new BillingAddress();
8
9
        await InvoiceDataService.CreateOrUpdateAsync(invoice);
10
11
        // TODO: Send invoice by email

12
13
    }
14
    break;

You can implement as many events in the controller as you need.

7. Building Note-Taking Functionality in Our App

The most important part of this SAAS application is to allow our customers to save notes. In order to create this functionality, let's start by creating the Note class:

1
public class Note
2
{
3
    public int Id { get; set; }
4
5
    [Required]
6
    [MaxLength(250)]
7
    public string Title { get; set; }
8
9
    [Required]
10
    public string Text { get; set; }
11
    public DateTime CreatedAt { get; set; }
12
}

Add a One to Many relationship from ApplicationUser to Note:

1
public virtual ICollection<Note> Notes { get; set; }

Because the DbContext has changed, we need to add a new database Migration, so open Nuget Package Manager console and run:

1
PM> add-migration NotesAddedToModel

This is the generated code:

1
public partial class NotesAddedToModel : DbMigration
2
{
3
    public override void Up()
4
    {
5
        CreateTable(
6
            "dbo.Notes",
7
            c => new
8
                {
9
                    Id = c.Int(nullable: false, identity: true),
10
                    Title = c.String(nullable: false, maxLength: 250),
11
                    Text = c.String(nullable: false),
12
                    CreatedAt = c.DateTime(nullable: false),
13
                    ApplicationUser_Id = c.String(maxLength: 128),
14
                })
15
            .PrimaryKey(t => t.Id)
16
            .ForeignKey("dbo.AspNetUsers", t => t.ApplicationUser_Id)
17
            .Index(t => t.ApplicationUser_Id);
18
        
19
    }
20
    
21
    public override void Down()
22
    {
23
        DropForeignKey("dbo.Notes", "ApplicationUser_Id", "dbo.AspNetUsers");
24
        DropIndex("dbo.Notes", new[] { "ApplicationUser_Id" });
25
        DropTable("dbo.Notes");
26
    }
27
}

The next thing we need is the Controller MyNotes. As we already have the model class Notes, we use the scaffold to create the controller class to have create, read, update and delete methods using Entity Framework. We also use the scaffold to generate the views.

Add ScaffoldingAdd ScaffoldingAdd Scaffolding 
Add ControllerAdd ControllerAdd Controller

At this point, after a user is registered successfully on My Notes, redirect the user to the Index action of NotesController

1
TempData["flash"] = new FlashSuccessViewModel("Congratulations! Your account has been created.");
2
return RedirectToAction("Index", "Notes");

So far, we have created a CRUD (Create / Read / Update / Delete) interface for Notes. We still need to check when users try to add notes, to make sure that they have enough space in their subscriptions.

Empty list of notes:

Index TemplateIndex TemplateIndex Template

Create new note:

Creating a NoteCreating a NoteCreating a Note

List of notes:

Listing NotesListing NotesListing Notes

Note detail:

Detailed NotesDetailed NotesDetailed Notes

Edit note:

Editing a NoteEditing a NoteEditing a Note

Confirm note deletion:

Deleting a NoteDeleting a NoteDeleting a Note

I'm going to edit slightly the default markup:

  • In the form to create a note, I removed the CreatedAt field, and set the value in the controller.
  • In the form to edit a note, I changed CreatedAt to be a hidden field so that it's not editable.
  • I have slightly modified the CSS to make this form look a bit nicer too.

When we generated the Notes controller using Entity Framework, the list of notes was listing all the notes in the database, not only the notes for the logged-in user. For security we need to check that users can only see, modify or delete the notes that belong to them.

We also need to check how many notes a user has before allowing him or her to create a new one, to check that the subscription plan limits are met. Here is the new code for NotesController:

1
public class NotesController : Controller
2
{
3
    private readonly ApplicationDbContext _db = new ApplicationDbContext();
4
5
    private SubscriptionsFacade _subscriptionsFacade;
6
    private SubscriptionsFacade SubscriptionsFacade
7
    {
8
        get
9
        {
10
            return _subscriptionsFacade ?? (_subscriptionsFacade = new SubscriptionsFacade(
11
                new SubscriptionDataService<ApplicationDbContext, ApplicationUser>
12
                    (HttpContext.GetOwinContext().Get<ApplicationDbContext>()),
13
                new SubscriptionProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]),
14
                new CardProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"],
15
                    new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>())),
16
                new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()),
17
                new CustomerProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]),
18
                new SubscriptionPlanDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()),
19
                new ChargeProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"])));
20
        }
21
    }
22
23
    // GET: Notes

24
    public async Task<ActionResult> Index()
25
    {
26
        var userId = User.Identity.GetUserId();
27
28
        var userNotes =
29
            await
30
                _db.Users.Where(u => u.Id == userId)
31
                    .Include(u => u.Notes)
32
                    .SelectMany(u => u.Notes)
33
                    .ToListAsync();
34
35
        return View(userNotes);
36
    }
37
38
    // GET: Notes/Details/5

39
    public async Task<ActionResult> Details(int? id)
40
    {
41
        if (id == null)
42
        {
43
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
44
        }
45
46
        var userId = User.Identity.GetUserId();
47
        ICollection<Note> userNotes = (
48
            await _db.Users.Where(u => u.Id == userId)
49
            .Include(u => u.Notes).Select(u => u.Notes)
50
            .FirstOrDefaultAsync());
51
52
        if (userNotes == null)
53
        {
54
            return HttpNotFound();
55
        }
56
57
        Note note = userNotes.FirstOrDefault(n => n.Id == id);
58
        if (note == null)
59
        {
60
            return HttpNotFound();
61
        }
62
        return View(note);
63
    }
64
65
    // GET: Notes/Create

66
    public ActionResult Create()
67
    {
68
        return View();
69
    }
70
71
    // POST: Notes/Create

72
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 

73
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.

74
    [HttpPost]
75
    [ValidateAntiForgeryToken]
76
    public async Task<ActionResult> Create([Bind(Include = "Id,Title,Text,CreatedAt")] Note note)
77
    {
78
        if (ModelState.IsValid)
79
        {
80
            if (await UserHasEnoughSpace(User.Identity.GetUserId()))
81
            {
82
                note.CreatedAt = DateTime.UtcNow;
83
84
                // The note is added to the user object so the Foreign Key is saved too

85
                var userId = User.Identity.GetUserId();
86
                var user = await this._db.Users.Where(u => u.Id == userId).FirstOrDefaultAsync();
87
                user.Notes.Add(note);
88
89
                await _db.SaveChangesAsync();
90
                return RedirectToAction("Index");
91
            }
92
            else
93
            {
94
                TempData.Add("flash", new FlashWarningViewModel("You can not add more notes, upgrade your subscription plan or delete some notes."));
95
            }
96
        }
97
98
        return View(note);
99
    }
100
101
    private async Task<bool> UserHasEnoughSpace(string userId)
102
    {
103
        var subscription = (await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId)).FirstOrDefault();
104
105
        if (subscription == null)
106
        {
107
            return false;
108
        }
109
110
        var userNotes = await _db.Users.Where(u => u.Id == userId).Include(u => u.Notes).Select(u => u.Notes).CountAsync();
111
112
        return subscription.SubscriptionPlan.GetPropertyInt("MaxNotes") > userNotes;
113
    }
114
115
    // GET: Notes/Edit/5

116
    public async Task<ActionResult> Edit(int? id)
117
    {
118
        if (id == null)
119
        {
120
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
121
        }
122
        if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id.Value))
123
        {
124
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
125
        }
126
        
127
        Note note = await _db.Notes.FindAsync(id);
128
        if (note == null)
129
        {
130
            return HttpNotFound();
131
        }
132
        return View(note);
133
    }
134
135
    // POST: Notes/Edit/5

136
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 

137
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.

138
    [HttpPost]
139
    [ValidateAntiForgeryToken]
140
    public async Task<ActionResult> Edit([Bind(Include = "Id,Title,Text,CreatedAt")] Note note)
141
    {
142
        if (ModelState.IsValid && await NoteBelongToUser(User.Identity.GetUserId(), note.Id))
143
        {
144
            _db.Entry(note).State = EntityState.Modified;
145
            await _db.SaveChangesAsync();
146
            return RedirectToAction("Index");
147
        }
148
        return View(note);
149
    }
150
151
    // GET: Notes/Delete/5

152
    public async Task<ActionResult> Delete(int? id)
153
    {
154
        if (id == null)
155
        {
156
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
157
        }
158
        if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id.Value))
159
        {
160
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
161
        }
162
163
        Note note = await _db.Notes.FindAsync(id);
164
        if (note == null)
165
        {
166
            return HttpNotFound();
167
        }
168
        return View(note);
169
    }
170
171
    // POST: Notes/Delete/5

172
    [HttpPost, ActionName("Delete")]
173
    [ValidateAntiForgeryToken]
174
    public async Task<ActionResult> DeleteConfirmed(int id)
175
    {
176
        if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id))
177
        {
178
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
179
        }
180
181
        Note note = await _db.Notes.FindAsync(id);
182
        _db.Notes.Remove(note);
183
        await _db.SaveChangesAsync();
184
        return RedirectToAction("Index");
185
    }
186
    private async Task<bool> NoteBelongToUser(string userId, int noteId)
187
    {
188
        return await _db.Users.Where(u => u.Id == userId).Where(u => u.Notes.Any(n => n.Id == noteId)).AnyAsync();
189
    }
190
191
    protected override void Dispose(bool disposing)
192
    {
193
        if (disposing)
194
        {
195
            _db.Dispose();
196
        }
197
        base.Dispose(disposing);
198
    }
199
}

This is it—we have the core functionality for our SAAS application.

8. Saving Customer's Location for European VAT Purposes

At the beginning of this year the legislation in the European Union for VAT for business supplying digital services to private consumers changed. The main difference is that businesses have to charge VAT to private customers, not business customers with a valid VAT number, according to the country in the EU in which they are based. To validate in which country they're based we need to keep a record of at least two of these forms:

  • the billing address of the customer
  • the Internet Protocol (IP) address of the device used by the customer
  • customer’s bank details
  • the country code of the SIM card used by the customer
  • the location of the customer’s fixed land line through which the service is supplied
  • other commercially relevant information (for example, product coding information which electronically links the sale to a particular jurisdiction)

For this reason we are going to geo-locate the user IP address, to save it along with the billing address and credit card country.

8.1 IP Address Geo-Location

For geo-location, I am going to use Maxmind GeoLite2. It's a free database that gives us the country where an IP is located.

Maxmind Geo Lite2Maxmind Geo Lite2Maxmind Geo Lite2

Download, and add the database to App_Data, as you can see in the photo:

Adding The DatabaseAdding The DatabaseAdding The Database
Install from NuGet MaxMind.GeoIP2.
NuGet PackagesNuGet PackagesNuGet Packages

Create Extensions/GeoLocationHelper.cs.

1
public static class GeoLocationHelper
2
{
3
    // ReSharper disable once InconsistentNaming

4
    /// <summary>

5
    /// Gets the country ISO code from IP.

6
    /// </summary>

7
    /// <param name="ipAddress">The ip address.</param>

8
    /// <returns></returns>

9
    public static string GetCountryFromIP(string ipAddress)
10
    {
11
        string country;
12
        try
13
        {
14
            using (
15
                var reader =
16
                    new DatabaseReader(HttpContext.Current.Server.MapPath("~/App_Data/GeoLite2-Country.mmdb")))
17
            {
18
                var response = reader.Country(ipAddress);
19
                country = response.Country.IsoCode;
20
            }
21
        }
22
        catch (Exception ex)
23
        {
24
            country = null;
25
        }
26
27
        return country;
28
    }
29
30
    /// <summary>

31
    /// Selects the list countries.

32
    /// </summary>

33
    /// <param name="country">The country.</param>

34
    /// <returns></returns>

35
    public static List<SelectListItem> SelectListCountries(string country)
36
    {
37
        var getCultureInfo = CultureInfo.GetCultures(CultureTypes.SpecificCultures);
38
        var countries =
39
            getCultureInfo.Select(cultureInfo => new RegionInfo(cultureInfo.LCID))
40
                .Select(getRegionInfo => new SelectListItem
41
                {
42
                    Text = getRegionInfo.EnglishName,
43
                    Value = getRegionInfo.TwoLetterISORegionName,
44
                    Selected = country == getRegionInfo.TwoLetterISORegionName
45
                }).OrderBy(c => c.Text).DistinctBy(i => i.Text).ToList();
46
        return countries;
47
    }
48
49
    public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
50
    {
51
        var seenKeys = new HashSet<TKey>();
52
        return source.Where(element => seenKeys.Add(keySelector(element)));
53
    }
54
}

There are two methods implemented in this static class:

  • GetCountryFromIP: Returns the country ISO Code given an IP Address.
  • SelectListCountries: Returns a list of countries to use in a drop-down field. It has the country ISO Code as a value for each country and the country name to be displayed.

8.2 Saving Customer Country on Registration

In the action Register from AccountController, when creating the user, save the IP and the country the IP belongs to:

1
var userIP = GeoLocation.GetUserIP(Request);
2
var user = new ApplicationUser
3
{
4
    UserName = model.Email, 
5
    Email = model.Email,
6
    RegistrationDate = DateTime.UtcNow,
7
    LastLoginTime = DateTime.UtcNow,
8
    IPAddress = userIP,
9
    IPAddressCountry = GeoLocationHelper.GetCountryFromIP(userIP),
10
};

Also, when we create the subscription in Stripe, we need to pass the Tax Percentage for this customer. We do that a few lines after creating the user:

1
// Create Stripe user

2
var taxPercent = EuropeanVat.Countries.ContainsKey(user.IPAddressCountry) ? 
3
    EuropeanVat.Countries[user.IPAddressCountry] : 0;
4
await SubscriptionsFacade.SubscribeUserAsync(user, model.SubscriptionPlan, taxPercent: taxPercent);

By default, if a user is based in the European Union, I'm setting the tax percentage to that subscription. The rules are a bit more complex than that, but summarizing:

  • If your business is registered in an EU country, you always charge VAT to customers in your country.
  • If your business is registered in an EU country, you only charge VAT to the customers that are in other EU countries, and are not VAT-registered business.
  • If your business is registered outside the EU, you only charge VAT to customers that are not businesses with a valid VAT number.

8.3 Adding a Billing Address to our Model

At the moment we are not allowing to our customers to save a Billing Address, and their VAT number if they are an EU VAT registered business. In that case, we need to change their tax percentage to 0.

SAAS Ecom provides the BillingAddress class, but it's not attached to any entity of the model. The main reason for this is that in some SAAS applications it might make sense to assign this to an Organization class if multiple users have access to the same account. If this is not the case, as in our sample, we can safely add that relationship to the ApplicationUser class:

1
public class ApplicationUser : SaasEcomUser
2
{
3
    public virtual ICollection<Note> Notes { get; set; }
4
5
    public virtual BillingAddress BillingAddress { get; set; }
6
7
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
8
    {
9
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType

10
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
11
        // Add custom user claims here

12
        return userIdentity;
13
    }
14
}

As each time that we modify the model, we need to add a database migration, open Tools > NuGet Package Manager > Package Manager Console:

1
PM> add-migration BillingAddressAddedToUser

And this is the migration class that we get:

1
public partial class BillingAddressAddedToUser : DbMigration
2
    {
3
        public override void Up()
4
        {
5
            AddColumn("dbo.AspNetUsers", "BillingAddress_Name", c => c.String());
6
            AddColumn("dbo.AspNetUsers", "BillingAddress_AddressLine1", c => c.String());
7
            AddColumn("dbo.AspNetUsers", "BillingAddress_AddressLine2", c => c.String());
8
            AddColumn("dbo.AspNetUsers", "BillingAddress_City", c => c.String());
9
            AddColumn("dbo.AspNetUsers", "BillingAddress_State", c => c.String());
10
            AddColumn("dbo.AspNetUsers", "BillingAddress_ZipCode", c => c.String());
11
            AddColumn("dbo.AspNetUsers", "BillingAddress_Country", c => c.String());
12
            AddColumn("dbo.AspNetUsers", "BillingAddress_Vat", c => c.String());
13
        }
14
        
15
        public override void Down()
16
        {
17
            DropColumn("dbo.AspNetUsers", "BillingAddress_Vat");
18
            DropColumn("dbo.AspNetUsers", "BillingAddress_Country");
19
            DropColumn("dbo.AspNetUsers", "BillingAddress_ZipCode");
20
            DropColumn("dbo.AspNetUsers", "BillingAddress_State");
21
            DropColumn("dbo.AspNetUsers", "BillingAddress_City");
22
            DropColumn("dbo.AspNetUsers", "BillingAddress_AddressLine2");
23
            DropColumn("dbo.AspNetUsers", "BillingAddress_AddressLine1");
24
            DropColumn("dbo.AspNetUsers", "BillingAddress_Name");
25
        }
26
    }

To create these changes in the database, we execute in the Package Manager Console: 

1
PM> update-database

One more detail that we need to fix is that in AccountController > Register, we need to set a default billing address as it's a non-nullable field.

1
var user = new ApplicationUser
2
{
3
    UserName = model.Email, 
4
    Email = model.Email,
5
    RegistrationDate = DateTime.UtcNow,
6
    LastLoginTime = DateTime.UtcNow,
7
    IPAddress = userIP,
8
    IPAddressCountry = GeoLocationHelper.GetCountryFromIP(userIP),
9
    BillingAddress = new BillingAddress()
10
};

In the billing page, we need to display the Billing Address for the customer if it has been added, and also allow our customers to edit it. First, we need to modify the action Index from BillingController to pass the billing address to the view:

1
public async Task<ViewResult> Index()
2
{
3
    var userId = User.Identity.GetUserId();
4
5
    ViewBag.Subscriptions = await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId);
6
    ViewBag.PaymentDetails = await SubscriptionsFacade.DefaultCreditCard(userId);
7
    ViewBag.Invoices = await InvoiceDataService.UserInvoicesAsync(userId);
8
    ViewBag.BillingAddress = (await UserManager.FindByIdAsync(userId)).BillingAddress;
9
10
    return View();
11
}

To display the address, we just need to edit the view "Billing/Index.cshtml", and add the view partial provided by SAAS Ecom for that:

1
<h2>Billing</h2>
2
<br />
3
@Html.Partial("_Subscriptions", (List<Subscription>)ViewBag.Subscriptions)
4
<br/>
5
@Html.Partial("_PaymentDetails", (CreditCard)ViewBag.PaymentDetails)
6
<br />
7
@Html.Partial("_BillingAddress", (BillingAddress)ViewBag.BillingAddress)
8
<br />
9
@Html.Partial("_Invoices", (List<Invoice>)ViewBag.Invoices)

Now, if we navigate to Billing we can see the new section:

Updated Billing TemplateUpdated Billing TemplateUpdated Billing Template

The next step is on the BillingController > BillingAddress action, we need to pass the Billing address to the view. Because we need to get the user's two-letter ISO country code, I've added a dropdown to select the country, which defaults to the country that the user IP belongs to:

1
public async Task<ViewResult> BillingAddress()
2
{
3
    var model = (await UserManager.FindByIdAsync(User.Identity.GetUserId())).BillingAddress;
4
5
    // List for dropdown country select

6
    var userCountry = (await UserManager.FindByIdAsync(User.Identity.GetUserId())).IPAddressCountry;
7
    ViewBag.Countries = GeoLocationHelper.SelectListCountries(userCountry);
8
9
    return View(model);
10
}

When the user submits the form, we need to save the billing address and update the tax percent if it's needed:

1
[HttpPost]
2
public async Task<ActionResult> BillingAddress(BillingAddress model)
3
{
4
    if (ModelState.IsValid)
5
    {
6
        var userId = User.Identity.GetUserId();
7
8
        // Call your service to save the billing address

9
        var user = await UserManager.FindByIdAsync(userId);
10
        user.BillingAddress = model;
11
        await UserManager.UpdateAsync(user);
12
        
13
        // Model Country has to be 2 letter ISO Code

14
        if (!string.IsNullOrEmpty(model.Vat) && !string.IsNullOrEmpty(model.Country) &&
15
            EuropeanVat.Countries.ContainsKey(model.Country))
16
        {
17
            await UpdateSubscriptionTax(userId, 0);
18
        }
19
        else if (!string.IsNullOrEmpty(model.Country) && EuropeanVat.Countries.ContainsKey(model.Country))
20
        {
21
            await UpdateSubscriptionTax(userId, EuropeanVat.Countries[model.Country]);
22
        }
23
24
        TempData.Add("flash", new FlashSuccessViewModel("Your billing address has been saved."));
25
26
        return RedirectToAction("Index");
27
    }
28
29
    return View(model);
30
}
31
32
private async Task UpdateSubscriptionTax(string userId, decimal tax)
33
{
34
    var user = await UserManager.FindByIdAsync(userId);
35
    var subscription = (await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId)).FirstOrDefault();
36
    if (subscription != null && subscription.TaxPercent != tax)
37
    {
38
        await SubscriptionsFacade.UpdateSubscriptionTax(user, subscription.StripeId, tax);
39
    }
40
}

This is what the form to add or edit a billing address looks like:

Adding a Billing AddressAdding a Billing AddressAdding a Billing Address

After adding the address, I get redirected back to the billing area:

Billing AreaBilling AreaBilling Area

As you can see in the screenshot above, because I set my country to United Kingdom, and I didn't enter a VAT number, 20% VAT is added to the monthly price. The code showed here is assuming that you are a non-EU-based company. If that's the case, you need to handle the case where your customer is in your country, and regardless of whether they have VAT or not, you'll have to charge VAT.

9. Deploy to Azure Websites (Web Hosting + SSL Free, SQL Database $5 Per Month)

9.1 Deploying the Website

Our SAAS project is ready to go live, and I've chosen Azure as the hosting platform. If you don't have an account yet, you can get a free trial for a month. We can deploy our app from Git (GitHub or BitBucket) on every commit if we like. I'm going to show you here how to deploy from Visual Studio 2013. In the solution explorer, right click on the project My Notes and select Publish from the context menu. The Publish Web wizard opens.

Publishing WizardPublishing WizardPublishing Wizard

Select Microsoft Azure Websites and click New.

Microsoft Azure WebsitesMicrosoft Azure WebsitesMicrosoft Azure Websites
Creating The SiteCreating The SiteCreating The Site

Fill in the details for your website and click Create. When your website has been created, you'll see this. Click Next.

Publishing The SitePublishing The SitePublishing The Site
Publishing To The WebPublishing To The WebPublishing To The Web

In this step you can add the connection string for your Database if you have it, or you can add it later from the management portal. Click Next.

Previewing The PublishingPreviewing The PublishingPreviewing The Publishing

Now, if we click Publish, Visual Studio will upload the website to Azure.

9.2 Deploying the Database

To create the database, you have to go to Azure Management Portal, select Browse, and then Data + Storage > SQL Database. Fill in the form to create your database.

Deploying The DatabaseDeploying The DatabaseDeploying The Database

Once the database is created, select Open in Visual Studio and accept to add an exception to the firewall.

Adding a Firewall ExceptionAdding a Firewall ExceptionAdding a Firewall Exception

Your database will be open in the SQL Server Object Explorer from Visual Studio. As you can see there are no tables yet:

SQL Object ExplorerSQL Object ExplorerSQL Object Explorer

To generate a SQL Script to create the tables in the database, open Package Manager Console in Visual Studio, and type:

1
PM> update-database -SourceMigration:0 -Script

Copy the script, and back in SQL Server Object Explorer, right-click on your database, and select New Query. Paste the script, and execute it.

Creating the SQL ScriptCreating the SQL ScriptCreating the SQL Script

This script doesn't include the data that we were inserting in the database from the Seed method. We need to create a script manually to add that data to the database:

1
INSERT INTO [dbo].[SubscriptionPlans] ([Id],[Name],[Price],[Currency],[Interval],[TrialPeriodInDays],[Disabled])
2
     VALUES('basic_monthly', 'Basic', 10, 'USD', 1, 30, 0)
3
INSERT INTO [dbo].[SubscriptionPlans] ([Id],[Name],[Price],[Currency],[Interval],[TrialPeriodInDays],[Disabled])
4
     VALUES('professional_monthly', 'Professional', 20, 'USD', 1, 30, 0)
5
INSERT INTO [dbo].[SubscriptionPlans] ([Id],[Name],[Price],[Currency],[Interval],[TrialPeriodInDays],[Disabled])
6
     VALUES('business_monthly', 'Business', 30, 'USD', 1, 30, 0)
7
8
INSERT INTO [dbo].[SubscriptionPlanProperties] ([Key],[Value],[SubscriptionPlan_Id])
9
     VALUES ('MaxNotes', '100', 'basic_monthly')
10
INSERT INTO [dbo].[SubscriptionPlanProperties] ([Key],[Value],[SubscriptionPlan_Id])
11
     VALUES ('MaxNotes', '10000', 'professional_monthly')
12
INSERT INTO [dbo].[SubscriptionPlanProperties] ([Key],[Value],[SubscriptionPlan_Id])
13
     VALUES ('MaxNotes', '1000000', 'business_monthly')

At this point My Notes SAAS is live. I have configured Stripe test API keys, so you can use test credit card details for testing if you like.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.