public override void LoadCache(Action <string, object> act, params object[] args) { var db = new BillingwareDataContext(); var statsBase = db.Transactions.GroupBy(a => a.AccountNumber); //sum and count of all debits and credits act($"sum.debit", db.Transactions.Where(t => t.TransactionType == TransactionType.Debit).Select(s => s.Amount).DefaultIfEmpty(0).Sum()); act($"sum.credit", db.Transactions.Where(t => t.TransactionType == TransactionType.Credit).Select(s => s.Amount).DefaultIfEmpty(0).Sum()); act($"count", db.Transactions.LongCount()); act($"count.credit", db.Transactions.Where(t => t.TransactionType == TransactionType.Credit).LongCount()); act($"count.debit", db.Transactions.Where(t => t.TransactionType == TransactionType.Debit).LongCount()); //sum and count of individual debit and credits foreach (var item in statsBase) { act($"count.{item.Key}", item.LongCount()); act($"count.debit.{item.Key}", item.LongCount(t => t.TransactionType == TransactionType.Debit)); act($"count.credit.{item.Key}", item.LongCount(t => t.TransactionType == TransactionType.Credit)); act($"sum.debit.{item.Key}", item.Where(t => t.TransactionType == TransactionType.Debit).Select(s => s.Amount).DefaultIfEmpty(0).Sum()); act($"sum.credit.{item.Key}", item.Where(t => t.TransactionType == TransactionType.Credit).Select(s => s.Amount).DefaultIfEmpty(0).Sum()); } }
private void DoPersistTransaction(PersistTransaction req) { using (var db = new BillingwareDataContext()) { var t = new Transaction { AccountNumber = req.AccountNumber, Amount = req.Amount, TransactionType = req.TransactionType, Reference = req.Reference, BalanceAfter = req.BalanceAfter, BalanceBefore = req.BalanceBefore, ClientId = req.ClientId, ConditionFailReason = req.ConditionFailReason, CreatedAt = req.CreatedAt, Honoured = req.Honoured, Narration = req.Narration, SatisfiedCondition = req.SatisfiedCondition, Ticket = req.Ticket }; //quick fix if (t.TransactionType == TransactionType.Debit) { //if balance = 100, amount=10 => after=90 and before=100 var sumBefore = t.BalanceAfter + t.Amount; if (t.BalanceBefore != sumBefore) { t.BalanceBefore = t.BalanceAfter + (t.Amount); } } else { //if balance = 90, amount=10 => after=100 and before=90 var sumBefore = t.BalanceAfter - t.Amount; if (t.BalanceBefore != sumBefore) { t.BalanceBefore = t.BalanceAfter - (t.Amount); } } db.Transactions.Add(t); db.SaveChanges(); } //increase cache items var typeKey = req.TransactionType == TransactionType.Credit ? "credit" : "debit"; _tranactionsStatsCache.Increase($"count", int.Parse("1")); _tranactionsStatsCache.Increase($"count.{typeKey}", int.Parse("1")); _tranactionsStatsCache.Increase($"count.{typeKey}.{req.AccountNumber}", int.Parse("1")); _tranactionsStatsCache.Increase($"sum.{typeKey}", req.Amount); _tranactionsStatsCache.Increase($"sum.{typeKey}.{req.AccountNumber}", req.Amount); }
private void DoCreditAccountEvent(CreditAccount detail) { using (var db = new BillingwareDataContext()) { var account = db.Accounts.FirstOrDefault(a => a.AccountNumber == detail.Request.AccountNumber); if (account == null) { return; } account.Balance += detail.Request.Amount; db.Entry(account).State = EntityState.Modified; db.SaveChanges(); } }
private void DoCreateAccount(CreateAccount req) { var db = new BillingwareDataContext(); db.Accounts.Add(new Account { AccountNumber = req.AccountNumber, Balance = decimal.Zero, Extra = req.Extra, Alias = req.Alias, CreatedAt = DateTime.Now }); db.SaveChanges(); Sender.Tell(new AccountCreated(new CommonStatusResponse(message: "Successful")), Self); }
private void DoEditAccount(EditAccount req) { var db = new BillingwareDataContext(); var account = db.Accounts.FirstOrDefault(a => a.AccountNumber == req.AccountNumber); if (account == null) { Sender.Tell(new AccountEdited(new CommonStatusResponse(message: "Not found", code: "404", subCode: "404.1")), Self); return; } account.Alias = req.Alias; account.Extra = req.Extra; db.Entry(account).State = EntityState.Modified; db.SaveChanges(); Sender.Tell(new AccountEdited(new CommonStatusResponse(message: "Successful")), Self); }
private void DoRequestAccountDebit(RequestAccountDebit request) { Log(CommonLogLevel.Debug, $"received request to debit {request.AccountNumber} with {request.Amount}", null, null); //we generate a unique ticket for every transaction var ticket = Guid.NewGuid().ToString("N"); /* * 1. Find the account * 2. Execute conditions on account - we shall use Expression Trees * 3. Apply outcome * 4. Persist results */ var db = new BillingwareDataContext(); var account = db.Accounts.AsNoTracking().FirstOrDefault(a => a.AccountNumber == request.AccountNumber); if (account == null) { Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, decimal.Zero, decimal.Zero, new CommonStatusResponse(message: $"account {request.AccountNumber} not found.", code: "404", subCode: "404.1")), Self); return; } var generalConditions = db.Conditions.AsNoTracking().Where(c => c.Active && c.ConditionApplicatorType == ConditionApplicatorType.All).ToList() ; var specificConditions = db.Conditions.AsNoTracking().Where(c => c.Active && c.ConditionApplicatorType == ConditionApplicatorType.One && c.AppliedToAccountNumbers.Contains(request.AccountNumber)).ToList(); var allTransactionsCount = long.Parse(_tranactionsStatsCache.Find($"count.{request.AccountNumber}").Result.ToString()); var debitTransactionSum = decimal.Parse(_tranactionsStatsCache.Find($"sum.debit.{request.AccountNumber}").Result.ToString()); var creditTransactionSum = decimal.Parse(_tranactionsStatsCache.Find($"sum.credit.{request.AccountNumber}").Result.ToString()); var debitTransactionCount = long.Parse(_tranactionsStatsCache.Find($"count.debit.{request.AccountNumber}").Result.ToString()); var creditTransactionCount = long.Parse(_tranactionsStatsCache.Find($"count.credit.{request.AccountNumber}").Result.ToString()); // var allConditions = new List<Condition>(); bool passedSpecificConditions = false; if (specificConditions.Any()) { //allConditions.AddRange(specificConditions); var perAccountOutcomeList = ConditionEvaluatorHelper.EvaluateConditionAndGetOutcomeIds(account, specificConditions, new ClientPayloadData("debit", request.Reference, request.Amount), creditTransactionSum, creditTransactionCount, debitTransactionSum, debitTransactionCount, allTransactionsCount); if (perAccountOutcomeList.Any()) { //check if there's any "halt" type in the outcome list...then, //raise an event to actually inspect the outcome of the condition evaluation //it would also increment the stats cache values var outcomes = db.Outcomes.AsNoTracking().Where(o => perAccountOutcomeList.Contains(o.Id) && o.Active) .OrderBy(i => i.Id); if (!outcomes.Any()) { Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, account.Balance , new CommonStatusResponse(message: $"condition was satisfied but no active outcome could be applied.", code: "500", subCode: "500.1")), Self); return; } if (outcomes.Any(o => o.OutcomeType == OutcomeType.Halt)) { Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, account.Balance, new CommonStatusResponse(message: $"condition was satisfied. but at least one outcome HALTs the process", code: "403", subCode: "403.1")), Self); return; } passedSpecificConditions = true; //Publish(new DebitAccount(request)); //Publish(new PersistTransaction(request.AccountNumber, DateTime.Now, request.Narration, request.Amount, // account.Balance, account.Balance - request.Amount, TransactionType.Debit, true, string.Empty, true, // request.ClientId, request.Reference, ticket)); //Sender.Tell( // new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, // account.Balance - request.Amount, new CommonStatusResponse(message: "Successful")), Self); //return; } } if (generalConditions.Any()) { // allConditions.AddRange(generalConditions); var generalOutcomeList = ConditionEvaluatorHelper.EvaluateConditionAndGetOutcomeIds(account, generalConditions, new ClientPayloadData("debit", request.Reference, request.Amount), creditTransactionSum, creditTransactionCount, debitTransactionSum, debitTransactionCount, allTransactionsCount); if (generalOutcomeList.Any()) { //check if there's any "halt" type in the outcome list...then, //raise an event to actually inspect the outcome of the condition evaluation //it would also increment the stats cache values var outcomes = db.Outcomes.AsNoTracking().Where(o => generalOutcomeList.Contains(o.Id) && o.Active) .OrderBy(i => i.Id); if (!outcomes.Any()) { //but..... if (passedSpecificConditions) { Publish(new DebitAccount(request)); Publish(new PersistTransaction(request.AccountNumber, DateTime.Now, request.Narration, request.Amount, account.Balance, account.Balance - request.Amount, TransactionType.Debit, true, string.Empty, true, request.ClientId, request.Reference, ticket)); Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, account.Balance - request.Amount, new CommonStatusResponse(message: "Successful")), Self); return; } Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, account.Balance, new CommonStatusResponse(message: $"condition was satisfied but no active outcome could be applied.", code: "500", subCode: "500.1")), Self); return; } if (outcomes.Any(o => o.OutcomeType == OutcomeType.Halt)) { //but..... if (passedSpecificConditions) { Publish(new DebitAccount(request)); Publish(new PersistTransaction(request.AccountNumber, DateTime.Now, request.Narration, request.Amount, account.Balance, account.Balance - request.Amount, TransactionType.Debit, true, string.Empty, true, request.ClientId, request.Reference, ticket)); Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, account.Balance - request.Amount, new CommonStatusResponse(message: "Successful")), Self); return; } Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, account.Balance, new CommonStatusResponse(message: $"condition was satisfied. but at least one outcome HALTs the process", code: "403", subCode: "403.1")), Self); return; } Publish(new DebitAccount(request)); Publish(new PersistTransaction(request.AccountNumber, DateTime.Now, request.Narration, request.Amount, account.Balance, account.Balance - request.Amount, TransactionType.Debit, true, string.Empty, true, request.ClientId, request.Reference, ticket)); Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, account.Balance - request.Amount, new CommonStatusResponse(message: "Successful")), Self); return; } } //if there are no conditions so we still proceed with the intended action Publish(new DebitAccount(request)); Publish(new PersistTransaction(request.AccountNumber, DateTime.Now, request.Narration, request.Amount, account.Balance, account.Balance - request.Amount, TransactionType.Debit, false, "no condition", true, request.ClientId, request.Reference, ticket)); Sender.Tell( new AccountDebitResponse(request.Reference, request.AccountNumber, ticket, request.Amount, account.Balance, account.Balance - request.Amount, new CommonStatusResponse(message: "Successful")), Self); return; }
public void CreateSampleCondition() { //arrange //create condition for the following case: //when the request is "debit" and the account balance <=0, halt the process var whenRequestIsDebit = new Condition { Active = true, ConditionApplicatorType = ConditionApplicatorType.All, ConditionConnector = ConditionConnector.And, Key = ComparatorKey.Custom, KeyExpression = "$.Payload.TransactionType", Type = ComparatorType.EqualTo, Name = "halt_no_balance", Value = "debit", OutcomeId = 1 //same outcome }; var whenAccountHasNoMoney = new Condition { Active = true, ConditionApplicatorType = ConditionApplicatorType.All, ConditionConnector = ConditionConnector.None, Key = ComparatorKey.Balance, Type = ComparatorType.LessThanOrEqualTo, Name = "halt_no_balance", Value = "0", OutcomeId = 1 //same outcome }; //when account has "allowOverdraft=true" in their Extras var whenAccountHasAllowOverdraftFlag = new Condition { Active = true, ConditionApplicatorType = ConditionApplicatorType.One, AppliedToAccountNumbers = "9237452718263673", ConditionConnector = ConditionConnector.None, Key = ComparatorKey.Custom, KeyExpression = "$.Extra.allowOverdraft", Type = ComparatorType.EqualTo, Name = "halt_no_balance", Value = "true", OutcomeId = 1 //same outcome }; var outcome = new ConditionOutcome { Active = true, OutcomeType = OutcomeType.Halt }; //act var db = new BillingwareDataContext(); db.Conditions.Add(whenRequestIsDebit); db.Conditions.Add(whenAccountHasNoMoney); db.Conditions.Add(whenAccountHasAllowOverdraftFlag); db.Outcomes.Add(outcome); db.SaveChanges(); //assert Assert.True(whenRequestIsDebit.Id > 0); }
public override void LoadCache(Action <string, object> act, params object[] args) { var db = new BillingwareDataContext(); }