using Dapper; using HashidsNet; using Microsoft.AspNetCore.JsonPatch; using MisIngredientesVue.Core.Exceptions; using MisIngredientesVue.Core.Extensions; using MisIngredientesVue.Core.Models; using Newtonsoft.Json; using Stripe; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; namespace MisIngredientesVue.Core.Services { public interface IBillingService { Task> GetCfdiUsageRecords(string tenantId, int skip, int take, string sortBy, string sortOrder, DateTime startDate, DateTime endDate); Task GetCfdiUsageRecordsExcel(string tenantId, int offset, string sortBy, string sortOrder, DateTime startDate, DateTime endDate); Task> GetPlans(); Task GetSubscription(string subscriptionId, string subSubscriptionId); Task CreateOrUpdateSubscription(string tenantId, string customerId, string currentSubscriptionId, string currentSubSubscriptionId, string paymentMethodId, string planTypeId, User newUser = null); Task<(bool refreshToken, IEnumerable paymentMethods)> GetPaymentMethods(string tenantId, string customerId, string userEmail, string type = "card"); Task CreateCustomer(string tenantId, string email, bool throwExceptionIfExisting = false); //Task ReportUsage(string tenantId, string subscriptionItemId, int userId, string folio, string uuid, CfdiRecordType type, CfdiRecordStatus status); //Task Test(string customerId); //Task UpdateSubscriptionQuantity(string subscriptionItemId, int quantity); Task GetUsage(string subSubscriptionId); Task GetUpcomingInvoice(string subscriptionId, string subSubscriptionId); Task CreateNewSubscription(string planTypeId, string paymentId, string email, string firstName, string lastName, string password, string companyName); //Task CreateNewSubscriptionNoCard(string planTypeId, string email, string firstName, string lastName, string password, string companyName); Task GetDefaultPaymentMethodId(string customerId); Task DeleteCards(string tenantId, string customerId, string[] ids); } public enum StripeProductType { Free = 0, //Plan Gratis (folios, uso medido) Basic = 1, //Plan Básico (folios, uso medido) Startup = 2, //Plan Startup (folios, uso medido) BasicUserMonthly = 3, StartupUserMonthly = 4, EnterpriseUserMonthly = 5, BasicUserYearly = 6, StartupUserYearly = 7, EnterpriseUserYearly = 8, FreeMonthly = 9, //Configurado como 0 pesos en Stripe FreeYearly = 10, //Configurado como 0 pesos en Stripe Enterprise = 11 //Plan Enterprise (folios, uso medido), configurado como 0 pesos en Stripe porque es infinito } //Este es el que se guarda en la base de datos, ya que un plan puede tener 2 tipos de productos (los folios y los usuarios) //stripeplanid public enum PlanType { FreeMonthly = 0, //Básíco, antes conocido como Gratis BasicMonthly = 1, //Ya no se usa StartupMonthly = 2, //Esencial, antes conocido como Startup EnterpriseMonthly = 3, //Plus, antes conocido como Enterprise BasicYearly = 4, //Ya no se usa StartupYearly = 5, //Esencial, antes conocido como Startup EnterpriseYearly = 6, //Plus, antes conocido como Enterprise FreeYearly = 7 //Básíco, antes conocido como Gratis } public enum SubscriptionLevelType { Free = 0, //Básíco, antes conocido como Gratis Basic = 1, //Ya no se usa Startup = 2, //Esencial, antes conocido como Startup Enterprise = 3 //Plus, antes conocido como Enterprise } public enum SubscriptionPeriodicityType { Monthly = 0, Yearly = 1, Free = 2 } public class BillingService : IBillingService { private readonly IRepository _repository; private readonly IEmailService _emailService; //QAPROD //DESARROLLO public static string API_KEY = "sk_test_51HBQGKARviRAQ4ozdH8mPrTluSan50NMev0bSZ2KVZDQGANVBPqg5QGzMAh7ZZ3yygPz2vlKtNpecjmMcbsQwplT00H5WTAxvs"; //PRODUCCIÓN //public static string API_KEY = "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"; /* Un producto contiene varios precios. En este caso se decidió que cada producto sólo contendrá un sólo precio. * La razón de esto es para que en el recibo de compra salga el nombre específico de lo que se está cobrando, y no el nombre del producto padre. * Para crear la subscripción se requiere del id del precio. * Una subscripción puede contener varios precios de varios productos (o de uno) * El planId aquí es el id del precio. * Se ocupan los siguientes tipos de subscripción (productos): * 1.- Usuario * 2.- Folios de facturación (este se pudo haber integrar con 1, sin embargo, en los folios no debe aplicar lo del trial, por lo que se decidió crear otro producto) * Cuando un usuario administrador se registra, se deben subscribir a los primeros 2 productos * Cada usuario nuevo no administrador debe tener el producto 1 * Al momento de crear la subscripción se puede determinar los días de prueba. * El periodo de prueba se aplica a todos los usuarios. Por ejemplo, si el primer usuario se registra se le otorgan 30 días. * Si después de los 15 días el usuario decide agregar más usuarios a esos nuevos usuarios se les aplicarán 15 días. * Si el periodo de prueba se termina los siguientes usuarios ya no tendrán periodo de prueba. */ //QAPROD //DESARROLLO private readonly Dictionary _priceIds = new Dictionary { { StripeProductType.Free, "price_1I4AFfARviRAQ4oz2XZyqaCX" }, //Gratis - Folios de Facturación (Tarifa escalonada) { StripeProductType.FreeMonthly, "price_1I8nsgARviRAQ4ozLbGTYC7o" }, //Gratis Mensual - Usuarios (Tarifa Standard con 0 pesos, esto es para que haga bien el prorrateo al cambiar entre suscripciones) { StripeProductType.FreeYearly, "price_1I8nt9ARviRAQ4ozJ9X7auus" }, //Gratis Anual - Usuarios (Tarifa Standard con 0 pesos, esto es para que haga bien el prorrateo al cambiar entre suscripciones) { StripeProductType.Basic, "price_1I4AIAARviRAQ4ozi2R5l84v" }, //Plan Básico - Folios de Facturación (Tarifa escalonada) { StripeProductType.Startup, "price_1I4AJMARviRAQ4ozEEpnK2He" }, //Plan Startup - Folios de Facturación (Tarifa escalonada) { StripeProductType.BasicUserMonthly, "price_1I4ALeARviRAQ4ozRJCGl9DS" }, //Plan Básico Mensual - Usuarios (Tarifa Standard) { StripeProductType.BasicUserYearly, "price_1I4AM8ARviRAQ4ozC6OW3f2U" }, //Plan Básico Anual - Usuarios (Tarifa Standard) { StripeProductType.StartupUserMonthly, "price_1I4APIARviRAQ4oz2HCSozAl" }, //Plan Startup Mensual - Usuarios (Tarifa Standard) { StripeProductType.StartupUserYearly, "price_1I4APyARviRAQ4oz3QwUDQzP" }, //Plan Startup Anual - Usuarios (Tarifa Standard) { StripeProductType.EnterpriseUserMonthly, "price_1I4ARgARviRAQ4ozeBd6qTaT" }, //Plan Enterprise Mensual - Usuarios (Tarifa Standard) { StripeProductType.EnterpriseUserYearly, "price_1I4ASTARviRAQ4ozfgYes4Qa" }, //Plan Enterprise Anual - Usuarios (Tarifa Standard) { StripeProductType.Enterprise, "price_1I8pjFARviRAQ4oz5aKnW50Y" }, //Plan Enterprise - Folios de Facturación (Tarifa Standard con 0 pesos) }; //PRODUCCIÓN //private readonly Dictionary _priceIds = new Dictionary { // { StripeProductType.Free, "price_1IEPCGARviRAQ4ozlteKxuoF" }, //Gratis - Folios de Facturación (Tarifa escalonada) // { StripeProductType.FreeMonthly, "price_1I8nUrARviRAQ4ozf01EmqTy" }, //Gratis Mensual - Usuarios (Tarifa Standard con 0 pesos, esto es para que haga bien el prorrateo al cambiar entre suscripciones) // { StripeProductType.FreeYearly, "price_1IEP3ZARviRAQ4ozIlMDd3Dh" }, //Gratis Anual - Usuarios (Tarifa Standard con 0 pesos, esto es para que haga bien el prorrateo al cambiar entre suscripciones) // { StripeProductType.Basic, "" }, //Plan Básico - Folios de Facturación (Tarifa escalonada) // { StripeProductType.Startup, "price_1IEPD7ARviRAQ4ozJcwNjCdb" }, //Plan Startup - Folios de Facturación (Tarifa escalonada) // { StripeProductType.BasicUserMonthly, "" }, //Plan Básico Mensual - Usuarios (Tarifa Standard) // { StripeProductType.BasicUserYearly, "" }, //Plan Básico Anual - Usuarios (Tarifa Standard) // { StripeProductType.StartupUserMonthly, "price_1KE3y3ARviRAQ4ozLtwfehor" }, //Plan Startup Mensual - Usuarios (Tarifa Standard) //el viejo -> price_1IEP4wARviRAQ4ozHcPaxuiD // { StripeProductType.StartupUserYearly, "price_1IEP7OARviRAQ4ozomCl9h5r" }, //Plan Startup Anual - Usuarios (Tarifa Standard) // { StripeProductType.EnterpriseUserMonthly, "price_1KE3wcARviRAQ4ozQOYjpEyo" }, //Plan Enterprise Mensual - Usuarios (Tarifa Standard) //el viejo -> price_1IEP5xARviRAQ4ozi0KafrRz // { StripeProductType.EnterpriseUserYearly, "price_1IEP6uARviRAQ4oz4HwRrqtP" }, //Plan Enterprise Anual - Usuarios (Tarifa Standard) // { StripeProductType.Enterprise, "price_1IEPAMARviRAQ4oz8LEyb0t6" }, //Plan Enterprise - Folios de Facturación (Tarifa Standard con 0 pesos) //}; public static readonly List SUBSCRIPTION_PLANS = new List { new SubscriptionPlan() { Id = (int)PlanType.FreeMonthly, Description = "Utilice el sistema gratis pero con limitantes", MainPrice = "$0.00 MXN", MainPriceWTaxes = "$0.00 MXN", Taxes = "$0.00 MXN", Name = "Plan Básico", //Antes llamado Gratis SubPrice = "$5.00 MXN", MainPriceDescription = $"20 Folios de facturación incluidos", FreeFolios = 20, SubPriceDescription = $"Por folio adicional consumido", SubscriptionType = ((int)SubscriptionLevelType.Free), PeriodicityType = ((int)SubscriptionPeriodicityType.Monthly) }, new SubscriptionPlan() { Id = (int)PlanType.FreeYearly, Description = "Utilice el sistema gratis pero con limitantes", MainPrice = "$0.00 MXN", MainPriceWTaxes = "$0.00 MXN", Taxes = "$0.00 MXN", Name = "Plan Básico", //Antes llamado Gratis SubPrice = "$5.00 MXN", MainPriceDescription = $"20 Folios de facturación incluidos", FreeFolios = 20, SubPriceDescription = $"Por folio adicional consumido", SubscriptionType = ((int)SubscriptionLevelType.Free), PeriodicityType = ((int)SubscriptionPeriodicityType.Yearly) }, //Mensual //new SubscriptionPlan() { // Id = (int)PlanType.BasicMonthly, // Description = "Perfecto para pequeños negocios", // MainPrice = "$200.00 MXN", // Name = "Plan Básico", // SubPrice = "$3.00 MXN", // MainPriceDescription = $"Tarifa fija que incluye 150 folios", // SubPriceDescription = $"Por folio", // SubscriptionType = ((int)SubscriptionLevelType.Basic), // PeriodicityType = ((int)SubscriptionPeriodicityType.Monthly) //}, new SubscriptionPlan() { Id = (int)PlanType.StartupMonthly, Description = "Perfecto para negocios medianos", MainPrice = "$400.00 MXN", MainPriceWTaxes = "$464.00 MXN", Taxes = "$64.00 MXN", Name = "Plan Esencial", //Antes llamado Startup SubPrice = "$3.00 MXN", MainPriceDescription = $"Tarifa fija que incluye 200 folios", FreeFolios = 200, SubPriceDescription = $"Por folio adicional consumido", SubscriptionType = ((int)SubscriptionLevelType.Startup), PeriodicityType = ((int)SubscriptionPeriodicityType.Monthly) }, new SubscriptionPlan() { Id = (int)PlanType.EnterpriseMonthly, Description = "Perfecto para negocios grandes", MainPrice = "$800.00 MXN", MainPriceWTaxes = "$928.00 MXN", Taxes = "$128.00 MXN", Name = "Plan Plus", //Antes llamado Enterprise SubPrice = "", MainPriceDescription = $"Tarifa fija que incluye folios ilimitados", FreeFolios = -1, SubPriceDescription = $"", SubscriptionType = ((int)SubscriptionLevelType.Enterprise), PeriodicityType = ((int)SubscriptionPeriodicityType.Monthly) }, //Yearly //new SubscriptionPlan() { // Id = (int)PlanType.BasicYearly, // Description = "Perfecto para pequeños negocios", // MainPrice = "$2000.00 MXN", // Name = "Plan Básico", // SubPrice = "$3.00 MXN", // MainPriceDescription = $"Tarifa fija que incluye 150 folios", // SubPriceDescription = $"Por folio", // SubscriptionType = ((int)SubscriptionLevelType.Basic), // PeriodicityType = ((int)SubscriptionPeriodicityType.Yearly) //}, new SubscriptionPlan() { Id = (int)PlanType.StartupYearly, Description = "Perfecto para negocios medianos", MainPrice = "$4,000.00 MXN", MainPriceWTaxes = "$4,640.00 MXN", Taxes = "$640.00 MXN", Name = "Plan Esencial", //Antes llamado Startup SubPrice = "$3.00 MXN", MainPriceDescription = $"Tarifa fija que incluye 200 folios", FreeFolios = 200, SubPriceDescription = $"Por folio adicional consumido", SubscriptionType = ((int)SubscriptionLevelType.Startup), PeriodicityType = ((int)SubscriptionPeriodicityType.Yearly) }, new SubscriptionPlan() { Id = (int)PlanType.EnterpriseYearly, Description = "Perfecto para negocios grandes", MainPrice = "$8,000.00 MXN", MainPriceWTaxes = "$9,280.00 MXN", Taxes = "$1,280.00 MXN", Name = "Plan Plus", //Antes llamado Enterprise SubPrice = "", MainPriceDescription = $"Tarifa fija que incluye folios ilimitados", FreeFolios = -1, SubPriceDescription = $"", SubscriptionType = ((int)SubscriptionLevelType.Enterprise), PeriodicityType = ((int)SubscriptionPeriodicityType.Yearly) }, }; public BillingService(IRepository repository, IEmailService emailService) { _repository = repository; _emailService = emailService; } public async Task DeleteCards(string tenantId, string customerId, string[] ids) { StripeConfiguration.ApiKey = API_KEY; var service = new PaymentMethodService(); await service.DetachAsync(ids[0]); } private string GetPriceId(StripeList prices, string productId) { return prices.FirstOrDefault(x => x.ProductId == productId)?.Id; } public async Task<(bool refreshToken, IEnumerable paymentMethods)> GetPaymentMethods(string tenantId, string customerId, string userEmail, string type = "card") { StripeConfiguration.ApiKey = API_KEY; //For possible types go to: https://stripe.com/docs/api/payment_methods/object var cardsService = new PaymentMethodService(); try { return (false, (await cardsService.ListAsync(new PaymentMethodListOptions() { Type = type, Customer = customerId })).ToList()); } catch (Exception ex) { if (ex.Message.Contains("No such customer")) { var customer = await CreateCustomer(tenantId, userEmail); return (true, (await cardsService.ListAsync(new PaymentMethodListOptions() { Type = type, Customer = customer.Id })).ToList()); } throw ex; } } public async Task GetSubscription(string subscriptionId, string subSubscriptionId) { StripeConfiguration.ApiKey = API_KEY; var subscriptionService = new SubscriptionService(); try { var subscription = !string.IsNullOrEmpty(subscriptionId) ? await subscriptionService.GetAsync(subscriptionId) : null; var subSubscription = !string.IsNullOrEmpty(subSubscriptionId) ? await subscriptionService.GetAsync(subSubscriptionId) : null; var planType = subscription != null ? _priceIds.FirstOrDefault(x => x.Value == subscription.Items.ElementAt(0).Price.Id).Key : StripeProductType.Free; return MapSubscription(subscription, subSubscription, ((int)planType).ToString()); } catch(Exception ex) { if(ex.Message.Contains("No such subscription")) { return new UserSubscription(); } throw ex; } } private Stripe.Price GetPrice(StripeList prices, string productId) { var productPrices = prices.Where(x => x.ProductId == productId); if (productPrices.Count() == 0) return null; if (productPrices.Count() == 1) return productPrices.First(); return productPrices.First(x => x.Tiers != null); } //private string GetFormattedMainPrice(StripeList prices, string productId) //{ // var price = GetPrice(prices, productId); // if (price == null) return "---"; // if (price.Tiers == null || price.Tiers.Count == 0) return $"${price.UnitAmountDecimal/100}"; // return $"${price.Tiers[0].FlatAmountDecimal / 100} {price.Currency.ToUpper()}"; //} //private string GetFormattedSubPrice(StripeList prices, string productId) //{ // var price = GetPrice(prices, productId); // if (price == null || price.Tiers == null) return ""; // return $"+ ${price.Tiers[1].UnitAmountDecimal / 100}"; //} //private string GetFormattedMainPriceDescription(StripeList prices, string productId) //{ // var price = GetPrice(prices, productId); // if (price == null || price.Tiers == null) return ""; // return $"Tarifa fija que incluye {price.Tiers[0].UpTo} folios"; //} //private string GetFormattedSubPriceDescription(StripeList prices, string productId) //{ // var price = GetPrice(prices, productId); // if (price == null || price.Tiers == null) return ""; // return "Por cada folio adicional consumido"; //} private UserSubscription MapSubscription(Subscription subscription, Subscription subSubscription, string priceId = null) { return new UserSubscription() { PlanId = priceId == null ? subscription?.Items?.ElementAt(0)?.Price?.Id : priceId, CreatedOn = subscription?.Created ?? subSubscription?.Created, EndPeriod = subscription?.CurrentPeriodEnd ?? subSubscription?.CurrentPeriodEnd, StartPeriod = subscription?.CurrentPeriodStart ?? subSubscription?.CurrentPeriodStart, //PlanName = subscription.Items?.ElementAt(0)?.Price?.Product?.Name, SubscriptionId = subscription?.Id ?? subSubscription?.Id, Status = subscription?.Status ?? subSubscription?.Status }; } public async Task GetDefaultPaymentMethodId(string customerId) { StripeConfiguration.ApiKey = API_KEY; var customerService = new CustomerService(); var customer = await customerService.GetAsync(customerId); return customer.InvoiceSettings.DefaultPaymentMethodId; } public async Task CreateOrUpdateSubscription(string tenantId, string customerId, string currentSubscriptionId, string currentSubSubscriptionId, string paymentMethodId, string planTypeId, User newUser = null) { StripeConfiguration.ApiKey = API_KEY; var planType = (PlanType)Convert.ToInt32(planTypeId); //El tipo principal de subscripción es el que alberga los usuarios. StripeProductType? mainProductType = null; switch (planType) { case PlanType.FreeMonthly: mainProductType = StripeProductType.FreeMonthly; break; case PlanType.FreeYearly: mainProductType = StripeProductType.FreeYearly; break; case PlanType.BasicMonthly: mainProductType = StripeProductType.BasicUserMonthly; break; case PlanType.BasicYearly: mainProductType = StripeProductType.BasicUserYearly; break; case PlanType.StartupMonthly: mainProductType = StripeProductType.StartupUserMonthly; break; case PlanType.StartupYearly: mainProductType = StripeProductType.StartupUserYearly; break; case PlanType.EnterpriseMonthly: mainProductType = StripeProductType.EnterpriseUserMonthly; break; case PlanType.EnterpriseYearly: mainProductType = StripeProductType.EnterpriseUserYearly; break; } //Subtipo de subscripción para los folios de facturación. StripeProductType? subProductType = null; switch (planType) { case PlanType.FreeMonthly: subProductType = StripeProductType.Free; break; case PlanType.FreeYearly: subProductType = StripeProductType.Free; break; case PlanType.BasicMonthly: subProductType = StripeProductType.Basic; break; case PlanType.BasicYearly: subProductType = StripeProductType.Basic; break; case PlanType.StartupMonthly: subProductType = StripeProductType.Startup; break; case PlanType.StartupYearly: subProductType = StripeProductType.Startup; break; case PlanType.EnterpriseMonthly: subProductType = StripeProductType.Enterprise; break; case PlanType.EnterpriseYearly: subProductType = StripeProductType.Enterprise; break; } var priceId = mainProductType.HasValue ? _priceIds[mainProductType.Value] : null; var subPriceId = subProductType.HasValue ? _priceIds[subProductType.Value] : null; var subscriptionService = new SubscriptionService(); var subscriptions = (await subscriptionService.ListAsync(new SubscriptionListOptions() { Customer = customerId })).ToList(); // Asigna el método de pago por defecto var options = new PaymentMethodAttachOptions { Customer = customerId, }; CustomerUpdateOptions customerOptions = null; try { if (!string.IsNullOrEmpty(paymentMethodId)) { var service = new PaymentMethodService(); var paymentMethod = await service.AttachAsync(paymentMethodId, options); customerOptions = new CustomerUpdateOptions { InvoiceSettings = new CustomerInvoiceSettingsOptions { DefaultPaymentMethod = paymentMethod.Id }, }; } else { customerOptions = new CustomerUpdateOptions { InvoiceSettings = new CustomerInvoiceSettingsOptions { } }; } } catch(Stripe.StripeException strex) { throw new UserFriendlyException("Error con la tarjeta especificada: " + strex.Message); } var customerService = new CustomerService(); await customerService.UpdateAsync(customerId, customerOptions); //Verifica que no exista ya la suscripción var currentSubscription = subscriptions.SingleOrDefault(x => x.Id == currentSubscriptionId); var currentSubSubscription = subscriptions.SingleOrDefault(x => x.Id == currentSubSubscriptionId); if (!string.IsNullOrEmpty(priceId)) { if (subscriptions.Any(x => x.Items.Any(x => x.Price.Id == priceId))) return new UserSubscription() { PlanId = planTypeId }; //throw new UserFriendlyException("La subscripción especificada ya existe"); } //Crea o actualiza las subscripciones var userCount = await _repository.Query(async (db) => { return await db.ExecuteScalarAsync("SELECT COUNT(*) FROM public.users WHERE tenantid = @tenantId", new { tenantId }); }); if (userCount == 0) userCount = 1; var subscription = await CreateSubscription(tenantId, newUser, planTypeId, customerId, priceId, userCount, true, currentSubscription, currentSubSubscription); var subSubscription = await CreateSubscription(tenantId, null, planTypeId, customerId, subPriceId, -1, false, currentSubSubscription, currentSubscription); return new UserSubscription() { PlanId = planTypeId, SubscriptionId = subscription != null ? subscription.Status : subSubscription.Status, Status = subscription != null ? subscription.Status : subSubscription.Status, }; } private async Task CreateSubscription(string tenantId, User newUser, string planId, string customerId, string priceId, long qty, bool isMainSub, Subscription currentSubscription, Subscription currentSubSubscription) { Console.WriteLine("Creando subscripción " + isMainSub + ", PriceId: " + priceId + ", PlanId: " + planId); Subscription stripeSub = null; var subscriptionId = ""; var subscriptionService = new SubscriptionService(); if (!string.IsNullOrEmpty(priceId)) { if (currentSubscription != null) { qty = currentSubscription.Items.ElementAt(0).Quantity; var service = new SubscriptionItemService(); var options = new SubscriptionItemUpdateOptions() { Price = priceId }; // cuando es !isMainSub falla, porque es una suscripción que se basa en el uso // en ese caso la cantidad representa el uso actual if (isMainSub) options.Quantity = qty; await service.UpdateAsync(currentSubscription.Items.ElementAt(0).Id, options); stripeSub = currentSubscription; subscriptionId = (stripeSub.Id + "?" + stripeSub.Items.ElementAt(0).Id); } else { var subscriptionOptions = new SubscriptionCreateOptions { BillingCycleAnchor = currentSubSubscription != null ? currentSubSubscription.CurrentPeriodEnd : currentSubscription?.CurrentPeriodEnd, Customer = customerId, //BillingCycleAnchor = new DateTime(2021, 02, 01), //TrialPeriodDays = newUser != null ? (long?)28 : null, Items = new List { qty > 0 ? new SubscriptionItemOptions { Price = priceId, Quantity = qty } : new SubscriptionItemOptions { Price = priceId } } }; if(newUser != null) { subscriptionOptions.TrialPeriodDays = 28; //subscriptionOptions.TrialPeriodDays = 2; subscriptionOptions.TrialFromPlan = false; } subscriptionOptions.AddExpand("latest_invoice.payment_intent"); stripeSub = await subscriptionService.CreateAsync(subscriptionOptions); subscriptionId = (stripeSub.Id + "?" + stripeSub.Items.ElementAt(0).Id); if(stripeSub.TrialEnd.HasValue) { if(newUser != null) { newUser.TrialPeriodEnd = stripeSub.TrialEnd.Value; } } //Aquí ya no debería entrar puesto que las subscripciones ya no se cancelan al cambiar entre planes if (qty > 0 && !isMainSub) { var service = new UsageRecordService(); await service.CreateAsync(stripeSub.Items.ElementAt(0).Id, new UsageRecordCreateOptions() { Timestamp = DateTime.UtcNow, Quantity = qty }); } } } else { //No Gratis -> Gratis = Cancelar la subscripción de usuarios (Aquí ya no debería entrar puuesto que ya //se crearon planes gratuitos en Stripe) //No Enterprise -> Enterprise = Cancelar la subscripción ya que Enterprise no contiene folios //(Aquí ya no debería entrar puuesto que ya se crearon planes gratuitos en Stripe) if (currentSubscription != null) { throw new Exception("Assertion failed"); //await subscriptionService.CancelAsync(currentSubscription.Id); } } var propName = !isMainSub ? "stripeSubSubscriptionId" : "stripeSubscriptionId"; await _repository.Query(async (db) => { if (newUser != null) { await db.ExecuteAsync( @$"INSERT INTO users (trialperiodend, sendexternalorderemail, isconfirmed, email, firstName, lastName, password, resetPassword, role, isActive, isClient, stripeCustomerId, stripePlanId, {propName}, created, tenantId) VALUES(@trialperiodend, @sendExternalOrderEmail, @isConfirmed, @email, @firstName, @lastName, @password, @resetPassword, @role, @isActive, @isClient, @customerId, @planId, @subscriptionId, @created, @tenantId)", new { trialperiodend = newUser.TrialPeriodEnd, sendExternalOrderEmail = true, email = newUser.Email, firstName = newUser.FirstName, lastName = newUser.LastName, password = newUser.Password, resetPassword = false, role = SystemUserRole.Owner, isActive = true, isClient = false, isConfirmed = false, customerId, planId, subscriptionId, created = DateTime.UtcNow, tenantId }); } else { await db.ExecuteAsync($"UPDATE users SET {propName} = @subscriptionId, stripePlanId = @planId WHERE tenantid = @tenantId", new { planId, tenantId, subscriptionId }); } return ""; }); return stripeSub; } //private async Task UpdateUserSubscription(int userId, PlanType planType, Subscription subscription, Subscription subSubscription = null) //{ //TODO: //await _repository.Query(async (db) => //{ // await db.ExecuteAsync("UPDATE users SET stripeSubscriptionId = @subscriptionId, stripePlanId = @planId WHERE id = @userId", // new { subscriptionId = subSubscription == null ? (subscription.Id + "?" + subscription.Items.ElementAt(0).Id) // : (subscription.Id + "?" + subscription.Items.ElementAt(0).Id + "?" + subSubscription?.Id), // userId, // planId = ((int)planType).ToString() }); // return ""; //}); //} public async Task> GetPlans() { return SUBSCRIPTION_PLANS; //StripeConfiguration.ApiKey = API_KEY; //var svc = new Stripe.SubscriptionService(); //var priceListOptions = new PriceListOptions(); //priceListOptions.AddExpand("data.tiers"); //var priceService = new Stripe.PriceService(); //var prices = await priceService.ListAsync(priceListOptions); //var productService = new Stripe.ProductService(); //var products = await productService.ListAsync(new ProductListOptions { Limit = 20 }); //return products.Select(x => new SubscriptionPlan //{ // Id = GetPriceId(prices, x.Id), // Name = x.Name, // Description = x.Description, // MainPrice = GetFormattedMainPrice(prices, x.Id), // MainPriceDescription = GetFormattedMainPriceDescription(prices, x.Id), // SubPrice = GetFormattedSubPrice(prices, x.Id), // SubPriceDescription = GetFormattedSubPriceDescription(prices, x.Id) //}); } public async Task CreateCustomer(string tenantId, string email, bool throwExceptionIfExisting = false) { StripeConfiguration.ApiKey = API_KEY; var options = new CustomerCreateOptions { Email = email }; var service = new CustomerService(); var allUsers = (await service.ListAsync(new CustomerListOptions() { Email = email })).ToList(); var existingUser = allUsers.FirstOrDefault(x => x.Email == email); if (throwExceptionIfExisting && existingUser != null) throw new UserFriendlyException("El usuario especificado ya existe"); if (existingUser != null) { await _repository.Update(tenantId, 0, (x) => x.Email == email, new UpdateInfo() { Expression = x => x.StripeCustomerId, Value = existingUser.Id }); return existingUser; } var customer = service.Create(options); await _repository.Update(tenantId, 0, (x) => x.Email == email, new UpdateInfo() { Expression = x => x.StripeCustomerId, Value = customer.Id }); return customer; } public async Task> GetCfdiUsageRecords(string tenantId, int skip, int take, string sortBy, string sortOrder, DateTime startDate, DateTime endDate) { return await _repository.Find(tenantId, x => x.Created >= startDate && x.Created < endDate, skip, take, sortBy, sortOrder); } public async Task GetCfdiUsageRecordsExcel(string tenantId, int offset, string sortBy, string sortOrder, DateTime startDate, DateTime endDate) { return await _repository.GetExcel(tenantId, offset, x => x.Created >= startDate && x.Created < endDate, sortBy, sortOrder); } //public async Task UpdateSubscriptionQuantity(string subscriptionItemId, int quantity) //{ // StripeConfiguration.ApiKey = API_KEY; // var service = new SubscriptionItemService(); // await service.UpdateAsync(subscriptionItemId, new SubscriptionItemUpdateOptions() // { // Quantity = quantity // }); //} //public async Task Test(string customerId) //{ //return null; //StripeConfiguration.ApiKey = API_KEY; //var subscriptionService = new SubscriptionService(); //var subscription = await subscriptionService.CreateAsync(new SubscriptionCreateOptions() //{ // //TrialPeriodDays = 30, // Customer = customerId, // Items = new List // { // new SubscriptionItemOptions // { // Price = "price_1I3yJdARviRAQ4ozj0jG6CQ7" // } // } //}); //return null; //StripeConfiguration.ApiKey = API_KEY; //var service = new UsageRecordService(); //await service.CreateAsync("si_IeV2vPnOQfyNDZ", new UsageRecordCreateOptions() //{ // Timestamp = DateTime.UtcNow, // Quantity = 1 //}); //return null; //sub_IeV2va87PXJbPD //var d = "sub_IeUvrxbUoewJ33"; //var d = "sub_IeV2va87PXJbPD"; //StripeConfiguration.ApiKey = API_KEY; //var service = new InvoiceService(); ////return (await service.ListUpcomingLineItemsAsync(new UpcomingInvoiceListLineItemsOptions() { Subscription = d })).ToList(); //return (await service.UpcomingAsync(new UpcomingInvoiceOptions() { Subscription = d })); //} // //subscriptionItemId = ID DE ELEMENTO DE LA SUSCRIPCIÓN, por ejemplo si_IeV2vPnOQfyNDZ (Ver en Subscripción -> Tarifas) // public async Task ReportUsage(string tenantId, string subscriptionItemId, int userId, string folio, string uuid, CfdiRecordType type, CfdiRecordStatus status) // { // if (tenantId == "test") // { // return; // } // StripeConfiguration.ApiKey = API_KEY; // if (string.IsNullOrEmpty(subscriptionItemId)) return; // var timestamp = DateTime.UtcNow; // var service = new UsageRecordService(); // var response = await service.CreateAsync(subscriptionItemId, new UsageRecordCreateOptions() // { // Timestamp = timestamp, // Quantity = 1 // }); // var record = new CfdiUsageRecord() // { // Created = timestamp, // CreatedById = userId, // Status = status, // Type = type, // Uuid = uuid, // Folio = folio, // SubscriptionItemId = subscriptionItemId, // UsageRecordId = response.Id, // TenantId = tenantId // }; // await _repository.Query(async (db) => // { // await db.QueryAsync(@$" //INSERT INTO cfdiusagerecords (createdbyid, created, status, type, uuid, folio, subscriptionitemid, usagerecordid, tenantid) //VALUES(@createdbyid, @created, @status, @type, @uuid, @folio, @subscriptionitemid, @usagerecordid, @tenantid)", new // { // created = record.Created, // createdbyid = record.CreatedById, // status = record.Status, // type = record.Type, // uuid = record.Uuid, // folio = record.Folio, // subscriptionitemid = record.SubscriptionItemId, // usagerecordid = record.UsageRecordId, // tenantid = record.TenantId // }); // return null; // }); // } public async Task GetUsage(string subSubscriptionId) { StripeConfiguration.ApiKey = API_KEY; if (string.IsNullOrEmpty(subSubscriptionId)) return new CfdiUsageSummary() { HasSubscription = false }; var service = new InvoiceService(); var list = (await service.ListUpcomingLineItemsAsync(new UpcomingInvoiceListLineItemsOptions() { Subscription = subSubscriptionId })).ToList(); return GetSummaryFromInvoiceLines(list); } public async Task CreateNewSubscription(string planTypeId, string paymentMethodId, string email, string firstName, string lastName, string password, string companyName) { var newUser = new User() { TenantId = Guid.NewGuid().ToString("N"), Email = email, FirstName = firstName, LastName = lastName, Password = BCrypt.Net.BCrypt.HashPassword(password, UsersService.HASH) }; var customer = await CreateCustomer(newUser.TenantId, email, true); //Crea la subscripción en Stripe await CreateOrUpdateSubscription(newUser.TenantId, customer.Id, null, null, paymentMethodId, planTypeId, newUser); _emailService.SendEmail(false, "noreply@misingredientes.mx", new string[] { "hola@misingredientes.mx" }, "Nuevo usuario registrado", $"Se registró un nuevo usuario con correo {email}"); return await _repository.FindOne(newUser.TenantId, x => x.Email == email); } //public async Task CreateNewSubscriptionNoCard(string planTypeId, string email, string firstName, // string lastName, string password, string companyName) //{ // var newUser = new User() // { // TenantId = Guid.NewGuid().ToString("N"), // Email = email, // FirstName = firstName, // LastName = lastName, // TrialPeriodEnd = DateTime.UtcNow, // Password = BCrypt.Net.BCrypt.HashPassword(password, UsersService.HASH) // }; // var customer = await CreateCustomer(newUser.TenantId, email, true); // _emailService.SendEmail(false, "noreply@misingredientes.mx", new string[] { "hola@misingredientes.mx" }, "Nuevo usuario registrado", // $"Se registró un nuevo usuario con correo {email}"); // return await _repository.FindOne(newUser.TenantId, x => x.Email == email); //} public async Task GetUpcomingInvoice(string subscriptionId, string subSubscriptionId) { if(string.IsNullOrEmpty(subscriptionId) && string.IsNullOrEmpty(subSubscriptionId)) { return new BillingInvoice() { Lines = new BillingInvoiceLine[] { }, Usage = new CfdiUsageSummary() { } }; } StripeConfiguration.ApiKey = API_KEY; var service = new InvoiceService(); //return (await service.ListUpcomingLineItemsAsync(new UpcomingInvoiceListLineItemsOptions() { Subscription = d })).ToList(); var upcoming = !string.IsNullOrEmpty(subscriptionId) ? (await service.UpcomingAsync(new UpcomingInvoiceOptions() { Subscription = subscriptionId })) : null; var upcomingSub = !string.IsNullOrEmpty(subSubscriptionId) ? (await service.UpcomingAsync(new UpcomingInvoiceOptions() { Subscription = subSubscriptionId })) : null; //Console.WriteLine(JsonConvert.SerializeObject(upcoming)); //Console.WriteLine("----------------------------------"); //Console.WriteLine(JsonConvert.SerializeObject(upcomingSub)); //Console.WriteLine("----------------------------------"); return new BillingInvoice() { NextPaymentDate = upcomingSub != null ? upcomingSub.NextPaymentAttempt : upcoming.NextPaymentAttempt, StartDateTime = upcomingSub != null ? upcomingSub.PeriodStart : upcoming.PeriodStart, EndDateTime = upcomingSub != null ? upcomingSub.PeriodEnd : upcoming.PeriodEnd, StartDateTimeTimestamp = upcomingSub != null ? upcomingSub.PeriodStart.ToFileTimeUtc() : upcoming.PeriodStart.ToFileTimeUtc(), EndDateTimeTimestamp = upcomingSub != null ? upcomingSub.PeriodEnd.ToFileTimeUtc() : upcoming.PeriodEnd.ToFileTimeUtc(), Lines = GetBillingInvoiceLines(upcoming?.Lines, upcomingSub?.Lines), Subtotal = (Convert.ToDecimal(upcoming?.Subtotal ?? 0) / 100M) + (Convert.ToDecimal(upcomingSub?.Subtotal ?? 0) / 100M), Total = (Convert.ToDecimal(upcoming?.Total ?? 0) / 100M) + (Convert.ToDecimal(upcomingSub?.Total ?? 0) / 100M), Usage = GetSummaryFromInvoiceLines(upcomingSub?.Lines?.ToList()) }; } private string GetPlanNameFromDescription(string description) { description = description.Split('×')[1]; description = description.Split('(')[0]; return description.Trim(); } private DateTime ConvertFromUnixTimestamp(double timestamp) { DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0); return origin.AddSeconds(timestamp); } private BillingInvoiceLine[] GetBillingInvoiceLines(StripeList upcomingLines, StripeList upcomingSubLines) { var list = new List(); if (upcomingLines != null) { if (upcomingLines.Count() == 1 && upcomingLines.ElementAt(0).Description.Contains("Trial")) { //Aquí no debería de entrar puesto que en los folios de facturación no hay trial list.Add(new BillingInvoiceLine() { Description = "Periodo de prueba", Amount = 0, Cost = 0, Qty = 0, Currency = upcomingLines.ElementAt(0).Currency }); } else { //var item = upcomingLines.ElementAt(0); //var qty = item.Quantity ?? 0; //var amount = Convert.ToDecimal(item.Amount) / 100M; //list.Add(new BillingInvoiceLine() //{ // Description = GetPlanNameFromDescription(upcomingLines.ElementAt(0).Description), // Amount = amount, // Cost = qty > 0 ? (amount / qty) : 0, // Qty = qty, // Currency = upcomingLines.ElementAt(0).Currency //}); foreach(var invoiceLine in upcomingLines) { list.Add(new BillingInvoiceLine() { Description = invoiceLine.Description.Replace("month", "mes").Replace("year", "año").Replace("(at ", "(a ").Replace("after","después de").Replace("Unused time on","Tiempo no usado en").Replace("Remaining time on", "Tiempo restante en"), Amount = Convert.ToDecimal(invoiceLine.Amount) / 100m, Cost = Convert.ToDecimal(invoiceLine.Price.UnitAmount) / 100m, Qty = invoiceLine.Quantity ?? 0, Period = $"{ConvertFromUnixTimestamp(invoiceLine.Period.Start).ToString("dd/MM/yyyy")} - {ConvertFromUnixTimestamp(invoiceLine.Period.End).ToString("dd/MM/yyyy")}", Currency = invoiceLine.Currency }); } } } if(upcomingSubLines != null) { if (upcomingSubLines.Count() == 1 && upcomingSubLines.ElementAt(0).Description.Contains("Trial")) { //Aquí no debería de entrar puesto que en los folios de facturación no hay trial list.Add(new BillingInvoiceLine() { Description = "Periodo de prueba", Amount = 0, Cost = 0, Qty = 0, Currency = upcomingSubLines.ElementAt(0).Currency }); } else if(upcomingSubLines.Count() >= 3) { var item = upcomingSubLines.ElementAt(2); var qty = item.Quantity ?? 0; var amount = Convert.ToDecimal(item.Amount) / 100M; list.Add(new BillingInvoiceLine() { Description = "Folio adicional no incluido en la suscripción", Amount = amount, Cost = qty > 0 ? (amount / qty) : 0, Qty = qty, Currency = item.Currency }); } else { if (list.Count == 0) { list.Add(new BillingInvoiceLine() { Description = "Plan Gratis", Amount = 0, Cost = 0, Qty = 1, Currency = "mxn" }); } list.Add(new BillingInvoiceLine() { Description = "Folio adicional no incluido en la suscripción", Amount = 0, Cost = 0, Qty = 0, Currency = "mxn" }); } } return list.ToArray(); //return upcomingLines.Select(x => new BillingInvoiceLine() //{ // Description = x.Description, // Amount = (Convert.ToDecimal(x.Amount) / 100M), // Qty = x.Quantity ?? 0, // Currency = x.Currency //}).ToArray(); } private CfdiUsageSummary GetSummaryFromInvoiceLines(List lines) { if (lines != null && lines.Count >= 3) { var line = lines[2]; var total = Convert.ToDecimal(line.Amount) / 100M; var qty = line.Quantity ?? 0; return new CfdiUsageSummary() { HasSubscription = true, Qty = qty, Cost = qty > 0 ? (total / Convert.ToDecimal(qty)) : 0, Total = total }; } return new CfdiUsageSummary() { HasSubscription = true, Qty = 0, Total = 0 }; } } public class BillingInvoice { public DateTime? NextPaymentDate { get; set; } public DateTime StartDateTime { get; set; } public DateTime EndDateTime { get; set; } public long StartDateTimeTimestamp { get; set; } public long EndDateTimeTimestamp { get; set; } public BillingInvoiceLine[] Lines { get; set; } public decimal Total { get; set; } public decimal Subtotal { get; set; } public CfdiUsageSummary Usage { get; set; } } public class BillingInvoiceLine { public string Description { get; set; } public decimal Amount { get; set; } public decimal Cost { get; set; } public string Currency { get; set; } public string Period { get; set; } public long Qty { get; set; } } public class CfdiUsageSummary { public bool HasSubscription { get; set; } public decimal Total { get; set; } public long Qty { get; set; } public decimal Cost { get; set; } } }