Пример #1
0
        public async Task<DomainBalanceHistory> AddAsync(DomainBalanceHistory record)
        {
            BalanceHistoryEntity entity = _mapper.Map<DomainBalanceHistory, BalanceHistoryEntity>(record);
            entity.Date = DateTime.UtcNow;

            entity = await _balanceHistoryRepository.AddAsync(entity);
            DomainBalanceHistory result = _mapper.Map<BalanceHistoryEntity, DomainBalanceHistory>(entity);

            return result;
        }
        public void TestInaccauntableClicks()
        {
            // 1. Arrange

            // config
            _config.Setup(x => x.SubscriptionPlans)
                .Returns(
                    () =>
                        JObject.Parse(
                            "{ 0: { syncPeriodMs: 0, cyclePeriodMs: 0, clickRates: { 0: 0 } }, 1: { syncPeriodMs: 60000, cyclePeriodMs: 2592000000, clickRates: { 0: 0, 20: 10 } }, 2: { syncPeriodMs: 2592000000, cyclePeriodMs: 2592000000, clickRates: { 0: 0, 100000: 0.05, 1000000: 0.01 } }, 3: { syncPeriodMs: 2592000000, cyclePeriodMs: 2592000000, clickRates: { 0: 0, 100000: 0.05, 1000000: 0.01 } } }"));
            _config.Setup(x => x.BillingInvoiceItemDescriptionTemplate).Returns(() => "{0}");

            // stats service
            var stas = new List<TrackingStatEntity>
            {
                new TrackingStatEntity { Date = new DateTime(2014, 6, 4, 14, 28, 35, 596) },
                new TrackingStatEntity { Date = new DateTime(2014, 6, 4, 14, 28, 36, 995) },
                new TrackingStatEntity { Date = new DateTime(2014, 6, 4, 14, 28, 41, 889) },
                new TrackingStatEntity { Date = new DateTime(2014, 6, 4, 14, 28, 44, 249) },
                new TrackingStatEntity { Date = new DateTime(2014, 6, 4, 14, 28, 48, 335) }
            };
            _statService.Setup(x => x.GetTotalAsync(It.IsAny<string>(), It.IsAny<DateTime?>(), It.IsAny<DateTime?>())).Returns<string, DateTime?, DateTime?>((i, f, t) =>
            {
                IQueryable<TrackingStatEntity> r = stas.AsQueryable();
                if (f.HasValue)
                {
                    r = r.Where(s => s.Date >= f.Value);
                }

                if (t.HasValue)
                {
                    r = r.Where(s => s.Date < t.Value);
                }

                return Task.FromResult(r.LongCount());
            });

            // company service
            var company = new DomainCompany
            {
                State = ResourceState.Available,
                Subscriptions = new List<CompanySubscription>
                {
                    new CompanySubscription
                    {
                        State = ResourceState.Available,
                        Type = SubscriptionType.Basic,
                        Created = new DateTime(2014, 6, 4, 14, 25, 14, 133),
                        LastSyncDate = null,
                        LastCycleDate = new DateTime(2014, 6, 4, 14, 27, 37, 939),
                        HasTrialClicks = false
                    }
                }
            };
            var companies = new List<DomainCompany> { company };
            _companyService.Setup(x => x.ListAsync()).Returns(() => Task.FromResult(companies.AsEnumerable()));

            // balance service
            var balance = new DomainBalanceHistory();
            _balanceService.Setup(x => x.AddAsync(It.IsAny<DomainBalanceHistory>())).Returns<DomainBalanceHistory>(b =>
            {
                balance = b;
                return Task.FromResult(balance);
            });


            // 2. Act
            var syncDate = new DateTime(2014, 6, 4, 14, 28, 37, 939);
            _syncManager.SyncAsync(syncDate).Wait();


            // 3. Assert
            long actualCount = Int64.Parse(balance.Description);
            Assert.AreEqual(2, actualCount);
        }
Пример #3
0
        private async Task ChargeSucceededAsync(DomainEvent billingEvent)
        {
            // Retrieve company by charge data
            DomainCharge charge = await _billingChargeService.GetAsync(new DomainCharge { Id = billingEvent.ObjectId });
            DomainCompany company = await _companyService.FindByCustomerAsync(charge.CustomerId);

            // Updating balance
            var balanceRecord = new DomainBalanceHistory
            {
                Amount = charge.AmountInCents,
                Description = BillingMessages.ChargeSucceeded,
                CompanyId = company.Id
            };

            await _balanceService.AddAsync(balanceRecord);

            // Notify client about payment operation result
            try
            {
                await _notificationService.SendPaymentNotificationAsync(billingEvent, company, charge);
            }
            catch (Exception e)
            {
                Trace.TraceError("Failed to send payment notification e-mail to company {0}: {1}", company.Id, e);
            }
        }
Пример #4
0
        private async Task ChargeRefundedAsync(DomainEvent billingEvent)
        {
            // Get charge info
            DomainCharge charge = await _billingChargeService.GetAsync(new DomainCharge { Id = billingEvent.ObjectId });

            var refunds = billingEvent.Object["refunds"] as JArray;
            if (refunds != null)
            {
                // refund can be partial, accounting only last refund
                JToken lastRefund = refunds.Last;
                int refundInCents = Int32.Parse(lastRefund["amount"].ToString());

                charge.AmountInCents = refundInCents;
            }

            // Retrieve company by customer
            DomainCompany company = await _companyService.FindByCustomerAsync(charge.CustomerId);

            // updating balance
            var balanceRecord = new DomainBalanceHistory
            {
                Amount = -charge.AmountInCents,
                Description = BillingMessages.ChargeRefunded,
                CompanyId = company.Id
            };

            await _balanceService.AddAsync(balanceRecord);

            // Notify client about payment operation result
            try
            {
                await _notificationService.SendPaymentNotificationAsync(billingEvent, company, charge);
            }
            catch (Exception e)
            {
                Trace.TraceError("Failed to send payment notification e-mail to company {0}: {1}", company.Id, e);
            }
        }
        private async Task SyncSubscriptionAsync(string companyId, CompanySubscription subscription, DateTime syncDate, JObject subscriptionPlans)
        {
            if (subscription.Type == SubscriptionType.Free)
            {
                // nothing to sync
                return;
            }

            JToken planInfo = subscriptionPlans[((int)subscription.Type).ToString(CultureInfo.InvariantCulture)];

            // Check invoice period
            DateTime lastSyncDate = subscription.LastSyncDate ?? subscription.Created;
            long syncPeriodMs = Int64.Parse((string)planInfo["syncPeriodMs"]);

            if (syncPeriodMs <= 0 || syncDate.Subtract(lastSyncDate).TotalMilliseconds < syncPeriodMs)
            {
                // its too early for invoice
                return;
            }

            // Calculate clicks for sync period
            long periodClicks = await _statService.GetTotalAsync(subscription.Id, lastSyncDate, syncDate);

            // Calculate clicks for cycle
            DateTime lastCycleDate = subscription.LastCycleDate ?? subscription.Created;
            long cycleClicks = await _statService.GetTotalAsync(subscription.Id, lastCycleDate, lastSyncDate);

            JToken clickRatesInfo = planInfo["clickRates"];
            var charges = new List<Tuple<decimal, long, long, long?>>();
            long clicks = periodClicks;
            decimal lastRate = 0;
            long lastCount = 0;
            long clicksMargin = cycleClicks;
            foreach (JToken clickRate in clickRatesInfo)
            {
                var r = clickRate as JProperty;
                if (r == null)
                {
                    continue;
                }

                long count = Int64.Parse(r.Name);
                decimal rate = Decimal.Parse((string)r.Value);
                if (count <= 0)
                {
                    // skip free/initial rate range
                    lastRate = rate;
                    continue;
                }

                if (clicksMargin >= count)
                {
                    // skip last bill
                    lastRate = rate;
                    lastCount = count;
                    continue;
                }

                // calculate charge for range of clicks
                long lastRange = Math.Max(clicksMargin, lastCount);
                long rangeClicks = lastRange + clicks < count ? clicks : count - lastRange;

                // reseting margin
                clicksMargin = 0;

                // creating charge
                if (rangeClicks > 0)
                {
                    charges.Add(new Tuple<decimal, long, long, long?>(lastRate, rangeClicks, lastCount, count));
                }


                // going to next range
                lastRate = rate;
                lastCount = count;
                clicks -= rangeClicks;
                if (clicks == 0)
                {
                    break;
                }
            }

            if (clicks > 0)
            {
                // using last specified rate for exceeded clicks
                charges.Add(new Tuple<decimal, long, long, long?>(lastRate, clicks, lastCount, null));
            }


            // Synchronizing
            var chargeRecords = new List<DomainBalanceHistory>();
            try
            {
                if (charges.Count > 0)
                {
                    if (charges.Sum(c => c.Item1) > 0)
                    {
                        // trial clicks ended
                        await _subscriptionService.UpdateHasTrialClicksAsync(subscription.Id, false);
                    }
                    else
                    {
                        // trial clicks
                        await _subscriptionService.UpdateHasTrialClicksAsync(subscription.Id, true);
                    }

                    foreach (var charge in charges)
                    {
                        decimal rate = charge.Item1;
                        long count = charge.Item2;
                        long lowCount = charge.Item3;
                        long? highCount = charge.Item4;
                        string packageName = highCount.HasValue ? string.Format("{0}-{1}", lowCount, highCount) : string.Format("{0}+", lowCount);
                        var balanceRecord = new DomainBalanceHistory
                        {
                            CompanyId = companyId,
                            Amount = -(rate*count),
                            Description =
                                string.Format(_config.BillingInvoiceItemDescriptionTemplate, count, subscription.SiteName, subscription.Id, subscription.Type, rate/100, packageName, lastSyncDate,
                                    syncDate)
                        };

                        await _balanceService.AddAsync(balanceRecord);
                        chargeRecords.Add(balanceRecord);
                    }
                }

                // cycle completed
                long cyclePeriodMs = Int64.Parse((string)planInfo["cyclePeriodMs"]);
                if (cyclePeriodMs > 0 && syncDate.Subtract(lastCycleDate).TotalMilliseconds >= cyclePeriodMs)
                {
                    // updating last cycle date if cycle completed
                    await _subscriptionService.UpdateLastCycleDateAsync(subscription.Id, syncDate);

                    // enabling trial clicks
                    await _subscriptionService.UpdateHasTrialClicksAsync(subscription.Id, true);
                }

                // updating last sync date
                await _subscriptionService.UpdateLastSyncDateAsync(subscription.Id, syncDate);
            }
            catch (Exception e)
            {
                Trace.TraceError("Failed to sync balance for subscription {0} of company {1}: {2}", companyId, subscription.Id, e);

                // trying rollback
                try
                {
                    // rollback balance
                    if (chargeRecords.Count > 0)
                    {
                        foreach (DomainBalanceHistory record in chargeRecords)
                        {
                            _balanceService.DeleteAsync(record.Id).Wait();
                        }
                    }

                    // rollback subscription state
                    _subscriptionService.UpdateLastCycleDateAsync(subscription.Id, lastCycleDate).Wait();
                    _subscriptionService.UpdateHasTrialClicksAsync(subscription.Id, subscription.HasTrialClicks).Wait();
                }
                catch (Exception ex)
                {
                    Trace.TraceError("Failed to roll back subscription {0} state for company {1}. Subscription data is corrupted: {2}", subscription.Id, companyId, ex);
                }
            }
        }