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