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