示例#1
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);
            }
        }
示例#2
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);
        }
示例#3
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);
            }
        }
示例#4
0
        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);
        }
示例#5
0
        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);
                }
            }
        }