public async Task SendPurchasePersonalMessage(ChargeBeeWebhookPayload payload) { ChargeBeeUtilities.ParseCustomerId(payload.Content.Customer.Id, out var accountType, out var accountId); if (accountType != "user") { // "activated" only happens on transition from trial -> active, and we only do trials // for personal subscriptions. throw new Exception("subscription_activated should only happen on personal/user subscriptions"); } var belongsToOrganization = false; using (var context = new ShipHubContext()) { belongsToOrganization = (await context.OrganizationAccounts.CountAsync(x => x.UserId == accountId)) > 0; } var wasGivenTrialCredit = payload.Content.Invoice.Discounts? .Count(x => x.EntityType == "document_level_coupon" && x.EntityId.StartsWith("trial_days_left")) > 0; var pdf = await PdfInfo(payload); await _mailer.PurchasePersonal( new Mail.Models.PurchasePersonalMailMessage() { GitHubUserName = payload.Content.Customer.GitHubUserName, ToAddress = payload.Content.Customer.Email, CustomerName = payload.Content.Customer.FirstName + " " + payload.Content.Customer.LastName, BelongsToOrganization = belongsToOrganization, WasGivenTrialCredit = wasGivenTrialCredit, InvoicePdfUrl = pdf.SignedUrl, AttachmentName = pdf.FileName, AttachmentUrl = pdf.DirectUrl, }); }
public async Task SendPaymentFailedMessage(ChargeBeeWebhookPayload payload) { var updateUrl = GetPaymentMethodUpdateUrl(_configuration, payload.Content.Customer.Id); var pdf = await PdfInfo(payload); var message = new Mail.Models.PaymentFailedMailMessage() { GitHubUserName = payload.Content.Customer.GitHubUserName, ToAddress = payload.Content.Customer.Email, CustomerName = payload.Content.Customer.FirstName + " " + payload.Content.Customer.LastName, Amount = payload.Content.Transaction.Amount / 100.0, InvoicePdfUrl = pdf.SignedUrl, AttachmentName = pdf.FileName, AttachmentUrl = pdf.DirectUrl, PaymentMethodSummary = PaymentMethodSummary(payload.Content.Transaction), ErrorText = payload.Content.Transaction.ErrorText, UpdatePaymentMethodUrl = updateUrl, }; if (payload.Content.Invoice.NextRetryAt != null) { message.NextRetryDate = DateTimeOffset.FromUnixTimeSeconds(payload.Content.Invoice.NextRetryAt.Value); } await _mailer.PaymentFailed(message); }
public async Task SendPaymentSucceededOrganizationMessage(ChargeBeeWebhookPayload payload) { var accountId = ChargeBeeUtilities.AccountIdFromCustomerId(payload.Content.Customer.Id); var planLineItem = payload.Content.Invoice.LineItems.Single(x => x.EntityType == "plan"); var newInvoiceStartDate = DateTimeOffset.FromUnixTimeSeconds(planLineItem.DateFrom); var previousMonthStart = DateTimeOffsetFloor(newInvoiceStartDate.AddMonths(-1)); var previousMonthEnd = DateTimeOffsetFloor(newInvoiceStartDate.AddDays(-1)); int activeUsersCount; string[] activeUsersSample; using (var context = new ShipHubContext()) { activeUsersCount = await context.Usage .Where(x => ( x.Date >= previousMonthStart && x.Date <= previousMonthEnd && context.OrganizationAccounts .Where(y => y.OrganizationId == accountId) .Select(y => y.UserId) .Contains(x.AccountId))) .Select(x => x.AccountId) .Distinct() .CountAsync(); activeUsersSample = await context.Usage .Where(x => ( x.Date >= previousMonthStart && x.Date <= previousMonthEnd && context.OrganizationAccounts .Where(y => y.OrganizationId == accountId) .Select(y => y.UserId) .Contains(x.AccountId))) .Select(x => x.Account.Login) .OrderBy(x => x) .Distinct() .Take(20) .ToArrayAsync(); } var pdf = await PdfInfo(payload); await _mailer.PaymentSucceededOrganization( new Mail.Models.PaymentSucceededOrganizationMailMessage() { GitHubUserName = payload.Content.Customer.GitHubUserName, ToAddress = payload.Content.Customer.Email, CustomerName = payload.Content.Customer.FirstName + " " + payload.Content.Customer.LastName, InvoicePdfUrl = pdf.SignedUrl, AttachmentName = pdf.FileName, AttachmentUrl = pdf.DirectUrl, ServiceThroughDate = DateTimeOffset.FromUnixTimeSeconds(planLineItem.DateTo), PreviousMonthActiveUsersCount = activeUsersCount, PreviousMonthActiveUsersSample = activeUsersSample, PreviousMonthStart = previousMonthStart, AmountPaid = payload.Content.Invoice.AmountPaid / 100.0, PaymentMethodSummary = PaymentMethodSummary(payload.Content.Transaction), }); }
private async Task <string> GitHubUserNameFromWebhookPayload(ChargeBeeWebhookPayload payload) { // Most events include the customer portion which gives us the GitHub username. if (payload.Content.Customer?.GitHubUserName != null) { return(payload.Content.Customer.GitHubUserName); } else { // Invoice events (and maybe others, TBD) don't include the Customer portion so // we have to find the customer id in another section. var candidates = new[] { payload.Content.Invoice?.CustomerId, payload.Content.CreditNote?.CustomerId, }; var customerId = candidates.SkipWhile(string.IsNullOrEmpty).FirstOrDefault(); if (customerId != null) { var accountId = ChargeBeeUtilities.AccountIdFromCustomerId(customerId); using (var context = new ShipHubContext()) { var login = await context.Accounts .AsNoTracking() .Where(x => x.Id == accountId) .Select(x => x.Login) .FirstOrDefaultAsync(); return(login); } } else { return(null); } } }
public async Task SendCancellationScheduled(ChargeBeeWebhookPayload payload) { var updateUrl = GetPaymentMethodUpdateUrl(_configuration, payload.Content.Customer.Id); await _mailer.CancellationScheduled(new Mail.Models.CancellationScheduledMailMessage() { GitHubUserName = payload.Content.Customer.GitHubUserName, ToAddress = payload.Content.Customer.Email, CustomerName = payload.Content.Customer.FirstName + " " + payload.Content.Customer.LastName, CurrentTermEnd = DateTimeOffset.FromUnixTimeSeconds(payload.Content.Subscription.CurrentTermEnd), }); }
public async Task SendPurchaseOrganizationMessage(ChargeBeeWebhookPayload payload) { var accountId = ChargeBeeUtilities.AccountIdFromCustomerId(payload.Content.Customer.Id); var pdf = await PdfInfo(payload); await _mailer.PurchaseOrganization( new Mail.Models.PurchaseOrganizationMailMessage() { GitHubUserName = payload.Content.Customer.GitHubUserName, ToAddress = payload.Content.Customer.Email, CustomerName = payload.Content.Customer.FirstName + " " + payload.Content.Customer.LastName, InvoicePdfUrl = pdf.SignedUrl, AttachmentName = pdf.FileName, AttachmentUrl = pdf.DirectUrl, }); }
public async Task SendPaymentRefundedMessage(ChargeBeeWebhookPayload payload) { var pdf = await PdfInfo(payload); await _mailer.PaymentRefunded(new Mail.Models.PaymentRefundedMailMessage() { GitHubUserName = payload.Content.Customer.GitHubUserName, ToAddress = payload.Content.Customer.Email, CustomerName = payload.Content.Customer.FirstName + " " + payload.Content.Customer.LastName, AmountRefunded = payload.Content.CreditNote.AmountRefunded / 100.0, CreditNotePdfUrl = pdf.SignedUrl, AttachmentName = pdf.FileName, AttachmentUrl = pdf.DirectUrl, PaymentMethodSummary = PaymentMethodSummary(payload.Content.Transaction), }); }
public async Task SendCardExpiryReminderMessage(ChargeBeeWebhookPayload payload) { var updateUrl = GetPaymentMethodUpdateUrl(_configuration, payload.Content.Customer.Id); await _mailer.CardExpiryReminder(new Mail.Models.CardExpiryReminderMailMessage() { GitHubUserName = payload.Content.Customer.GitHubUserName, ToAddress = payload.Content.Customer.Email, CustomerName = payload.Content.Customer.FirstName + " " + payload.Content.Customer.LastName, LastCardDigits = payload.Content.Card.Last4, UpdatePaymentMethodUrl = updateUrl, ExpiryMonth = payload.Content.Card.ExpiryMonth, ExpiryYear = payload.Content.Card.ExpiryYear, AlreadyExpired = payload.EventType == "card_expired", }); }
/// <summary> /// When testing ChargeBee webhooks on your local server, you want to prevent shiphub-dev /// from handling the same hook. Sometimes it's harmless for shiphub-dev and your local server /// to process the same hook twice. Other times, it's a race condition because handling that hook /// changes some ChargeBee state. /// /// If you're testing ChargeBee webhooks locally, do this -- /// /// In the app settings for shiphub-dev, add the github username to "ChargeBeeWebhookExcludeList" -- /// https://portal.azure.com/#resource/subscriptions/b9f28aae-2074-4097-b5ce-ec28f68c4981/resourceGroups/ShipHub-Dev/providers/Microsoft.Web/sites/shiphub-dev/application /// /// In your Secret.AppSettings.config, add the following -- <![CDATA[ /// <add key="ChargeBeeWebhookIncludeList" value="some_github_username"/> /// ]]> /// /// Dev will ignore hooks for your user and your local server will only process /// hooks for your user. /// </summary> /// <param name="gitHubUserName">GitHub user or organization name</param> /// <returns>True if we should reject this webhook event</returns> public virtual async Task <bool> ShouldIgnoreWebhook(ChargeBeeWebhookPayload payload) { if (_configuration.ChargeBeeWebhookIncludeOnlyList != null && !_configuration.ChargeBeeWebhookIncludeOnlyList.Contains(await GitHubUserNameFromWebhookPayload(payload))) { return(true); } if (_configuration.ChargeBeeWebhookExcludeList != null && _configuration.ChargeBeeWebhookExcludeList.Contains(await GitHubUserNameFromWebhookPayload(payload))) { return(true); } return(false); }
public async Task SendPaymentSucceededPersonalMessage(ChargeBeeWebhookPayload payload) { var planLineItem = payload.Content.Invoice.LineItems.Single(x => x.EntityType == "plan"); var pdf = await PdfInfo(payload); await _mailer.PaymentSucceededPersonal( new Mail.Models.PaymentSucceededPersonalMailMessage() { GitHubUserName = payload.Content.Customer.GitHubUserName, ToAddress = payload.Content.Customer.Email, CustomerName = payload.Content.Customer.FirstName + " " + payload.Content.Customer.LastName, InvoicePdfUrl = pdf.SignedUrl, AttachmentName = pdf.FileName, AttachmentUrl = pdf.DirectUrl, AmountPaid = payload.Content.Invoice.AmountPaid / 100.0, ServiceThroughDate = DateTimeOffset.FromUnixTimeSeconds(planLineItem.DateTo), PaymentMethodSummary = PaymentMethodSummary(payload.Content.Transaction), }); }
public async Task HandlePendingInvoiceCreated(ChargeBeeWebhookPayload payload) { var accountId = ChargeBeeUtilities.AccountIdFromCustomerId(payload.Content.Invoice.CustomerId); var planLineItem = payload.Content.Invoice.LineItems.Single(x => x.EntityType == "plan"); if (planLineItem.EntityId == "organization") { // Calculate the number of active users during the previous month, and then // attach extra charges to this month's invoice. So, for organizations, the // base charge on every invoice is for the coming month, but the metered // component is always for the trailing month. var newInvoiceStartDate = DateTimeOffset.FromUnixTimeSeconds(planLineItem.DateFrom); var previousMonthStart = DateTimeOffsetFloor(newInvoiceStartDate.AddMonths(-1)); var previousMonthEnd = DateTimeOffsetFloor(newInvoiceStartDate.AddDays(-1)); int activeUsers; using (var context = new ShipHubContext()) { activeUsers = await context.Usage .AsNoTracking() .Where(x => ( x.Date >= previousMonthStart && x.Date <= previousMonthEnd && context.OrganizationAccounts .Where(y => y.OrganizationId == accountId) .Select(y => y.UserId) .Contains(x.AccountId))) .Select(x => x.AccountId) .Distinct() .CountAsync(); } if (activeUsers > 1) { await _chargeBee.Invoice.AddAddonCharge(payload.Content.Invoice.Id) .AddonId("additional-seats") .AddonQuantity(Math.Max(activeUsers - 1, 0)) .Request(); } } await _chargeBee.Invoice.Close(payload.Content.Invoice.Id).Request(); }
public async Task HandleSubscriptionStateChange(ChargeBeeWebhookPayload payload) { var accountId = ChargeBeeUtilities.AccountIdFromCustomerId(payload.Content.Customer.Id); ChangeSummary changes; var tasks = new List <Task>(); using (var context = new ShipHubContext()) { var sub = await context.Subscriptions .AsNoTracking() .SingleOrDefaultAsync(x => x.AccountId == accountId); if (sub == null) { // We only care to update subscriptions we've already sync'ed. This case often happens // in development - e.g., you might be testing subscriptions on your local machine, and // chargebee delivers webhook calls to shiphub-dev about subscriptions it doesn't know // about yet. return; } long incomingVersion; if (payload.EventType == "customer_deleted") { incomingVersion = payload.Content.Customer.ResourceVersion; } else { incomingVersion = payload.Content.Subscription.ResourceVersion; } if (incomingVersion <= sub.Version) { // We're receiving webhook events out-of-order (which can happen due to re-delivery), // so ignore. return; } sub.Version = incomingVersion; var beforeState = sub.State; if (payload.EventType.Equals("subscription_deleted") || payload.EventType.Equals("customer_deleted")) { sub.State = SubscriptionState.NotSubscribed; sub.TrialEndDate = null; } else { switch (payload.Content.Subscription.Status) { case "in_trial": sub.State = SubscriptionState.InTrial; sub.TrialEndDate = DateTimeOffset.FromUnixTimeSeconds((long)payload.Content.Subscription.TrialEnd); break; case "active": case "non_renewing": case "future": sub.State = SubscriptionState.Subscribed; sub.TrialEndDate = null; break; case "cancelled": sub.State = SubscriptionState.NotSubscribed; sub.TrialEndDate = null; break; } } changes = await context.BulkUpdateSubscriptions(new[] { new SubscriptionTableType() { AccountId = sub.AccountId, State = sub.StateName, TrialEndDate = sub.TrialEndDate, Version = sub.Version, } }); } if (!changes.IsEmpty) { await _queueClient.NotifyChanges(changes); } await Task.WhenAll(tasks); }
private async Task <(string SignedUrl, string FileName, string DirectUrl)> PdfInfo(ChargeBeeWebhookPayload payload) { string signedUrl; string fileName; string directUrl; var githubUserName = await GitHubUserNameFromWebhookPayload(payload); if (payload.Content.CreditNote?.Id != null) { var creditNoteId = payload.Content.CreditNote.Id; var signature = BillingController.CreateLegacySignature(creditNoteId); fileName = $"ship-credit-{githubUserName}-{DateTimeOffset.FromUnixTimeSeconds(payload.Content.CreditNote.Date).ToString("yyyy-MM-dd")}.pdf"; signedUrl = $"https://{_configuration.ApiHostName}/billing/credit/{creditNoteId}/{signature}/{fileName}"; directUrl = (await _chargeBee.CreditNote.Pdf(creditNoteId).Request()).Download.DownloadUrl; } else if (payload.Content.Invoice?.Id != null) { var invoiceId = payload.Content.Invoice.Id; var signature = BillingController.CreateLegacySignature(invoiceId); fileName = $"ship-invoice-{githubUserName}-{DateTimeOffset.FromUnixTimeSeconds(payload.Content.Invoice.Date).ToString("yyyy-MM-dd")}.pdf"; signedUrl = $"https://{_configuration.ApiHostName}/billing/invoice/{invoiceId}/{signature}/{fileName}"; directUrl = (await _chargeBee.Invoice.Pdf(invoiceId).Request()).Download.DownloadUrl; } else { throw new Exception($"Cannot determine PDF for {payload.EventType}"); } return(SignedUrl : signedUrl, FileName : fileName, DirectUrl : directUrl); }