<commit_before>﻿using Microsoft.EntityFrameworkCore;
using Money.Events;
using Money.Models.Queries;
using Money.Models.Sorting;
using Neptuo;
using Neptuo.Activators;
using Neptuo.Events.Handlers;
using Neptuo.Models.Keys;
using Neptuo.Queries.Handlers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Money.Models.Builders
{
    public class OutcomeBuilder : IEventHandler<OutcomeCreated>,
        IEventHandler<OutcomeCategoryAdded>,
        IEventHandler<OutcomeAmountChanged>,
        IEventHandler<OutcomeDescriptionChanged>,
        IEventHandler<OutcomeWhenChanged>,
        IEventHandler<OutcomeDeleted>,
        IQueryHandler<ListMonthWithOutcome, List<MonthModel>>,
        IQueryHandler<ListYearWithOutcome, List<YearModel>>,
        IQueryHandler<ListMonthCategoryWithOutcome, List<CategoryWithAmountModel>>,
        IQueryHandler<ListYearCategoryWithOutcome, List<CategoryWithAmountModel>>,
        IQueryHandler<GetTotalMonthOutcome, Price>,
        IQueryHandler<GetTotalYearOutcome, Price>,
        IQueryHandler<GetCategoryName, string>,
        IQueryHandler<GetCategoryColor, Color>,
        IQueryHandler<ListMonthOutcomeFromCategory, List<OutcomeOverviewModel>>,
        IQueryHandler<ListYearOutcomeFromCategory, List<OutcomeOverviewModel>>,
        IQueryHandler<SearchOutcomes, List<OutcomeOverviewModel>>,
        IQueryHandler<ListMonthOutcomesForCategory, List<MonthWithAmountModel>>,
        IQueryHandler<ListYearOutcomesForCategory, List<YearWithAmountModel>>
    {
        const int PageSize = 10;

        private readonly IFactory<ReadModelContext> dbFactory;
        private readonly IPriceConverter priceConverter;

        internal OutcomeBuilder(IFactory<ReadModelContext> dbFactory, IPriceConverter priceConverter)
        {
            Ensure.NotNull(dbFactory, "dbFactory");
            Ensure.NotNull(priceConverter, "priceConverter");
            this.dbFactory = dbFactory;
            this.priceConverter = priceConverter;
        }

        public async Task<List<MonthModel>> HandleAsync(ListMonthWithOutcome query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                var entities = await db.Outcomes
                    .WhereUserKey(query.UserKey)
                    .Select(o => new { Year = o.When.Year, Month = o.When.Month })
                    .Distinct()
                    .OrderByDescending(o => o.Year)
                    .ThenByDescending(o => o.Month)
                    .ToListAsync();

                return entities
                    .Select(o => new MonthModel(o.Year, o.Month))
                    .ToList();
            }
        }

        public async Task<List<YearModel>> HandleAsync(ListYearWithOutcome query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                var entities = await db.Outcomes
                    .WhereUserKey(query.UserKey)
                    .Select(o => o.When.Year)
                    .Distinct()
                    .OrderByDescending(o => o)
                    .ToListAsync();

                return entities
                    .Select(o => new YearModel(o))
                    .ToList();
            }
        }

        private async Task<List<CategoryWithAmountModel>> GetCategoryWithAmounts(ReadModelContext db, IKey userKey, List<OutcomeEntity> outcomes)
        {
            Dictionary<Guid, Price> totals = new Dictionary<Guid, Price>();

            foreach (OutcomeEntity outcome in outcomes)
            {
                foreach (OutcomeCategoryEntity category in outcome.Categories)
                {
                    Price price;
                    if (totals.TryGetValue(category.CategoryId, out price))
                        price = price + priceConverter.ToDefault(userKey, outcome);
                    else
                        price = priceConverter.ToDefault(userKey, outcome);

                    totals[category.CategoryId] = price;
                }
            }

            List<CategoryWithAmountModel> result = new List<CategoryWithAmountModel>();
            foreach (var item in totals)
            {
                CategoryEntity entity = await db.Categories.FirstOrDefaultAsync(c => c.Id == item.Key);
                if (entity == null)
                    throw Ensure.Exception.InvalidOperation($"Missing category with id '{item.Key}'.");

                CategoryModel model = entity.ToModel();
                result.Add(new CategoryWithAmountModel(
                    model.Key,
                    model.Name,
                    model.Description,
                    model.Color,
                    model.Icon,
                    item.Value
                ));
            }

            return result.OrderBy(m => m.Name).ToList();
        }

        public async Task<List<CategoryWithAmountModel>> HandleAsync(ListMonthCategoryWithOutcome query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                List<OutcomeEntity> outcomes = await db.Outcomes
                    .WhereUserKey(query.UserKey)
                    .Where(o => o.When.Month == query.Month.Month && o.When.Year == query.Month.Year)
                    .Where(o => o.IsFixed == false)
                    .Include(o => o.Categories)
                    .ToListAsync();

                return await GetCategoryWithAmounts(db, query.UserKey, outcomes);
            }
        }

        public async Task<List<CategoryWithAmountModel>> HandleAsync(ListYearCategoryWithOutcome query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                List<OutcomeEntity> outcomes = await db.Outcomes
                    .WhereUserKey(query.UserKey)
                    .Where(o => o.When.Year == query.Year.Year)
                    .Where(o => o.IsFixed == false)
                    .Include(o => o.Categories)
                    .ToListAsync();

                return await GetCategoryWithAmounts(db, query.UserKey, outcomes);
            }
        }

        public async Task<string> HandleAsync(GetCategoryName query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                CategoryEntity category = await db.Categories.FindAsync(query.CategoryKey.AsGuidKey().Guid);
                if (category != null && category.IsUserKey(query.UserKey))
                    return category.Name;

                throw Ensure.Exception.ArgumentOutOfRange("categoryKey", "No such category with key '{0}'.", query.CategoryKey);
            }
        }<commit_msg>#377 - Category with only fixed expenses in summary period is shown with zero price.<commit_after>﻿using Microsoft.EntityFrameworkCore;
using Money.Events;
using Money.Models.Queries;
using Money.Models.Sorting;
using Neptuo;
using Neptuo.Activators;
using Neptuo.Events.Handlers;
using Neptuo.Models.Keys;
using Neptuo.Queries.Handlers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Money.Models.Builders
{
    public class OutcomeBuilder : IEventHandler<OutcomeCreated>,
        IEventHandler<OutcomeCategoryAdded>,
        IEventHandler<OutcomeAmountChanged>,
        IEventHandler<OutcomeDescriptionChanged>,
        IEventHandler<OutcomeWhenChanged>,
        IEventHandler<OutcomeDeleted>,
        IQueryHandler<ListMonthWithOutcome, List<MonthModel>>,
        IQueryHandler<ListYearWithOutcome, List<YearModel>>,
        IQueryHandler<ListMonthCategoryWithOutcome, List<CategoryWithAmountModel>>,
        IQueryHandler<ListYearCategoryWithOutcome, List<CategoryWithAmountModel>>,
        IQueryHandler<GetTotalMonthOutcome, Price>,
        IQueryHandler<GetTotalYearOutcome, Price>,
        IQueryHandler<GetCategoryName, string>,
        IQueryHandler<GetCategoryColor, Color>,
        IQueryHandler<ListMonthOutcomeFromCategory, List<OutcomeOverviewModel>>,
        IQueryHandler<ListYearOutcomeFromCategory, List<OutcomeOverviewModel>>,
        IQueryHandler<SearchOutcomes, List<OutcomeOverviewModel>>,
        IQueryHandler<ListMonthOutcomesForCategory, List<MonthWithAmountModel>>,
        IQueryHandler<ListYearOutcomesForCategory, List<YearWithAmountModel>>
    {
        const int PageSize = 10;

        private readonly IFactory<ReadModelContext> dbFactory;
        private readonly IPriceConverter priceConverter;

        internal OutcomeBuilder(IFactory<ReadModelContext> dbFactory, IPriceConverter priceConverter)
        {
            Ensure.NotNull(dbFactory, "dbFactory");
            Ensure.NotNull(priceConverter, "priceConverter");
            this.dbFactory = dbFactory;
            this.priceConverter = priceConverter;
        }

        public async Task<List<MonthModel>> HandleAsync(ListMonthWithOutcome query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                var entities = await db.Outcomes
                    .WhereUserKey(query.UserKey)
                    .Select(o => new { Year = o.When.Year, Month = o.When.Month })
                    .Distinct()
                    .OrderByDescending(o => o.Year)
                    .ThenByDescending(o => o.Month)
                    .ToListAsync();

                return entities
                    .Select(o => new MonthModel(o.Year, o.Month))
                    .ToList();
            }
        }

        public async Task<List<YearModel>> HandleAsync(ListYearWithOutcome query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                var entities = await db.Outcomes
                    .WhereUserKey(query.UserKey)
                    .Select(o => o.When.Year)
                    .Distinct()
                    .OrderByDescending(o => o)
                    .ToListAsync();

                return entities
                    .Select(o => new YearModel(o))
                    .ToList();
            }
        }

        private async Task<List<CategoryWithAmountModel>> GetCategoryWithAmounts(ReadModelContext db, IKey userKey, List<OutcomeEntity> outcomes)
        {
            Dictionary<Guid, Price> totals = new Dictionary<Guid, Price>();

            foreach (OutcomeEntity outcome in outcomes)
            {
                foreach (OutcomeCategoryEntity category in outcome.Categories)
                {
                    Price price;
                    if (totals.TryGetValue(category.CategoryId, out price))
                    {
                        if (!outcome.IsFixed)
                            price = price + priceConverter.ToDefault(userKey, outcome);
                    }
                    else
                    {
                        if (!outcome.IsFixed)
                            price = priceConverter.ToDefault(userKey, outcome);
                        else
                            price = priceConverter.ZeroDefault(userKey);
                    }

                    totals[category.CategoryId] = price;
                }
            }

            List<CategoryWithAmountModel> result = new List<CategoryWithAmountModel>();
            foreach (var item in totals)
            {
                CategoryEntity entity = await db.Categories.FirstOrDefaultAsync(c => c.Id == item.Key);
                if (entity == null)
                    throw Ensure.Exception.InvalidOperation($"Missing category with id '{item.Key}'.");

                CategoryModel model = entity.ToModel();
                result.Add(new CategoryWithAmountModel(
                    model.Key,
                    model.Name,
                    model.Description,
                    model.Color,
                    model.Icon,
                    item.Value
                ));
            }

            return result.OrderBy(m => m.Name).ToList();
        }

        public async Task<List<CategoryWithAmountModel>> HandleAsync(ListMonthCategoryWithOutcome query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                List<OutcomeEntity> outcomes = await db.Outcomes
                    .WhereUserKey(query.UserKey)
                    .Where(o => o.When.Month == query.Month.Month && o.When.Year == query.Month.Year)
                    .Include(o => o.Categories)
                    .ToListAsync();

                return await GetCategoryWithAmounts(db, query.UserKey, outcomes);
            }
        }

        public async Task<List<CategoryWithAmountModel>> HandleAsync(ListYearCategoryWithOutcome query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                List<OutcomeEntity> outcomes = await db.Outcomes
                    .WhereUserKey(query.UserKey)
                    .Where(o => o.When.Year == query.Year.Year)
                    .Include(o => o.Categories)
                    .ToListAsync();

                return await GetCategoryWithAmounts(db, query.UserKey, outcomes);
            }
        }

        public async Task<string> HandleAsync(GetCategoryName query)
        {
            using (ReadModelContext db = dbFactory.Create())
            {
                CategoryEntity category = await db.Categories.FindAsync(query.CategoryKey.AsGuidKey().Guid);
                if (category != null && category.IsUserKey(query.UserKey))
                    return category.Name;

                throw Ensure.Exception.ArgumentOutOfRange("categoryKey", "No such category with key '{0}'.", query.CategoryKey);
            }
        }
{'fraction_non_alphanumeric': 0.08737081287106942, 'fraction_numerical': 0.0006596789562412959, 'mean_word_length': 1.8436848686952898, 'pattern_counts': {'":': 0, '<': 99, '<?xml version=': 0, '>': 127, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2391242', 'n_tokens_mistral': 3412, 'n_tokens_neox': 3263, 'n_words': 642}