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