Advertisement
  1. Code
  2. Web Development

Erstellen eines mit Notizen versehenen SAAS mit ASP.NET MVC 5

Scroll to top
Read Time: 35 min

German (Deutsch) translation by Federicco Ancie (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

1. Einleitung

In diesem Tutorial zeige ich Ihnen, wie Sie ein Software-as-a-Service (SaaS) Minimum Viable Product (MVP) erstellen. Der Einfachheit halber wird die Software unseren Kunden ermöglichen, eine Liste mit Notizen zu speichern.

Ich werde drei Abonnements anbieten: Der Basic-Plan hat ein Limit von 100 Notizen pro Benutzer, der Professional-Plan ermöglicht es Kunden, bis zu 10.000 Notizen zu speichern, und der Business-Plan erlaubt eine Million Notizen. Die Pläne werden jeweils 10, 20 und 30 US-Dollar pro Monat kosten. Um Zahlungen von unseren Kunden zu erhalten, werde ich Stripe als Zahlungsgateway verwenden und die Website wird in Azure bereitgestellt.

2. Einrichtung

2.1 Streifen

Stripe hat sich in kürzester Zeit zu einem sehr bekannten Payment Gateway entwickelt, vor allem wegen seines entwicklerfreundlichen Ansatzes mit einfachen und gut dokumentierten APIs. Auch die Preisgestaltung ist sehr klar: 2,9% pro Transaktion + 30 Cent. Keine Einrichtungsgebühren oder versteckte Gebühren.

Kreditkartendaten sind ebenfalls sehr sensible Daten, und um diese Daten auf meinem Server empfangen und speichern zu können, muss ich PCI-konform sein. Da dies für die meisten kleinen Unternehmen keine leichte oder schnelle Aufgabe ist, gehen viele Zahlungsgateways vor: Sie zeigen die Bestelldetails an und wenn der Kunde dem Kauf zustimmt, leiten Sie den Kunden auf eine Seite des Zahlungsgateways (Bank , PayPal usw.) und leiten den Kunden dann zurück.

Stripe hat einen schöneren Ansatz für dieses Problem. Sie bieten eine JavaScript-API, sodass wir die Kreditkartennummer direkt vom Front-End an die Server von Stripe senden können. Sie geben ein Token zur einmaligen Verwendung zurück, das wir in unserer Datenbank speichern können. Jetzt benötigen wir für unsere Website nur noch ein SSL-Zertifikat, das wir schnell ab ca. 5 US-Dollar pro Jahr erwerben können.

Melden Sie sich jetzt für ein Stripe-Konto an, da Sie es zum Aufladen Ihrer Kunden benötigen.

2.2 Azure

Als Entwickler möchte ich mich nicht mit Dev-Ops-Aufgaben und der Verwaltung von Servern befassen, wenn ich nicht muss. Azure-Websites ist meine Wahl zum Hosten, da es sich um eine vollständig verwaltete Platform-as-a-Service handelt. Es ermöglicht mir die Bereitstellung aus Visual Studio oder Git, ich kann es leicht skalieren, wenn mein Dienst erfolgreich ist, und ich kann mich auf die Verbesserung meiner Anwendung konzentrieren. Sie bieten Neukunden 200 US-Dollar an, die sie im ersten Monat für alle Azure-Dienste ausgeben können. Das ist genug, um die Dienste zu bezahlen, die ich für dieses MVP nutze. Registrieren Sie sich für Azure.

2.3 Mandrill und Mailchimp: Transaktions-E-Mail

Das Versenden von E-Mails aus unserer Anwendung scheint keine sehr komplexe Aufgabe zu sein, aber ich möchte überwachen, wie viele E-Mails erfolgreich zugestellt werden, und auch responsive Vorlagen einfach entwerfen. Das bietet Mandrill und lässt uns bis zu 12.000 E-Mails pro Monat kostenlos versenden. Mandrill wurde von MailChimp entwickelt, damit sie über das Geschäft mit dem Versenden von E-Mails Bescheid wissen. Außerdem können wir unsere Vorlagen aus MailChimp erstellen, sie nach Mandrill exportieren und mit unseren Vorlagen E-Mails aus unserer App senden. Melden Sie sich für Mandrill an und melden Sie sich für MailChimp an.

2.4 Visual Studio 2013 Community-Edition

Zu guter Letzt benötigen wir Visual Studio, um unsere Anwendung zu schreiben. Diese Edition, die erst vor wenigen Monaten auf den Markt kam, ist völlig kostenlos und entspricht in etwa Visual Studio Professional. Sie können es hier herunterladen, und das ist alles, was wir brauchen, damit wir uns jetzt auf die Entwicklung konzentrieren können.

3. Erstellen der Website

Als erstes müssen wir Visual Studio 2013 öffnen. Erstellen Sie eine neue ASP.NET-Webanwendung:

  • Gehen Sie zu Datei > Neues Projekt und wählen Sie ASP.NET-Webanwendung.
  • Wählen Sie im Dialogfeld ASP.NET-Vorlage die MVC-Vorlage aus, und wählen Sie Individuelle Benutzerkonten aus.
New ASPNET MVC ProjectNew ASPNET MVC ProjectNew ASPNET MVC Project

Dieses Projekt erstellt eine Anwendung, bei der sich ein Benutzer anmelden kann, indem er ein Konto auf der Website registriert. Die Website ist mit Bootstrap gestaltet, und ich werde den Rest der App mit Bootstrap erstellen. Wenn Sie in Visual Studio F5 drücken, um die Anwendung auszuführen, sehen Sie Folgendes:

Example HomepageExample HomepageExample Homepage

Dies ist die Standard-Landingpage, und diese Seite ist einer der wichtigsten Schritte, um unsere Besucher in Kunden zu verwandeln. Wir müssen das Produkt erklären, den Preis für jeden Plan zeigen und ihnen die Möglichkeit bieten, sich für eine kostenlose Testversion anzumelden. Für diese Anwendung erstelle ich drei verschiedene Abonnementpläne:

  • Basis: 10 $ pro Monat
  • Professionell: $20 pro Monat
  • Geschäft: 30 $ pro Monat

3.1 Landingpage

Wenn Sie Hilfe beim Erstellen einer Zielseite benötigen, können Sie ThemeForest besuchen und eine Vorlage kaufen. Für dieses Beispiel verwende ich eine kostenlose Vorlage, und Sie können das Endergebnis auf dem Foto unten sehen.

Landing PageLanding PageLanding Page

3.2 Registrierungsseite

Auf der Website, die wir im vorherigen Schritt erstellt haben, erhalten wir auch eine Registrierungsformularvorlage. Wenn Sie von der Zielseite zu Preise navigieren und auf Kostenlose Testversion klicken, navigieren Sie zur Registrierungsseite. Dies ist das Standarddesign:

Registration PageRegistration PageRegistration Page

Wir benötigen hier nur ein zusätzliches Feld, um den Abonnementplan zu identifizieren, dem der Benutzer beitritt. Wenn Sie in der Navigationsleiste des Fotos sehen können, übergebe ich das als GET-Parameter. Dazu generiere ich das Markup für die Links auf der Landingpage mit dieser Codezeile:

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

Um den Abonnementplan an das Back-End zu binden, muss ich die Klasse RegisterViewModel ändern und die neue Eigenschaft hinzufügen.

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
}

Ich muss auch AccountController.cs bearbeiten und das Aktionsregister ändern, um den Plan zu erhalten:

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

Jetzt muss ich die Plankennung in einem versteckten Feld im Registrierungsformular rendern:

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

Der letzte Schritt besteht darin, den Benutzer für den Plan zu abonnieren, aber dazu kommen wir etwas später. Außerdem aktualisiere ich das Design des Anmeldeformulars.

Updated Registration FormUpdated Registration FormUpdated Registration Form

3.3 Login-Seite

Login PageLogin PageLogin Page

Im Template bekommen wir auch eine Login-Seite und Action-Controller implementiert. Das einzige, was ich tun muss, ist, dass es hübscher aussieht.

Updated Login PageUpdated Login PageUpdated Login Page

3.4 Passwort vergessen

Schauen Sie sich den vorherigen Screenshot noch einmal an und Sie werden feststellen, dass ich ein "Passwort vergessen?" hinzugefügt habe. Verknüpfung. Dies ist bereits in der Vorlage implementiert, aber standardmäßig auskommentiert. Ich mag das Standardverhalten nicht, bei dem der Benutzer die E-Mail-Adresse bestätigen muss, um das Passwort zurücksetzen zu können. Lassen Sie uns diese Einschränkung aufheben. Bearbeiten Sie in der Datei AccountController.cs die Aktion 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
}

Der Code zum Senden der E-Mail mit dem Link zum Zurücksetzen des Passworts ist auskommentiert. Ich werde etwas später zeigen, wie man diesen Teil implementiert. Jetzt bleibt nur noch das Design der Seiten zu aktualisieren:

  • ForgotPassword.cshtml: Formular, das dem Benutzer angezeigt wird, um seine E-Mail-Adresse einzugeben.
  • ForgotPasswordConfirmation.cshtml: Bestätigungsnachricht, nachdem der Link zum Zurücksetzen per E-Mail an den Benutzer gesendet wurde.
  • ResetPassword.cshtml: Formular zum Zurücksetzen des Passworts nach dem Navigieren zum Zurücksetzen-Link aus der E-Mail.
  • ResetPasswordConfirmation.cshtml: Bestätigungsmeldung nach dem Zurücksetzen des Passworts.
Forgot Password PageForgot Password PageForgot Password Page

4. ASP.NET-Identität 2.0

ASP.NET Identity ist eine ziemlich neue Bibliothek, die auf der Annahme basiert, dass sich Benutzer nicht mehr nur mit Benutzernamen und Kennwort anmelden. Die OAuth-Integration, mit der sich Benutzer über soziale Kanäle wie Facebook, Twitter und andere anmelden können, ist jetzt sehr einfach. Außerdem kann diese Bibliothek mit Web-API und SignalR verwendet werden.

Andererseits kann die Persistenzschicht ersetzt werden, und es ist einfach, verschiedene Speichermechanismen wie NoSQL-Datenbanken einzubinden. Für die Zwecke dieser Anwendung verwende ich Entity Framework und SQL Server.

Das gerade erstellte Projekt enthält die folgenden drei NuGet-Pakete für ASP.NET Identity:

  • Microsoft.AspNet.Identity.Core: Dieses Paket enthält die Kernschnittstellen für ASP.NET Identity.
  • Microsoft.AspNet.Identity.EntityFramework: Dieses Paket enthält die Entity Framework-Implementierung der vorherigen Bibliothek. Die Daten werden in SQL Server gespeichert.
  • Microsoft.AspNet.Identity.Owin: Dieses Paket verbindet die Middleware-OWIN-Authentifizierung mit ASP.NET Identity.

Die Hauptkonfiguration für Identity befindet sich in App_Start/IdentityConfig.cs. Dies ist der Code, der Identity initialisiert.

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
}

Wie Sie im Code sehen können, ist es ziemlich einfach, die Validatoren und Passwort-Validatoren der Benutzer zu konfigurieren, und auch die Zwei-Faktor-Authentifizierung kann aktiviert werden. Für diese Anwendung verwende ich eine Cookie-basierte Authentifizierung. Das Cookie wird vom Framework generiert und ist verschlüsselt. Auf diese Weise können wir horizontal skalieren und weitere Server hinzufügen, wenn unsere Anwendung dies benötigt.

5. E-Mails mit Mandrill senden

Sie können MailChimp verwenden, um E-Mail-Vorlagen zu entwerfen, und Mandrill, um E-Mails aus Ihrer Anwendung zu senden. Zunächst müssen Sie Ihr Mandrill-Konto mit Ihrem MailChimp-Konto verknüpfen:

  • Melden Sie sich bei MailChimp an, klicken Sie im rechten Bereich auf Ihren Benutzernamen und wählen Sie Konto aus der Dropdown-Liste.
  • Klicken Sie auf Integrationen und suchen Sie die Option Mandrill in der Liste der Integrationen.
  • Klicken Sie darauf, um die Integrationsdetails anzuzeigen, und klicken Sie auf die Schaltfläche Verbindung autorisieren. Sie werden zu Mandrill weitergeleitet. Lassen Sie die Verbindung zu und die Integration wird abgeschlossen.
Setting Up MandrillSetting Up MandrillSetting Up Mandrill

5.1 E-Mail-Vorlage „Willkommen bei meinen Notizen“ erstellen

Navigieren Sie in MailChimp zu Vorlagen und klicken Sie auf Vorlage erstellen.

Creating a TemplateCreating a TemplateCreating a Template

Wählen Sie nun eine der von MailChimp angebotenen Vorlagen aus. Ich habe das erste gewählt:

Choosing a TemplateChoosing a TemplateChoosing a Template

Im Vorlageneditor ändern wir den Inhalt nach Belieben. Beachten Sie, wie Sie unten sehen können, dass wir Variablen verwenden können. Das Format ist *|VARIABLE_NAME|*. Anhand des Codes legen wir diese für jeden Kunden fest. Wenn Sie fertig sind, klicken Sie unten rechts auf Speichern und beenden.

Populating the TemplatePopulating the TemplatePopulating the Template

Klicken Sie in der Liste Vorlagen auf der rechten Seite auf Bearbeiten und wählen Sie An Mandrill senden. Nach einigen Sekunden erhalten Sie eine Bestätigungsmeldung.

Sending To MandrillSending To MandrillSending To Mandrill

Um zu bestätigen, dass die Vorlage exportiert wurde, navigieren Sie zu Mandrill und melden Sie sich an. Wählen Sie Ausgehend aus dem linken Menü und dann Vorlagen aus dem oberen Menü. Im Bild unten sehen Sie, dass die Vorlage exportiert wurde.

Outbound DataOutbound DataOutbound Data

Wenn Sie auf den Namen der Vorlage klicken, werden weitere Informationen zur Vorlage angezeigt. Das Feld "Template Slug" ist die Textkennung, die wir in unserer Anwendung verwenden, um Mandrill API mitzuteilen, welche Vorlage wir für die von uns gesendete E-Mail verwenden möchten.

Template MarkupTemplate MarkupTemplate Markup

Ich überlasse es Ihnen als Übung, eine Vorlage "Passwort zurücksetzen" zu erstellen.

Reset Password TemplateReset Password TemplateReset Password Template

5.2 Senden von E-Mails aus meinen Notizen

Sending EmailsSending EmailsSending Emails

Installieren Sie zunächst Mandrill von NuGet. Fügen Sie anschließend Ihren Mandrill-API-Schlüssel zu den Web.config-App-Einstellungen hinzu. Öffnen Sie nun App_Start/IdentityConfig.cs und Sie sehen die ausstehende Implementierung der Klasse EmailService:

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
}

Obwohl diese Klasse nur die Methode SendAsync hat, werden wir neue Methoden implementieren, da wir zwei verschiedene Vorlagen haben (Welcome Email Template und Reset Password Template). Die endgültige Implementierung sieht so aus.

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
}

So senden Sie eine E-Mail über die Mandrill-API:

  1. E-Mail-Nachricht erstellen.
  2. Legen Sie die Werte der Nachrichtenvariablen fest.
  3. Senden Sie eine E-Mail mit Angabe des Vorlagen-Slugs.

In AccountController -> Aktion registrieren ist dies das Code-Snippet zum Versenden der Willkommens-E-Mail:

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

In AccountController -> ForgotPassword-Aktion ist dies der Code zum Senden der E-Mail:

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. Integration von SAAS Ecom für die Abrechnung

Eine wichtige Sache bei SAAS-Anwendungen ist die Abrechnung. Wir müssen eine Möglichkeit haben, unseren Kunden regelmäßig, in diesem Beispiel monatlich, Gebühren zu berechnen. Da dieser Teil viel Arbeit erfordert, aber dem von uns verkauften Produkt nichts Wertvolles hinzufügt, werden wir die zu diesem Zweck erstellte Open-Source-Bibliothek SAAS Ecom verwenden.

6.1 Datenmodell: Entity Framework Code First

SAAS Ecom ist von Entity Framework Code First abhängig. Für diejenigen unter Ihnen, die damit nicht vertraut sind, können Sie sich mit Entity Framework Code First auf das Erstellen von C#-POCO-Klassen konzentrieren, sodass Entity Framework die Klassen Datenbanktabellen zuordnen kann. Es folgt der Idee der Konvention gegenüber der Konfiguration, aber Sie können bei Bedarf weiterhin Zuordnungen, Fremdschlüssel usw. angeben.

Um SAAS Ecom zu unserem Projekt hinzuzufügen, installieren Sie einfach die Abhängigkeit mit NuGet. Die Bibliothek ist in zwei Pakete aufgeteilt: SaasEcom.Core, das die Geschäftslogik enthält, und SaasEcom.FrontEnd, das einige Ansichtshelfer zur Verwendung in einer MVC-Anwendung enthält. Fahren Sie fort und installieren Sie SaasEcom.FrontEnd.

Installing SaasEcomInstalling SaasEcomInstalling SaasEcom

Sie können sehen, dass Ihrer Lösung einige Dateien hinzugefügt wurden:

  • Inhalt/Karten-Icons: Kreditkarten-Icons zur Anzeige im Abrechnungsbereich
  • Controller/BillingController: Hauptcontroller
  • Controller/StripeWebhooksController: Stripe-Webhooks
  • Scripts/saasecom.card.form.js: Skript zum Hinzufügen einer Kreditkarte zu Stripe
  • Aufrufe/Abrechnung: Aufrufe und Teilansichten
Files Added To The ProjectFiles Added To The ProjectFiles Added To The Project

Es sind noch einige Schritte erforderlich, um SAAS Ecom zu integrieren, also besorgen Sie sich Ihre Stripe-API-Schlüssel und fügen Sie sie zu Web.config hinzu.

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

Wenn Sie versuchen zu kompilieren, werden Fehler angezeigt:

Öffnen Sie die Datei Models/IdentityModels.cs, und lassen Sie dann die Klasse ApplicationUser von SaasEcomUser erben.

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

Öffnen Sie die Datei Models/IdentityModels.cs, und dann sollte Ihre Klasse ApplicationDbContext von SaasEcomDbContext<ApplicationUser> erben.

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

Da ApplicationUser von SaasEcomUser erbt, besteht das Standardverhalten für Entity Framework darin, zwei Tabellen in der Datenbank zu erstellen. Da wir das in diesem Fall nicht benötigen, müssen wir diese Methode der Klasse ApplicationDbContext hinzufügen, um anzugeben, dass sie nur eine Tabelle verwenden soll:

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

Da wir gerade den DbContext aktualisiert haben, muss auch die Datenbank aktualisiert werden, damit er von SaasEcomDbContext erbt. Aktivieren Sie dazu Codemigrationen und aktualisieren Sie die Datenbank, indem Sie den NuGet-Paket-Manager über das Menü Extras > NuGet-Paket-Manager > Paket-Manager-Konsole öffnen:

1
PM > enable-migrations

2
PM > add-migration Initial

3
PM > update-database

Wenn Sie beim Ausführen von update-database einen Fehler erhalten, befindet sich die Datenbank (SQL Compact) in Ihrem AppData-Ordner. Öffnen Sie die Datenbank, löschen Sie alle darin enthaltenen Tabellen und führen Sie update-database erneut aus.

6.2 Erstellen der Abonnementpläne in Stripe und Datenbank

Der nächste Schritt im Projekt besteht darin, Stripe zu integrieren, um unseren Kunden monatliche Gebühren zu berechnen, und dafür müssen wir die Abonnementpläne und Preise in Stripe erstellen. Melden Sie sich also bei Ihrem Stripe-Dashboard an und erstellen Sie Ihre Abonnementpläne, wie Sie auf den Bildern sehen können.

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

Nachdem wir die Abonnementpläne in Stripe erstellt haben, fügen wir sie der Datenbank hinzu. Wir tun dies, damit wir die Stripe-API nicht jedes Mal abfragen müssen, wenn wir Informationen zu Abonnementplänen benötigen.

Außerdem können wir spezifische Eigenschaften für jeden Plan speichern. In diesem Beispiel speichere ich als Eigenschaft jedes Plans die Anzahl der Notizen, die ein Benutzer speichern kann: 100 Notizen für den Basisplan, 10.000 für den Profiplan und 1 Million für den Geschäftsplan. Wir fügen diese Informationen der Seed-Methode hinzu, die jedes Mal ausgeführt wird, wenn die Datenbank aktualisiert wird, wenn wir update-database über die NuGet Package Manager-Konsole ausführen.

Öffnen Sie die Datei Migrations/Configuration.cs und fügen Sie diese Methode hinzu:

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 Abonnieren eines Kunden für einen Plan bei der Anmeldung

Als nächstes müssen wir sicherstellen, dass wir jedes Mal, wenn sich ein Benutzer für unsere App registriert, den Benutzer auch in Stripe mithilfe seiner API erstellen. Dazu verwenden wir die SAAS Ecom API, und wir müssen nur die Aktion Register on AccountController bearbeiten und diese Zeilen hinzufügen, nachdem der Benutzer in der Datenbank erstellt wurde:

1
// Create Stripe user

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

Die Methode SubscribeUserAsync abonniert den Benutzer für den Plan in Stripe, und wenn der Benutzer noch nicht in Stripe vorhanden ist, wird er ebenfalls erstellt. Dies ist nützlich, wenn Sie ein Freemium-SAAS haben und Benutzer in Stripe nur erstellen, wenn sie einen kostenpflichtigen Plan haben. Eine weitere kleine Änderung in der Registrierungsaktion von AccountController besteht darin, das RegistrationDate und LastLoginTime beim Erstellen des Benutzers zu speichern:

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);

Da wir die Abhängigkeit SubscriptionsFacade von SAAS Ecom benötigen, fügen Sie sie als Eigenschaft zu Account Controller hinzu:

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
}

Sie können die Instanziierung mit Dependency Injection vereinfachen, aber dies kann in einem anderen Artikel behandelt werden.

6.4 Abrechnungsansichten integrieren

Als wir SAAS Ecom zum Projekt hinzugefügt haben, wurden auch einige Teilansichten hinzugefügt. Sie verwenden das Hauptlayout _Layout.cshtml, aber dieses Layout wird von der Zielseite verwendet. Wir müssen ein anderes Layout für den Webanwendungsbereich oder das Kunden-Dashboard hinzufügen.

Ich habe eine sehr ähnliche Version der _Layout.cshtml erstellt, die erstellt wird, wenn Sie ein neues MVC-Projekt in Visual Studio hinzufügen. Sie können die _DashboardLayout.cshtml in GitHub sehen.

Die Hauptunterschiede sind, dass ich font-awesome und einen Bereich hinzugefügt habe, um Bootstrap-Benachrichtigungen anzuzeigen, wenn sie vorhanden sind:

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

Legen Sie für die Ansichten im Ordner Ansichten/Billing das Layout auf _DashboardLayout fest, andernfalls wird das Standardlayout _Layout.cshtml verwendet. Gehen Sie für Ansichten im Ordner Ansichten/Verwalten genauso vor:

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

Ich habe "DashboardLayout" leicht modifiziert, um einige Stile von der Hauptwebsite zu verwenden, und es sieht so aus, nachdem ich mich angemeldet und zum Abschnitt Abrechnung navigiert habe:

Billing TemplateBilling TemplateBilling Template

Im Abrechnungsbereich kann ein Kunde ein Abonnement kündigen oder upgraden / downgraden. Fügen Sie Zahlungsdetails mithilfe der Stripe-JavaScript-API hinzu, damit wir nicht PCI-konform sein müssen und nur SSL im Server benötigen, um Zahlungen von unseren Kunden entgegenzunehmen.

Payment TemplatePayment TemplatePayment Template

Um Ihre neue Anwendung richtig zu testen, können Sie mehrere von Stripe bereitgestellte Kreditkartennummern verwenden.

Testing PaymentTesting PaymentTesting Payment

Das letzte, was Sie vielleicht tun möchten, ist die Einrichtung von Stripe Webhooks. Dies wird verwendet, damit Stripe Sie über Ereignisse in Ihrer Abrechnung benachrichtigt, z. B. Zahlung erfolgreich, Zahlung überfällig, Testversion läuft bald ab usw. Sie können eine vollständige Liste aus der Stripe-Dokumentation abrufen. Das Stripe-Ereignis wird als JSON an eine öffentlich zugängliche URL gesendet. Um dies lokal zu testen, möchten Sie wahrscheinlich Ngrok verwenden.

Bei der Installation von SAAS Ecom wurde ein neuer Controller hinzugefügt, um die Webhooks von Stripe zu verarbeiten: StripeWebhooksController.cs. Dort sehen Sie, wie das Ereignis Rechnungserstellung behandelt wird:

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;

Sie können beliebig viele Events im Controller implementieren.

7. Erstellen von Notizfunktionen in unserer App

Der wichtigste Teil dieser SAAS-Anwendung besteht darin, unseren Kunden das Speichern von Notizen zu ermöglichen. Um diese Funktionalität zu erstellen, erstellen wir zunächst die Note-Klasse:

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
}

Fügen Sie eine Eins-zu-Viele-Beziehung von ApplicationUser zu Note hinzu:

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

Da sich der DbContext geändert hat, müssen wir eine neue Datenbankmigration hinzufügen, also öffnen Sie die Nuget Package Manager-Konsole und führen Sie Folgendes aus:

1
PM> add-migration NotesAddedToModel

Dies ist der generierte 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
}

Als nächstes brauchen wir den Controller MyNotes. Da wir bereits über die Modellklasse Notes verfügen, verwenden wir das Gerüst, um die Controller-Klasse zu erstellen, um Methoden mit Entity Framework erstellen, lesen, aktualisieren und löschen zu können. Wir verwenden das Gerüst auch, um die Ansichten zu generieren.

Add ScaffoldingAdd ScaffoldingAdd Scaffolding
Add ControllerAdd ControllerAdd Controller

Nachdem ein Benutzer erfolgreich bei My Notes registriert wurde, leiten Sie den Benutzer an dieser Stelle zur Index-Aktion von NotesController um:

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

Bisher haben wir eine CRUD-Schnittstelle (Create / Read / Update / Delete) für Notes erstellt. Wir müssen weiterhin überprüfen, wann Benutzer versuchen, Notizen hinzuzufügen, um sicherzustellen, dass sie genügend Speicherplatz in ihren Abonnements haben.

Leere Liste mit Notizen:

Index TemplateIndex TemplateIndex Template

Neue Notiz erstellen:

Creating a NoteCreating a NoteCreating a Note

Liste der Anmerkungen:

Listing NotesListing NotesListing Notes

Hinweisdetails:

Detailed NotesDetailed NotesDetailed Notes

Notiz bearbeiten:

Editing a NoteEditing a NoteEditing a Note

Löschen der Notiz bestätigen:

Deleting a NoteDeleting a NoteDeleting a Note

Ich werde das Standard-Markup leicht bearbeiten:

  • Im Formular zum Erstellen einer Notiz habe ich das Feld CreatedAt entfernt und den Wert im Controller festgelegt.
  • Im Formular zum Bearbeiten einer Notiz habe ich CreatedAt in ein ausgeblendetes Feld geändert, sodass es nicht bearbeitet werden kann.
  • Ich habe das CSS leicht modifiziert, damit dieses Formular auch ein bisschen schöner aussieht.

Als wir den Notes-Controller mit Entity Framework generierten, listete die Liste der Notizen alle Notizen in der Datenbank auf, nicht nur die Notizen für den angemeldeten Benutzer. Aus Sicherheitsgründen müssen wir überprüfen, dass Benutzer nur die Notizen sehen, ändern oder löschen können, die ihnen gehören.

Wir müssen auch überprüfen, wie viele Notizen ein Benutzer hat, bevor wir ihm erlauben, eine neue zu erstellen, um zu überprüfen, ob die Grenzen des Abonnementplans eingehalten werden. Hier ist der neue Code für 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
}

Das ist es – wir haben die Kernfunktionalität für unsere SAAS-Anwendung.

8. Speicherung des Kundenstandorts für europäische Mehrwertsteuerzwecke

Zu Beginn dieses Jahres hat sich die Gesetzgebung in der Europäischen Union zur Mehrwertsteuer für Unternehmen, die digitale Dienstleistungen an private Verbraucher erbringen, geändert. Der Hauptunterschied besteht darin, dass Unternehmen Privatkunden und nicht Geschäftskunden mit gültiger USt-IdNr. Um zu überprüfen, in welchem ​​Land sie ansässig sind, müssen wir mindestens zwei dieser Formulare aufzeichnen:

  • die Rechnungsadresse des Kunden
  • die Internet Protocol (IP)-Adresse des vom Kunden verwendeten Geräts
  • Bankverbindung des Kunden
  • die Landesvorwahl der vom Kunden verwendeten SIM-Karte
  • der Standort des Festnetzanschlusses des Kunden, über den die Dienstleistung erbracht wird
  • andere kommerziell relevante Informationen (z. B. Produktcodierungsinformationen, die den Verkauf elektronisch mit einer bestimmten Gerichtsbarkeit verknüpfen)

Aus diesem Grund werden wir die IP-Adresse des Benutzers geolokalisieren, um sie zusammen mit der Rechnungsadresse und dem Kreditkartenland zu speichern.

8.1 Geo-Standort der IP-Adresse

Für die Geolokalisierung verwende ich Maxmind GeoLite2. Es ist eine kostenlose Datenbank, die uns das Land angibt, in dem sich eine IP befindet.

Maxmind Geo Lite2Maxmind Geo Lite2Maxmind Geo Lite2

Laden Sie die Datenbank herunter und fügen Sie sie zu App_Data hinzu, wie Sie auf dem Foto sehen können:

Adding The DatabaseAdding The DatabaseAdding The Database
Installation von NuGet MaxMind.GeoIP2.
NuGet PackagesNuGet PackagesNuGet Packages

Erstellen Sie Erweiterungen/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
}

In dieser statischen Klasse sind zwei Methoden implementiert:

  • GetCountryFromIP: Gibt den ISO-Code des Landes mit einer IP-Adresse zurück.
  • SelectListCountries: Gibt eine Liste der zu verwendenden Länder in einem Dropdown-Feld zurück. Es enthält den Länder-ISO-Code als Wert für jedes Land und den anzuzeigenden Ländernamen.

8.2 Kundenland bei Registrierung speichern

Speichern Sie in der Aktion Register from AccountController beim Anlegen des Benutzers die IP und das Land, zu dem die IP gehört:

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
};

Außerdem müssen wir beim Erstellen des Abonnements in Stripe den Steuerprozentsatz für diesen Kunden übergeben. Das machen wir ein paar Zeilen nach dem Anlegen des Benutzers:

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);

Wenn ein Benutzer in der Europäischen Union ansässig ist, lege ich den Steuerprozentsatz standardmäßig auf dieses Abonnement fest. Die Regeln sind etwas komplexer, aber zusammenfassend:

  • Wenn Ihr Unternehmen in einem EU-Land registriert ist, berechnen Sie Kunden in Ihrem Land immer Mehrwertsteuer.
  • Wenn Ihr Unternehmen in einem EU-Land registriert ist, berechnen Sie nur den Kunden, die sich in anderen EU-Ländern befinden und kein umsatzsteuerpflichtiges Unternehmen sind.
  • Wenn Ihr Unternehmen außerhalb der EU registriert ist, berechnen Sie nur Kunden, die keine Unternehmen mit einer gültigen Umsatzsteuer-Identifikationsnummer sind, Mehrwertsteuer.

8.3 Hinzufügen einer Rechnungsadresse zu unserem Modell

Im Moment gestatten wir unseren Kunden nicht, eine Rechnungsadresse und ihre Umsatzsteuer-Identifikationsnummer zu speichern, wenn sie ein in der EU mehrwertsteuerpflichtiges Unternehmen sind. In diesem Fall müssen wir ihren Steuersatz auf 0 ändern.

SAAS Ecom stellt die BillingAddress-Klasse bereit, die jedoch keiner Entität des Modells zugeordnet ist. Der Hauptgrund dafür ist, dass es in einigen SAAS-Anwendungen sinnvoll sein kann, diese einer Organisationsklasse zuzuordnen, wenn mehrere Benutzer Zugriff auf dasselbe Konto haben. Wenn dies nicht der Fall ist, wie in unserem Beispiel, können wir diese Beziehung sicher zur ApplicationUser-Klasse hinzufügen:

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
}

Da wir jedes Mal, wenn wir das Modell ändern, eine Datenbankmigration hinzufügen müssen, öffnen Sie Tools > NuGet Package Manager > Package Manager Console:

1
PM> add-migration BillingAddressAddedToUser

Und das ist die Migrationsklasse, die wir erhalten:

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
    }

Um diese Änderungen in der Datenbank zu erstellen, führen wir in der Package Manager Console aus:

1
PM> update-database

Ein weiteres Detail, das wir korrigieren müssen, besteht darin, dass wir in AccountController > Registrieren eine Standard-Rechnungsadresse festlegen müssen, da es sich um ein Feld handelt, das keine NULL-Werte enthält.

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
};

Auf der Rechnungsseite müssen wir die Rechnungsadresse für den Kunden anzeigen, falls sie hinzugefügt wurde, und unseren Kunden auch erlauben, sie zu bearbeiten. Zuerst müssen wir den Index-Aktion von BillingController ändern, um die Rechnungsadresse an die Ansicht zu übergeben:

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
}

Um die Adresse anzuzeigen, müssen wir nur die Ansicht "Billing/Index.cshtml" bearbeiten und die von SAAS Ecom dafür bereitgestellte Ansicht teilweise hinzufügen:

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)

Wenn wir nun zu Abrechnung navigieren, sehen wir den neuen Abschnitt:

Updated Billing TemplateUpdated Billing TemplateUpdated Billing Template

Der nächste Schritt ist die Aktion BillingController > BillingAddress, wir müssen die Rechnungsadresse an die Ansicht übergeben. Da wir den zweibuchstabigen ISO-Ländercode des Benutzers benötigen, habe ich ein Dropdown-Menü hinzugefügt, um das Land auszuwählen, das standardmäßig das Land ist, zu dem die Benutzer-IP gehört:

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
}

Wenn der Benutzer das Formular abschickt, müssen wir die Rechnungsadresse speichern und den Steuerprozentsatz bei Bedarf aktualisieren:

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
}

So sieht das Formular zum Hinzufügen oder Bearbeiten einer Rechnungsadresse aus:

Adding a Billing AddressAdding a Billing AddressAdding a Billing Address

Nach dem Hinzufügen der Adresse werde ich zurück in den Abrechnungsbereich weitergeleitet:

Billing AreaBilling AreaBilling Area

Wie Sie im obigen Screenshot sehen können, werden 20 % Mehrwertsteuer zum monatlichen Preis hinzugefügt, da ich mein Land auf Großbritannien eingestellt habe und keine Umsatzsteuer-Identifikationsnummer eingegeben habe. Der hier gezeigte Code geht davon aus, dass Sie ein nicht in der EU ansässiges Unternehmen sind. In diesem Fall müssen Sie den Fall bearbeiten, in dem sich Ihr Kunde in Ihrem Land befindet, und unabhängig davon, ob er Mehrwertsteuer hat oder nicht, müssen Sie Mehrwertsteuer berechnen.

9. Bereitstellen auf Azure-Websites (Webhosting + SSL kostenlos, SQL-Datenbank 5 USD pro Monat)

9.1 Bereitstellung der Website

Unser SAAS-Projekt ist bereit, live zu gehen, und ich habe Azure als Hosting-Plattform gewählt. Wenn Sie noch kein Konto haben, können Sie einen Monat lang kostenlos testen. Wir können unsere App von Git (GitHub oder BitBucket) bei jedem Commit bereitstellen, wenn wir möchten. Ich zeige Ihnen hier, wie Sie aus Visual Studio 2013 bereitstellen. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt Meine Notizen und wählen Sie im Kontextmenü Veröffentlichen. Der Assistent zum Veröffentlichen von Web wird geöffnet.

Publishing WizardPublishing WizardPublishing Wizard

Wählen Sie Microsoft Azure-Websites aus und klicken Sie auf Neu.

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

Geben Sie die Details für Ihre Website ein und klicken Sie auf Erstellen. Wenn Ihre Website erstellt wurde, sehen Sie dies. Weiter klicken.

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

In diesem Schritt können Sie die Verbindungszeichenfolge für Ihre Datenbank hinzufügen, falls vorhanden, oder Sie können sie später über das Verwaltungsportal hinzufügen. Weiter klicken.

Previewing The PublishingPreviewing The PublishingPreviewing The Publishing

Wenn wir nun auf Veröffentlichen klicken, lädt Visual Studio die Website in Azure hoch.

9.2 Bereitstellen der Datenbank

Um die Datenbank zu erstellen, müssen Sie zum Azure-Verwaltungsportal gehen, Durchsuchen und dann Daten + Speicher > SQL-Datenbank auswählen. Füllen Sie das Formular aus, um Ihre Datenbank zu erstellen.

Deploying The DatabaseDeploying The DatabaseDeploying The Database

Nachdem die Datenbank erstellt wurde, wählen Sie In Visual Studio öffnen und akzeptieren Sie, um der Firewall eine Ausnahme hinzuzufügen.

Adding a Firewall ExceptionAdding a Firewall ExceptionAdding a Firewall Exception

Ihre Datenbank wird im SQL Server-Objekt-Explorer von Visual Studio geöffnet. Wie Sie sehen, gibt es noch keine Tabellen:

SQL Object ExplorerSQL Object ExplorerSQL Object Explorer

Um ein SQL-Skript zum Erstellen der Tabellen in der Datenbank zu generieren, öffnen Sie die Paket-Manager-Konsole in Visual Studio und geben Sie Folgendes ein:

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

Kopieren Sie das Skript und wieder in den SQL Server-Objekt-Explorer, klicken Sie mit der rechten Maustaste auf Ihre Datenbank und wählen Sie Neue Abfrage aus. Fügen Sie das Skript ein und führen Sie es aus.

Creating the SQL ScriptCreating the SQL ScriptCreating the SQL Script

Dieses Skript enthält nicht die Daten, die wir aus der Seed-Methode in die Datenbank eingefügt haben. Wir müssen ein Skript manuell erstellen, um diese Daten zur Datenbank hinzuzufügen:

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')

Zu diesem Zeitpunkt ist My Notes SAAS live. Ich habe Stripe-Test-API-Schlüssel konfiguriert, sodass Sie die Testkreditkartendaten zum Testen verwenden können, wenn Sie möchten.

Advertisement
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.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.