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 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);
                }
            }
        }
Ejemplo n.º 4
0
        public async Task <IHttpActionResult> BuyFinish(string id, string state)
        {
            var hostedPage = (await _chargeBee.HostedPage.Retrieve(id).Request()).HostedPage;

            if (hostedPage.State != cbm.HostedPage.StateEnum.Succeeded)
            {
                // We should only get here if the signup was completed.
                throw new InvalidOperationException("Asked to complete signup for subscription when checkout did not complete.");
            }

            var passThruContent = JsonConvert.DeserializeObject <BuyPassThruContent>(hostedPage.PassThruContent);

            ChargeBeeUtilities.ParseCustomerId(hostedPage.Content.Subscription.CustomerId, out var accountType, out var accountId);

            ChangeSummary changes;

            using (var context = new ShipHubContext()) {
                changes = await context.BulkUpdateSubscriptions(new[] {
                    new SubscriptionTableType()
                    {
                        AccountId    = accountId,
                        State        = SubscriptionState.Subscribed.ToString(),
                        TrialEndDate = null,
                        Version      = hostedPage.Content.Subscription.ResourceVersion.Value,
                    },
                });
            }

            if (!changes.IsEmpty)
            {
                await _queueClient.NotifyChanges(changes);
            }

            if (passThruContent.AnalyticsId != null)
            {
                await _mixpanelClient.TrackAsync(
                    "Purchased",
                    passThruContent.AnalyticsId,
                    new {
                    plan        = hostedPage.Content.Subscription.PlanId,
                    customer_id = hostedPage.Content.Subscription.CustomerId,
                    // These refer to the account performing the action, which in the case of
                    // an org subscription, is different than the account being purchased.
                    _github_id    = passThruContent.ActorId,
                    _github_login = passThruContent.ActorLogin,
                });
            }

            var hashParams = new ThankYouPageHashParameters()
            {
                Value  = hostedPage.Content.Subscription.PlanUnitPrice.Value / 100,
                PlanId = hostedPage.Content.Subscription.PlanId,
            };
            var hashParamBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(
                                                             JsonConvert.SerializeObject(hashParams, GitHubSerialization.JsonSerializerSettings)));

            return(Redirect($"https://{_configuration.WebsiteHostName}/signup-thankyou.html#{WebUtility.UrlEncode(hashParamBase64)}"));
        }
        private static string GetPaymentMethodUpdateUrl(IShipHubConfiguration configuration, string customerId)
        {
            var accountId = ChargeBeeUtilities.AccountIdFromCustomerId(customerId);

            var apiHostName = configuration.ApiHostName;

            var signature = BillingController.CreateSignature(accountId, accountId);
            var updateUrl = $"https://{apiHostName}/billing/update/{accountId}/{signature}";

            return(updateUrl);
        }
        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 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);
        }