From ee14a6cb16bae8124a312982f9bf97eb3f6c9c5e Mon Sep 17 00:00:00 2001 From: barhen Date: Sun, 28 May 2023 23:11:57 -0500 Subject: [PATCH 1/4] Analysis backend initial feature --- Server/BiskAcdbContext.cs | 5 +- Server/BiskilogContext.cs | 7 + Server/Controllers/AnalyticsController.cs | 96 ++++++++++ Server/Program.cs | 3 + Server/Services/AnalyticalService.cs | 165 ++++++++++++++++++ Server/Services/AuthenticationService.cs | 3 +- Server/Services/ConnectionService.cs | 2 +- Shared/ClientContractModels/Clientbusiness.cs | 1 + Shared/CustomModels/CancelledSales.cs | 16 ++ Shared/CustomModels/InDebtCustomers.cs | 15 ++ Shared/CustomModels/ProductItem.cs | 16 ++ Shared/CustomModels/ProductPriceChange.cs | 23 +++ Shared/Interfaces/IAnalytics.cs | 53 ++++++ Shared/Interfaces/ITokenService.cs | 4 +- Shared/ServiceRepo/TokenService.cs | 32 +++- 15 files changed, 433 insertions(+), 8 deletions(-) create mode 100644 Server/Controllers/AnalyticsController.cs create mode 100644 Server/Services/AnalyticalService.cs create mode 100644 Shared/CustomModels/CancelledSales.cs create mode 100644 Shared/CustomModels/InDebtCustomers.cs create mode 100644 Shared/CustomModels/ProductItem.cs create mode 100644 Shared/CustomModels/ProductPriceChange.cs create mode 100644 Shared/Interfaces/IAnalytics.cs diff --git a/Server/BiskAcdbContext.cs b/Server/BiskAcdbContext.cs index 10468ab..0583fb5 100644 --- a/Server/BiskAcdbContext.cs +++ b/Server/BiskAcdbContext.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Biskilog_Accounting.Shared.POSModels; +using Biskilog_Accounting.Shared.POSModels; using Microsoft.EntityFrameworkCore; namespace Biskilog_Accounting.Server.POSModels; @@ -15,7 +13,6 @@ public partial class BiskAcdbContext : DbContext : base(options) { } - public virtual DbSet Creditpurchases { get; set; } public virtual DbSet Customeraccounts { get; set; } diff --git a/Server/BiskilogContext.cs b/Server/BiskilogContext.cs index 0795270..fd6d206 100644 --- a/Server/BiskilogContext.cs +++ b/Server/BiskilogContext.cs @@ -90,6 +90,13 @@ public partial class BiskilogContext : DbContext .HasDefaultValueSql("current_timestamp()") .HasColumnType("datetime") .HasColumnName("date_joined"); + entity.Property(e => e.BusinessExternalId) + .HasMaxLength(50) + .HasDefaultValueSql("''") + .HasColumnName("businessExternalId") + .UseCollation("utf8mb4_general_ci") + .HasCharSet("utf8mb4"); + }); modelBuilder.Entity(entity => diff --git a/Server/Controllers/AnalyticsController.cs b/Server/Controllers/AnalyticsController.cs new file mode 100644 index 0000000..73156e7 --- /dev/null +++ b/Server/Controllers/AnalyticsController.cs @@ -0,0 +1,96 @@ +using Biskilog_Accounting.Server.POSModels; +using Biskilog_Accounting.Server.Services; +using Biskilog_Accounting.Shared.CustomModels; +using Biskilog_Accounting.Shared.Interfaces; +using Biskilog_Accounting.Shared.POSModels; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Net.Http.Headers; +using System.ComponentModel.DataAnnotations; +using System.Data.Entity; + +namespace Biskilog_Accounting.Server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class AnalyticsController : ControllerBase + { + private readonly IConnectionService m_connection; + private readonly ITokenService m_tokenService; + public AnalyticsController(ITokenService tokenService, IConnectionService connection) + { + m_tokenService = tokenService; + m_connection = connection; + } + + /// + /// Endpoint to return analysis on CancelledSales within a specified period + /// + /// + /// + [Authorize] + [HttpGet, Route("cancelledsales/{a_start}/{a_end}")] + public IEnumerable GetCancelledSalesAsync(DateTime a_start, DateTime a_end) + { + string token = Request.Headers[HeaderNames.Authorization]!; + int? databaseId = m_tokenService.GetDatabaseIdFromToken(token); + string connectionString = m_connection.GetClientConnectionString(databaseId!.Value); + bool? comparisonMode = m_tokenService.GetComparison(token); + string? currentBranch = m_tokenService.GetBaseBranch(token); + + //Creates a new db context + BiskAcdbContext newContext = (BiskAcdbContext)m_connection.PrepareDBContext(connectionString); + + AnalyticalService analysis = new(newContext, comparisonMode!.Value, currentBranch!); + var result = analysis.GetCancelledSales(a_start, a_end); + + return result; + } + /// + /// Endpoint to return analysis on Sales within a specified period + /// + /// + /// + [Authorize] + [HttpGet, Route("sales/{a_start}/{a_end}")] + public IEnumerable GetSalesAsync(DateTime a_start, DateTime a_end) + { + string token = Request.Headers[HeaderNames.Authorization]!; + int? databaseId = m_tokenService.GetDatabaseIdFromToken(token); + string connectionString = m_connection.GetClientConnectionString(databaseId!.Value); + bool? comparisonMode = m_tokenService.GetComparison(token); + string? currentBranch = m_tokenService.GetBaseBranch(token); + + //Creates a new db context + BiskAcdbContext newContext = (BiskAcdbContext)m_connection.PrepareDBContext(connectionString); + + AnalyticalService analysis = new(newContext, comparisonMode!.Value, currentBranch!); + return analysis.GetSalesTransaction(a_start, a_end); + } + /// + /// Endpoint to return analysis on in-debt customers + /// + /// + /// + [Authorize] + [HttpGet, Route("debtors")] + public IEnumerable GetInDebtCustomers() + { + string token = Request.Headers[HeaderNames.Authorization]!; + int? databaseId = m_tokenService.GetDatabaseIdFromToken(token); + string connectionString = m_connection.GetClientConnectionString(databaseId!.Value); + bool? comparisonMode = m_tokenService.GetComparison(token); + string? currentBranch = m_tokenService.GetBaseBranch(token); + + //Creates a new db context + BiskAcdbContext newContext = (BiskAcdbContext)m_connection.PrepareDBContext(connectionString); + + AnalyticalService analysis = new(newContext, comparisonMode!.Value, currentBranch!); + return analysis.GetInDebtCustomers(); + } + + } +} diff --git a/Server/Program.cs b/Server/Program.cs index b898305..da1c2bf 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -9,6 +9,7 @@ using Biskilog_Accounting.Server; using Biskilog_Accounting.ServiceRepo; using Biskilog_Accounting.Shared.Interfaces; using Biskilog_Accounting.Server.Services; +using Biskilog_Accounting.Server.POSModels; var builder = WebApplication.CreateBuilder(args); @@ -21,8 +22,10 @@ builder.Services.AddEntityFrameworkMySql().AddDbContext(options { options.UseMySql(builder.Configuration.GetConnectionString("Connection"), new MariaDbServerVersion(new Version())); }); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddCors(options => { diff --git a/Server/Services/AnalyticalService.cs b/Server/Services/AnalyticalService.cs new file mode 100644 index 0000000..6c2fc08 --- /dev/null +++ b/Server/Services/AnalyticalService.cs @@ -0,0 +1,165 @@ +using Biskilog_Accounting.Server.POSModels; +using Biskilog_Accounting.Shared.CustomModels; +using Biskilog_Accounting.Shared.Interfaces; +using Biskilog_Accounting.Shared.POSModels; +using Microsoft.EntityFrameworkCore; +using System.Data.Entity; +using System.Runtime.CompilerServices; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace Biskilog_Accounting.Server.Services +{ + /// + /// Gets the KPIs/ Analysis of operations made with the software + /// + public class AnalyticalService : IAnalytics + { + private readonly BiskAcdbContext m_context; + private bool m_comparisonMode; + private string m_activeBranch; + public AnalyticalService(BiskAcdbContext a_context, bool a_comparisonMode, string a_activeBranchId) + { + m_context = a_context; + m_comparisonMode = a_comparisonMode; + m_activeBranch = a_activeBranchId; + } + public IEnumerable GetCancelledSales(DateTime a_start, DateTime a_end) + { + //If in comparison mode, the list is fetched from all branchid of the business + if (m_comparisonMode) + { + return from cSale in m_context.Tblcancelledtransactions + join aSale in m_context.Tblcarts on cSale.Transno equals aSale.Transno into activeSale + join cPurchase in m_context.Tblcustomerpurchases on cSale.Transno equals cPurchase.TransactionId into customerSales + from c in customerSales.DefaultIfEmpty() + join customer in m_context.Tblcustomers on c.CustomerId equals customer.CustomerId into Customers + from cc in Customers.DefaultIfEmpty() + where cSale.DateCancelled >= a_start && cSale.DateCancelled <= a_end + select new CancelledSales + { + CancelledTransaction = cSale, + Value = activeSale.Sum(s => s.Total), + Customer = !String.IsNullOrEmpty(cc.CustomerId) ? $"{cc.Firstname} {cc.Surname}" : "Walk-IN Purchase" + }; + } + else + { + return from cSale in m_context.Tblcancelledtransactions + join aSale in m_context.Tblcarts on cSale.Transno equals aSale.Transno into activeSale + join cPurchase in m_context.Tblcustomerpurchases on cSale.Transno equals cPurchase.TransactionId into customerSales + from c in customerSales.DefaultIfEmpty() + join customer in m_context.Tblcustomers on c.CustomerId equals customer.CustomerId into Customers + from cc in Customers.DefaultIfEmpty() + where cSale.DateCancelled >= a_start && cSale.DateCancelled <= a_end && cSale.BranchId == m_activeBranch + select new CancelledSales + { + CancelledTransaction = cSale, + Value = (from a in activeSale where a.BranchId == m_activeBranch select a.Total).Sum(), + Customer = !String.IsNullOrEmpty(cc.CustomerId) ? $"{cc.Firstname} {cc.Surname}" : "Walk-IN Purchase" + }; + + } + } + + public IEnumerable>> GetEmployeeSales(DateTime a_start, DateTime a_end) + { + throw new NotImplementedException(); + } + + public IEnumerable GetInDebtCustomers() + { + if (m_comparisonMode) + { + return from c in m_context.Tblcustomers + join a in m_context.Customeraccounts on c.CustomerId equals a.CustomerId into CustomerAccounts + from ac in CustomerAccounts.OrderByDescending(ac => ac.Date).Take(1).DefaultIfEmpty() + where ac.Balance < 0 + orderby ac.Date descending + select new InDebtCustomers + { + Customer = c, + Debt = ac != null ? ac.Balance : 0 + }; + + } + else + { + return null; + } + } + + public IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end) + { + throw new NotImplementedException(); + } + + public IEnumerable GetOutOfStockItems() + { + //if (m_comparisonMode) + //{ + // return from r in m_context.Restocklevels + // join i in m_context.Tblinventories on r.ProductId equals i.Pcode + // join p in m_context.Tblproducts on i.Pcode equals p.Pcode + // where i.Quantity == r.WarnLevel + // select new ProductItem + // { + // Product = p, + // Stock = i, + // Unitofmeasure = + // } + //} + //else { + //} + return null; + } + + public IEnumerable GetPriceChanges(DateTime a_start, DateTime a_end) + { + if (m_comparisonMode) + { + return from change in m_context.Tblpricechanges + join p in m_context.Tblproducts on change.Pcode equals p.Pcode + where change.ChangeDate <= a_start && change.ChangeDate >= a_end + select new ProductPriceChange + { + BranchId = change.BranchId, + ChangeDate = change.ChangeDate, + Pcode = change.Pcode, + CountId = change.CountId, + CurrentPrice = change.CurrentPrice, + PreviousPrice = change.PreviousPrice, + ProductName = p.ProductName + }; + } + else + { + return from change in m_context.Tblpricechanges + join p in m_context.Tblproducts on change.Pcode equals p.Pcode + where change.ChangeDate <= a_start && change.ChangeDate >= a_end && change.BranchId == m_activeBranch + select new ProductPriceChange + { + BranchId = change.BranchId, + ChangeDate = change.ChangeDate, + Pcode = change.Pcode, + CountId = change.CountId, + CurrentPrice = change.CurrentPrice, + PreviousPrice = change.PreviousPrice, + ProductName = p.ProductName + }; + } + } + + public IEnumerable GetSalesTransaction(DateTime a_start, DateTime a_end) + { + //If in comparison mode, the list is fetched from all branchid of the business + if (m_comparisonMode) + { + return m_context.Tblcarts.Where(t => t.Date >= a_start && t.Date <= a_end); + } + else + { + return m_context.Tblcarts.Where(t => t.Date >= a_start && t.Date <= a_end && t.BranchId == m_activeBranch); + } + } + } +} diff --git a/Server/Services/AuthenticationService.cs b/Server/Services/AuthenticationService.cs index 172071b..6ca95f6 100644 --- a/Server/Services/AuthenticationService.cs +++ b/Server/Services/AuthenticationService.cs @@ -55,11 +55,12 @@ namespace Biskilog_Accounting.Server.Services List businessIds = GetSiteaccesspermission(user.ClientId, user.UserId).Select(t => t.BusinessId).ToList(); Contract? contract = GetContract(user.ClientId, businessIds); + List businesses = GetClientbusiness(user.ClientId); if (contract == null) return AuthEnums.Invalid.ToString(); - return m_tokenService.GenerateToken(user, contract, databasemap); + return m_tokenService.GenerateToken(user, contract, databasemap, businesses[0], false); } /// diff --git a/Server/Services/ConnectionService.cs b/Server/Services/ConnectionService.cs index fa89b02..54bd806 100644 --- a/Server/Services/ConnectionService.cs +++ b/Server/Services/ConnectionService.cs @@ -46,7 +46,7 @@ namespace Biskilog_Accounting.Server.Services DbContextOptionsBuilder acdbContext = new DbContextOptionsBuilder(); acdbContext.UseMySql(a_connectionString, new MariaDbServerVersion(new Version())); - return acdbContext; + return new BiskAcdbContext(acdbContext.Options); } } } diff --git a/Shared/ClientContractModels/Clientbusiness.cs b/Shared/ClientContractModels/Clientbusiness.cs index 83a4577..661f01b 100644 --- a/Shared/ClientContractModels/Clientbusiness.cs +++ b/Shared/ClientContractModels/Clientbusiness.cs @@ -17,4 +17,5 @@ public partial class Clientbusiness public string BiskilogVersion { get; set; } = null!; public DateTime DateJoined { get; set; } + public string BusinessExternalId { get; set; } = string.Empty!; } diff --git a/Shared/CustomModels/CancelledSales.cs b/Shared/CustomModels/CancelledSales.cs new file mode 100644 index 0000000..c7d60f3 --- /dev/null +++ b/Shared/CustomModels/CancelledSales.cs @@ -0,0 +1,16 @@ +using Biskilog_Accounting.Shared.POSModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Biskilog_Accounting.Shared.CustomModels +{ + public class CancelledSales + { + public Tblcancelledtransaction? CancelledTransaction { get; set; } + public string Customer { get; set; } = "WALK-IN Purchase"; + public decimal? Value { get; set; } + } +} diff --git a/Shared/CustomModels/InDebtCustomers.cs b/Shared/CustomModels/InDebtCustomers.cs new file mode 100644 index 0000000..07ab50f --- /dev/null +++ b/Shared/CustomModels/InDebtCustomers.cs @@ -0,0 +1,15 @@ +using Biskilog_Accounting.Shared.POSModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Biskilog_Accounting.Shared.CustomModels +{ + public class InDebtCustomers + { + public Tblcustomer Customer { get; set; } + public decimal Debt { get; set; } = 0; + } +} diff --git a/Shared/CustomModels/ProductItem.cs b/Shared/CustomModels/ProductItem.cs new file mode 100644 index 0000000..6db6f2b --- /dev/null +++ b/Shared/CustomModels/ProductItem.cs @@ -0,0 +1,16 @@ +using Biskilog_Accounting.Shared.POSModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Biskilog_Accounting.Shared.CustomModels +{ + public class ProductItem + { + public Tblproduct? Product { get; set; } + public Tblinventory? Stock { get; set; } + public List Unitofmeasure { get; set; } = new List(); + } +} diff --git a/Shared/CustomModels/ProductPriceChange.cs b/Shared/CustomModels/ProductPriceChange.cs new file mode 100644 index 0000000..7cd9131 --- /dev/null +++ b/Shared/CustomModels/ProductPriceChange.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Biskilog_Accounting.Shared.CustomModels +{ + public class ProductPriceChange + { + public string? Pcode { get; set; } + public string? ProductName { get; set; } + public decimal? PreviousPrice { get; set; } + + public decimal? CurrentPrice { get; set; } + + public DateTime? ChangeDate { get; set; } + + public string BranchId { get; set; } = null!; + + public string CountId { get; set; } = null!; + } +} diff --git a/Shared/Interfaces/IAnalytics.cs b/Shared/Interfaces/IAnalytics.cs new file mode 100644 index 0000000..b22b92f --- /dev/null +++ b/Shared/Interfaces/IAnalytics.cs @@ -0,0 +1,53 @@ +using Biskilog_Accounting.Shared.CustomModels; +using Biskilog_Accounting.Shared.POSModels; + +namespace Biskilog_Accounting.Shared.Interfaces +{ + public interface IAnalytics + { + /// + /// Fetches a collection of sales transaction made from the specified start date to the end date + /// + /// Specified Start Date + /// Specified end Date + /// + IEnumerable GetSalesTransaction(DateTime a_start, DateTime a_end); + /// + /// Fetches a collection of in-debt customers + /// + IEnumerable GetInDebtCustomers(); + /// + /// Fetches a collection of Product Items which are currently out of stock + /// + /// + IEnumerable GetOutOfStockItems(); + /// + /// Fetches a collection of the most purchased Product Items within a specified date range + /// + /// + /// + /// + IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end); + /// + /// Fetches a collection of cancelled transaction within a specified date range + /// + /// + /// + /// + IEnumerable GetCancelledSales(DateTime a_start, DateTime a_end); + /// + /// Fetches a collection of transaction made by employees within a specified date range + /// + /// + /// + /// A dictionary of transactions made by employees with employee name as key + IEnumerable>> GetEmployeeSales(DateTime a_start, DateTime a_end); + /// + /// Fetches a collection of product price changes with a specified date range + /// + /// + /// + /// + IEnumerable GetPriceChanges(DateTime a_start, DateTime a_end); + } +} diff --git a/Shared/Interfaces/ITokenService.cs b/Shared/Interfaces/ITokenService.cs index 730926f..27cdf82 100644 --- a/Shared/Interfaces/ITokenService.cs +++ b/Shared/Interfaces/ITokenService.cs @@ -6,10 +6,12 @@ namespace Biskilog_Accounting.Shared.Interfaces public interface ITokenService { AuthEnums ValidateToken(string a_token); - string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database); + string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database, Clientbusiness a_business, bool a_comparison); int? GetDatabaseIdFromToken(string a_token); int? GetUserIdFromToken(string a_token); string? GetUserNameFromToken(string a_token); + string? GetBaseBranch(string a_token); + bool? GetComparison(string a_token); } } diff --git a/Shared/ServiceRepo/TokenService.cs b/Shared/ServiceRepo/TokenService.cs index 8d3639a..022137e 100644 --- a/Shared/ServiceRepo/TokenService.cs +++ b/Shared/ServiceRepo/TokenService.cs @@ -43,7 +43,7 @@ namespace Biskilog_Accounting.ServiceRepo /// Generates an access token based on the user /// /// A tokenized string - public string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database) + public string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database, Clientbusiness a_business, bool a_comparison) { try { @@ -56,6 +56,8 @@ namespace Biskilog_Accounting.ServiceRepo new Claim("UserId", a_user.UserId.ToString()), new Claim("Username", a_user.Username.ToString()), new Claim("DbId",a_database.DbNo.ToString()), + new Claim("ComparisonMode",a_comparison.ToString()), + new Claim("BranchId",a_business.BusinessExternalId.ToString()), new Claim("ClientId", a_user.ClientId.ToString()), }; @@ -120,5 +122,33 @@ namespace Biskilog_Accounting.ServiceRepo } return null; } + /// + ///Deserializes the token string if valid to return the specified branchId in the token string + /// + /// + /// Username + public string? GetBaseBranch(string a_token) + { + if (ValidateToken(a_token) == AuthEnums.Valid) + { + string token = a_token.Substring(6).Trim(); + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtToken = (JwtSecurityToken)handler.ReadToken(token); + return jwtToken.Claims.First(claim => claim.Type == "BranchId").Value; + } + return null; + } + + public bool? GetComparison(string a_token) + { + if (ValidateToken(a_token) == AuthEnums.Valid) + { + string token = a_token.Substring(6).Trim(); + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtToken = (JwtSecurityToken)handler.ReadToken(token); + return bool.Parse(jwtToken.Claims.First(claim => claim.Type == "ComparisonMode").Value); + } + return null; + } } } From 781cb2ad95d36ba94290a6010434331c3753f468 Mon Sep 17 00:00:00 2001 From: barhen Date: Mon, 29 May 2023 14:16:59 -0500 Subject: [PATCH 2/4] Reworked on-the-fly dbcontext creation and db connection --- Server/BiskAcdbContext.cs | 34 +++++++++++++++++++++++++--- Server/Program.cs | 6 +++-- Server/Services/ConnectionService.cs | 12 ++++------ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/Server/BiskAcdbContext.cs b/Server/BiskAcdbContext.cs index 0583fb5..d0ba890 100644 --- a/Server/BiskAcdbContext.cs +++ b/Server/BiskAcdbContext.cs @@ -1,17 +1,45 @@ -using Biskilog_Accounting.Shared.POSModels; +using Azure.Core; +using Biskilog_Accounting.Shared.Enums; +using Biskilog_Accounting.Shared.Interfaces; +using Biskilog_Accounting.Shared.POSModels; using Microsoft.EntityFrameworkCore; +using Microsoft.Net.Http.Headers; +using System.Security.Claims; namespace Biskilog_Accounting.Server.POSModels; public partial class BiskAcdbContext : DbContext { + private readonly HttpContext m_httpContext; + private readonly IConnectionService m_connection; + private readonly ITokenService m_tokenService; public BiskAcdbContext() { } + public BiskAcdbContext(DbContextOptions options, ITokenService tokenService, IConnectionService connection, IHttpContextAccessor a_httpContextAccessor = null) + : base(options) + { + m_tokenService = tokenService; + m_connection = connection; + m_httpContext = a_httpContextAccessor?.HttpContext; + } - public BiskAcdbContext(DbContextOptions options) - : base(options) + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + if (!optionsBuilder.IsConfigured) + { + string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!; + if (AuthEnums.Valid == m_tokenService.ValidateToken(token)) + { + int? databaseId = m_tokenService.GetDatabaseIdFromToken(token); + string connectionString = m_connection.GetClientConnectionString(databaseId!.Value); + optionsBuilder.UseMySql(connectionString, new MariaDbServerVersion(new Version())); + } + else + { + m_httpContext.Abort(); + } + } } public virtual DbSet Creditpurchases { get; set; } diff --git a/Server/Program.cs b/Server/Program.cs index da1c2bf..7e20b15 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -22,10 +22,12 @@ builder.Services.AddEntityFrameworkMySql().AddDbContext(options { options.UseMySql(builder.Configuration.GetConnectionString("Connection"), new MariaDbServerVersion(new Version())); }); -builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddDbContext(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddCors(options => { diff --git a/Server/Services/ConnectionService.cs b/Server/Services/ConnectionService.cs index 54bd806..6fa3e8c 100644 --- a/Server/Services/ConnectionService.cs +++ b/Server/Services/ConnectionService.cs @@ -10,13 +10,11 @@ namespace Biskilog_Accounting.Server.Services public class ConnectionService : IConnectionService { private readonly BiskilogContext m_context; - private readonly ITokenService m_tokenService; private readonly IConfiguration m_configuration; - public ConnectionService(BiskilogContext a_context, ITokenService a_tokenService, IConfiguration configuration) + public ConnectionService(BiskilogContext a_context, IConfiguration configuration) { m_context = a_context; - m_tokenService = a_tokenService; m_configuration = configuration; } /// @@ -42,11 +40,11 @@ namespace Biskilog_Accounting.Server.Services /// A configured BiskAcdbContext public object PrepareDBContext(string a_connectionString) { - - DbContextOptionsBuilder acdbContext = new DbContextOptionsBuilder(); - acdbContext.UseMySql(a_connectionString, new MariaDbServerVersion(new Version())); + throw new NotImplementedException(); + //DbContextOptionsBuilder acdbContext = new DbContextOptionsBuilder(); + //acdbContext.UseMySql(a_connectionString, new MariaDbServerVersion(new Version())); - return new BiskAcdbContext(acdbContext.Options); + //return new BiskAcdbContext(acdbContext.Options); } } } From e912e6279d5b8a33bdc33bc1787d54ad4b80b2fb Mon Sep 17 00:00:00 2001 From: barhen Date: Mon, 29 May 2023 14:17:20 -0500 Subject: [PATCH 3/4] Analysis controller completed --- Server/Controllers/AnalyticsController.cs | 104 ++++++++----- Server/Services/AnalyticalService.cs | 172 +++++++++++++++++----- Shared/CustomModels/MostPurchaseItem.cs | 29 ++++ Shared/CustomModels/ProductItem.cs | 2 +- Shared/CustomModels/SaleItem.cs | 22 +++ Shared/Interfaces/IAnalytics.cs | 5 +- 6 files changed, 254 insertions(+), 80 deletions(-) create mode 100644 Shared/CustomModels/MostPurchaseItem.cs create mode 100644 Shared/CustomModels/SaleItem.cs diff --git a/Server/Controllers/AnalyticsController.cs b/Server/Controllers/AnalyticsController.cs index 73156e7..b09e292 100644 --- a/Server/Controllers/AnalyticsController.cs +++ b/Server/Controllers/AnalyticsController.cs @@ -1,16 +1,10 @@ -using Biskilog_Accounting.Server.POSModels; -using Biskilog_Accounting.Server.Services; -using Biskilog_Accounting.Shared.CustomModels; +using Biskilog_Accounting.Shared.CustomModels; using Biskilog_Accounting.Shared.Interfaces; using Biskilog_Accounting.Shared.POSModels; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Net.Http.Headers; -using System.ComponentModel.DataAnnotations; -using System.Data.Entity; +using NuGet.Common; namespace Biskilog_Accounting.Server.Controllers { @@ -18,12 +12,12 @@ namespace Biskilog_Accounting.Server.Controllers [ApiController] public class AnalyticsController : ControllerBase { - private readonly IConnectionService m_connection; private readonly ITokenService m_tokenService; - public AnalyticsController(ITokenService tokenService, IConnectionService connection) + private readonly IAnalytics m_analyticService; + public AnalyticsController(ITokenService tokenService, IAnalytics a_analytics) { m_tokenService = tokenService; - m_connection = connection; + m_analyticService = a_analytics; } /// @@ -36,18 +30,9 @@ namespace Biskilog_Accounting.Server.Controllers public IEnumerable GetCancelledSalesAsync(DateTime a_start, DateTime a_end) { string token = Request.Headers[HeaderNames.Authorization]!; - int? databaseId = m_tokenService.GetDatabaseIdFromToken(token); - string connectionString = m_connection.GetClientConnectionString(databaseId!.Value); - bool? comparisonMode = m_tokenService.GetComparison(token); - string? currentBranch = m_tokenService.GetBaseBranch(token); + m_analyticService.SetContraints(token); - //Creates a new db context - BiskAcdbContext newContext = (BiskAcdbContext)m_connection.PrepareDBContext(connectionString); - - AnalyticalService analysis = new(newContext, comparisonMode!.Value, currentBranch!); - var result = analysis.GetCancelledSales(a_start, a_end); - - return result; + return m_analyticService.GetCancelledSales(a_start, a_end); } /// /// Endpoint to return analysis on Sales within a specified period @@ -59,16 +44,9 @@ namespace Biskilog_Accounting.Server.Controllers public IEnumerable GetSalesAsync(DateTime a_start, DateTime a_end) { string token = Request.Headers[HeaderNames.Authorization]!; - int? databaseId = m_tokenService.GetDatabaseIdFromToken(token); - string connectionString = m_connection.GetClientConnectionString(databaseId!.Value); - bool? comparisonMode = m_tokenService.GetComparison(token); - string? currentBranch = m_tokenService.GetBaseBranch(token); - - //Creates a new db context - BiskAcdbContext newContext = (BiskAcdbContext)m_connection.PrepareDBContext(connectionString); - AnalyticalService analysis = new(newContext, comparisonMode!.Value, currentBranch!); - return analysis.GetSalesTransaction(a_start, a_end); + m_analyticService.SetContraints(token); + return m_analyticService.GetSalesTransaction(a_start, a_end); } /// /// Endpoint to return analysis on in-debt customers @@ -80,17 +58,65 @@ namespace Biskilog_Accounting.Server.Controllers public IEnumerable GetInDebtCustomers() { string token = Request.Headers[HeaderNames.Authorization]!; - int? databaseId = m_tokenService.GetDatabaseIdFromToken(token); - string connectionString = m_connection.GetClientConnectionString(databaseId!.Value); - bool? comparisonMode = m_tokenService.GetComparison(token); - string? currentBranch = m_tokenService.GetBaseBranch(token); + + m_analyticService.SetContraints(token); + return m_analyticService.GetInDebtCustomers(); + } + /// + /// Endpoint to return analysis on product price changes + /// + /// + /// + [Authorize] + [HttpGet, Route("pricechanges/{a_start}/{a_end}")] + public IEnumerable GetPriceChanges(DateTime a_start, DateTime a_end) + { + string token = Request.Headers[HeaderNames.Authorization]!; + + m_analyticService.SetContraints(token); + return m_analyticService.GetPriceChanges(a_start, a_end); + } + /// + /// Endpoint to return analysis on sales by employees + /// + /// + /// + [Authorize] + [HttpGet, Route("employeesales/{a_start}/{a_end}")] + public Dictionary> GetEmployeeSales(DateTime a_start, DateTime a_end) + { + string token = Request.Headers[HeaderNames.Authorization]!; - //Creates a new db context - BiskAcdbContext newContext = (BiskAcdbContext)m_connection.PrepareDBContext(connectionString); + m_analyticService.SetContraints(token); + return m_analyticService.GetEmployeeSales(a_start, a_end); + } + /// + /// Endpoint to return analysis on product items low on stock + /// + /// + /// + [Authorize] + [HttpGet, Route("lowonstock")] + public IEnumerable GetLowOnStockItems() + { + string token = Request.Headers[HeaderNames.Authorization]!; - AnalyticalService analysis = new(newContext, comparisonMode!.Value, currentBranch!); - return analysis.GetInDebtCustomers(); + m_analyticService.SetContraints(token); + return m_analyticService.GetOutOfStockItems(); } + /// + /// Endpoint to return analysis on the most purchased product item + /// + /// + /// + [Authorize] + [HttpGet, Route("mostpurchaseditem/{a_start}/{a_end}")] + public IEnumerable GetMostPurchased(DateTime a_start, DateTime a_end) + { + string token = Request.Headers[HeaderNames.Authorization]!; + m_analyticService.SetContraints(token); + return m_analyticService.GetMostPurchasedItem(a_start, a_end); + } } } diff --git a/Server/Services/AnalyticalService.cs b/Server/Services/AnalyticalService.cs index 6c2fc08..84918d8 100644 --- a/Server/Services/AnalyticalService.cs +++ b/Server/Services/AnalyticalService.cs @@ -1,8 +1,11 @@ -using Biskilog_Accounting.Server.POSModels; +using Azure.Core; +using Biskilog_Accounting.Server.POSModels; +using Biskilog_Accounting.ServiceRepo; using Biskilog_Accounting.Shared.CustomModels; using Biskilog_Accounting.Shared.Interfaces; using Biskilog_Accounting.Shared.POSModels; using Microsoft.EntityFrameworkCore; +using Microsoft.Net.Http.Headers; using System.Data.Entity; using System.Runtime.CompilerServices; using static Microsoft.EntityFrameworkCore.DbLoggerCategory; @@ -15,13 +18,13 @@ namespace Biskilog_Accounting.Server.Services public class AnalyticalService : IAnalytics { private readonly BiskAcdbContext m_context; + private readonly ITokenService m_tokenService; private bool m_comparisonMode; private string m_activeBranch; - public AnalyticalService(BiskAcdbContext a_context, bool a_comparisonMode, string a_activeBranchId) + public AnalyticalService(BiskAcdbContext a_context, ITokenService a_tokenService) { m_context = a_context; - m_comparisonMode = a_comparisonMode; - m_activeBranch = a_activeBranchId; + m_tokenService = a_tokenService; } public IEnumerable GetCancelledSales(DateTime a_start, DateTime a_end) { @@ -61,56 +64,143 @@ namespace Biskilog_Accounting.Server.Services } } - public IEnumerable>> GetEmployeeSales(DateTime a_start, DateTime a_end) + public Dictionary> GetEmployeeSales(DateTime a_start, DateTime a_end) { - throw new NotImplementedException(); + Dictionary> sales = new Dictionary>(); + if (m_comparisonMode) + { + var employeeSales = m_context.Tblcarts.Where(c => c.Date >= a_start && c.Date <= a_end).Select(e => e.Cashier).Distinct(); + + foreach (string employeeName in employeeSales) + { + var list = (from a in employeeSales + join c in m_context.Tblcarts on a equals c.Cashier into Sales + from s in Sales + group s by s.Transno into saleItem + select new SaleItem + { + Total = saleItem.Sum(c => c.Total), + Transno = saleItem.Key, + Cashier = employeeName, + Date = saleItem.First().Date, + Status = saleItem.First().Status, + BranchId = saleItem.First().BranchId + }).ToList(); + sales.Add(employeeName, list); + + } + } + else + { + var employeeSales = m_context.Tblcarts.Where(c => c.Date >= a_start && c.Date <= a_end && c.BranchId == m_activeBranch).Select(e => e.Cashier).Distinct().ToList(); + + foreach (var employeeName in employeeSales) + { + var list = (from a in employeeSales + join c in m_context.Tblcarts on a equals c.Cashier into Sales + from s in Sales + group s by s.Transno into saleItem + select new SaleItem + { + Total = saleItem.Sum(c => c.Total), + Transno = saleItem.Key, + Cashier = employeeName, + Date = saleItem.First().Date, + Status = saleItem.First().Status, + BranchId = saleItem.First().BranchId + }).ToList(); + sales.Add(employeeName, list); + } + } + return sales; } public IEnumerable GetInDebtCustomers() { if (m_comparisonMode) { - return from c in m_context.Tblcustomers - join a in m_context.Customeraccounts on c.CustomerId equals a.CustomerId into CustomerAccounts - from ac in CustomerAccounts.OrderByDescending(ac => ac.Date).Take(1).DefaultIfEmpty() - where ac.Balance < 0 - orderby ac.Date descending - select new InDebtCustomers - { - Customer = c, - Debt = ac != null ? ac.Balance : 0 - }; - + var listDebts = m_context.Customeraccounts.Where(t => t.Balance < 0).OrderByDescending(d => d.Date).Select(t => t.CustomerId).Distinct().ToList(); + foreach (var customerId in listDebts) + { + yield return new InDebtCustomers + { + Customer = m_context.Tblcustomers.FirstOrDefault(i => i.CustomerId == customerId), + Debt = m_context.Customeraccounts.OrderByDescending(d => d.Date).FirstOrDefault(t => t.Balance < 0 && t.CustomerId == customerId).Balance, + }; + } } else { - return null; + var listDebts = m_context.Customeraccounts.Where(t => t.Balance < 0 && t.BranchId == m_activeBranch).OrderByDescending(d => d.Date).Select(t => t.CustomerId).Distinct().ToList(); + foreach (var customerId in listDebts) + { + yield return new InDebtCustomers + { + Customer = m_context.Tblcustomers.FirstOrDefault(i => i.CustomerId == customerId), + Debt = m_context.Customeraccounts.OrderByDescending(d => d.Date).FirstOrDefault(t => t.Balance < 0 && t.CustomerId == customerId).Balance, + }; + } } } - public IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end) + public IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end) { - throw new NotImplementedException(); + var items = (from s in m_context.Tblcarts + join p in m_context.Tblproducts on s.Id equals p.Pcode + where s.Date >= a_start && s.Date <= a_end + group s by p into g + orderby g.Count() descending + select new MostPurchasedItem + { + ProductId = g.Key.Pcode, + ProductName = g.Key.ProductName, + NbrTimesSold = g.Count(), + Revenue = g.Sum(s => s.Price) + }).Take(50).ToList(); + return items; } public IEnumerable GetOutOfStockItems() { - //if (m_comparisonMode) - //{ - // return from r in m_context.Restocklevels - // join i in m_context.Tblinventories on r.ProductId equals i.Pcode - // join p in m_context.Tblproducts on i.Pcode equals p.Pcode - // where i.Quantity == r.WarnLevel - // select new ProductItem - // { - // Product = p, - // Stock = i, - // Unitofmeasure = - // } - //} - //else { - //} - return null; + if (m_comparisonMode) + { + return (from item in m_context.Tblinventories + join p in m_context.Tblproducts on item.Pcode equals p.Pcode + join pu in m_context.Productaltunits on item.Pcode equals pu.Pcode into AltUnit + from au in AltUnit.DefaultIfEmpty() + join rs in m_context.Restocklevels on item.Pcode equals rs.ProductId + join un in m_context.Unitofmeasures on p.BaseUnit equals un.UnitCode + where p.Status!.ToLower() != "inactive" && + ((rs.WarnLevel >= item.Quantity && rs.Unit == p.BaseUnit) || (rs.WarnLevel >= (item.Quantity / au.QuantityUnit) && + rs.Unit == au.UnitCode) + ) + select new ProductItem + { + Product = p, + Stock = item, + BaseUnit = un.Unitshort! + }); + } + else + { + return (from item in m_context.Tblinventories + join p in m_context.Tblproducts on item.Pcode equals p.Pcode + join pu in m_context.Productaltunits on item.Pcode equals pu.Pcode into AltUnit + from au in AltUnit.DefaultIfEmpty() + join rs in m_context.Restocklevels on item.Pcode equals rs.ProductId + join un in m_context.Unitofmeasures on p.BaseUnit equals un.UnitCode + where p.Status!.ToLower() != "inactive" && item.BranchId == m_activeBranch && + ((rs.WarnLevel >= item.Quantity && rs.Unit == p.BaseUnit) || (rs.WarnLevel >= (item.Quantity / au.QuantityUnit) && + rs.Unit == au.UnitCode) + ) + select new ProductItem + { + Product = p, + Stock = item, + BaseUnit = un.Unitshort! + }); + } + //return null; } public IEnumerable GetPriceChanges(DateTime a_start, DateTime a_end) @@ -119,7 +209,7 @@ namespace Biskilog_Accounting.Server.Services { return from change in m_context.Tblpricechanges join p in m_context.Tblproducts on change.Pcode equals p.Pcode - where change.ChangeDate <= a_start && change.ChangeDate >= a_end + where change.ChangeDate >= a_start && change.ChangeDate <= a_end select new ProductPriceChange { BranchId = change.BranchId, @@ -135,7 +225,7 @@ namespace Biskilog_Accounting.Server.Services { return from change in m_context.Tblpricechanges join p in m_context.Tblproducts on change.Pcode equals p.Pcode - where change.ChangeDate <= a_start && change.ChangeDate >= a_end && change.BranchId == m_activeBranch + where change.ChangeDate >= a_start && change.ChangeDate <= a_end && change.BranchId == m_activeBranch select new ProductPriceChange { BranchId = change.BranchId, @@ -161,5 +251,11 @@ namespace Biskilog_Accounting.Server.Services return m_context.Tblcarts.Where(t => t.Date >= a_start && t.Date <= a_end && t.BranchId == m_activeBranch); } } + + public void SetContraints(string a_token) + { + m_comparisonMode = m_tokenService.GetComparison(a_token)!.Value; + m_activeBranch = m_tokenService.GetBaseBranch(a_token)!; + } } } diff --git a/Shared/CustomModels/MostPurchaseItem.cs b/Shared/CustomModels/MostPurchaseItem.cs new file mode 100644 index 0000000..bbe8b16 --- /dev/null +++ b/Shared/CustomModels/MostPurchaseItem.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Biskilog_Accounting.Shared.CustomModels +{ + public class MostPurchasedItem + { + /// + /// Specifies the id of the product + /// + public string ProductId { get; set; } + /// + /// Specifies the name of the product + /// + public string ProductName { get; set; } + /// + /// The total revenue generated from the sale + /// + public decimal? Revenue { get; set; } + /// + /// This is the number of times the item has been sold + /// + public int? NbrTimesSold { get; set; } + + } +} diff --git a/Shared/CustomModels/ProductItem.cs b/Shared/CustomModels/ProductItem.cs index 6db6f2b..26dc93a 100644 --- a/Shared/CustomModels/ProductItem.cs +++ b/Shared/CustomModels/ProductItem.cs @@ -11,6 +11,6 @@ namespace Biskilog_Accounting.Shared.CustomModels { public Tblproduct? Product { get; set; } public Tblinventory? Stock { get; set; } - public List Unitofmeasure { get; set; } = new List(); + public string BaseUnit { get;set; } = string.Empty; } } diff --git a/Shared/CustomModels/SaleItem.cs b/Shared/CustomModels/SaleItem.cs new file mode 100644 index 0000000..82ad876 --- /dev/null +++ b/Shared/CustomModels/SaleItem.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Biskilog_Accounting.Shared.CustomModels +{ + public class SaleItem + { + public string? Transno { get; set; } + + public DateTime? Date { get; set; } + + public string? Cashier { get; set; } + public string? Status { get; set; } + public decimal? Total { get; set; } + public string BranchId { get; set; } = null!; + + public string Customer { get; set; } = "Walk-In Purchase"!; + } +} diff --git a/Shared/Interfaces/IAnalytics.cs b/Shared/Interfaces/IAnalytics.cs index b22b92f..c1f0102 100644 --- a/Shared/Interfaces/IAnalytics.cs +++ b/Shared/Interfaces/IAnalytics.cs @@ -27,7 +27,7 @@ namespace Biskilog_Accounting.Shared.Interfaces /// /// /// - IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end); + IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end); /// /// Fetches a collection of cancelled transaction within a specified date range /// @@ -41,7 +41,7 @@ namespace Biskilog_Accounting.Shared.Interfaces /// /// /// A dictionary of transactions made by employees with employee name as key - IEnumerable>> GetEmployeeSales(DateTime a_start, DateTime a_end); + Dictionary> GetEmployeeSales(DateTime a_start, DateTime a_end); /// /// Fetches a collection of product price changes with a specified date range /// @@ -49,5 +49,6 @@ namespace Biskilog_Accounting.Shared.Interfaces /// /// IEnumerable GetPriceChanges(DateTime a_start, DateTime a_end); + void SetContraints(string a_token); } } From 6ed45ef6d6d19db3d25bdb78972abbe5b041ba62 Mon Sep 17 00:00:00 2001 From: barhen Date: Mon, 29 May 2023 16:29:48 -0500 Subject: [PATCH 4/4] Comparison mode on analysis refractored with slight performance issues. See TODO in AnalyticalService.cs line 93 --- Server/Controllers/AnalyticsController.cs | 27 +- Server/Services/AnalyticalService.cs | 285 +++++++--------------- Server/Services/AuthenticationService.cs | 11 +- Server/appsettings.json | 2 +- Shared/Interfaces/IAnalytics.cs | 4 +- Shared/Interfaces/IAuthService.cs | 2 +- Shared/Interfaces/ITokenService.cs | 5 +- Shared/ServiceRepo/TokenService.cs | 50 +++- 8 files changed, 159 insertions(+), 227 deletions(-) diff --git a/Server/Controllers/AnalyticsController.cs b/Server/Controllers/AnalyticsController.cs index b09e292..33e35bb 100644 --- a/Server/Controllers/AnalyticsController.cs +++ b/Server/Controllers/AnalyticsController.cs @@ -12,11 +12,9 @@ namespace Biskilog_Accounting.Server.Controllers [ApiController] public class AnalyticsController : ControllerBase { - private readonly ITokenService m_tokenService; private readonly IAnalytics m_analyticService; - public AnalyticsController(ITokenService tokenService, IAnalytics a_analytics) + public AnalyticsController(IAnalytics a_analytics) { - m_tokenService = tokenService; m_analyticService = a_analytics; } @@ -29,9 +27,6 @@ namespace Biskilog_Accounting.Server.Controllers [HttpGet, Route("cancelledsales/{a_start}/{a_end}")] public IEnumerable GetCancelledSalesAsync(DateTime a_start, DateTime a_end) { - string token = Request.Headers[HeaderNames.Authorization]!; - m_analyticService.SetContraints(token); - return m_analyticService.GetCancelledSales(a_start, a_end); } /// @@ -43,9 +38,6 @@ namespace Biskilog_Accounting.Server.Controllers [HttpGet, Route("sales/{a_start}/{a_end}")] public IEnumerable GetSalesAsync(DateTime a_start, DateTime a_end) { - string token = Request.Headers[HeaderNames.Authorization]!; - - m_analyticService.SetContraints(token); return m_analyticService.GetSalesTransaction(a_start, a_end); } /// @@ -57,9 +49,6 @@ namespace Biskilog_Accounting.Server.Controllers [HttpGet, Route("debtors")] public IEnumerable GetInDebtCustomers() { - string token = Request.Headers[HeaderNames.Authorization]!; - - m_analyticService.SetContraints(token); return m_analyticService.GetInDebtCustomers(); } /// @@ -71,9 +60,6 @@ namespace Biskilog_Accounting.Server.Controllers [HttpGet, Route("pricechanges/{a_start}/{a_end}")] public IEnumerable GetPriceChanges(DateTime a_start, DateTime a_end) { - string token = Request.Headers[HeaderNames.Authorization]!; - - m_analyticService.SetContraints(token); return m_analyticService.GetPriceChanges(a_start, a_end); } /// @@ -85,9 +71,6 @@ namespace Biskilog_Accounting.Server.Controllers [HttpGet, Route("employeesales/{a_start}/{a_end}")] public Dictionary> GetEmployeeSales(DateTime a_start, DateTime a_end) { - string token = Request.Headers[HeaderNames.Authorization]!; - - m_analyticService.SetContraints(token); return m_analyticService.GetEmployeeSales(a_start, a_end); } /// @@ -100,8 +83,7 @@ namespace Biskilog_Accounting.Server.Controllers public IEnumerable GetLowOnStockItems() { string token = Request.Headers[HeaderNames.Authorization]!; - - m_analyticService.SetContraints(token); + return m_analyticService.GetOutOfStockItems(); } /// @@ -113,10 +95,7 @@ namespace Biskilog_Accounting.Server.Controllers [HttpGet, Route("mostpurchaseditem/{a_start}/{a_end}")] public IEnumerable GetMostPurchased(DateTime a_start, DateTime a_end) { - string token = Request.Headers[HeaderNames.Authorization]!; - - m_analyticService.SetContraints(token); - return m_analyticService.GetMostPurchasedItem(a_start, a_end); + return m_analyticService.GetMostPurchasedItem(a_start, a_end); } } } diff --git a/Server/Services/AnalyticalService.cs b/Server/Services/AnalyticalService.cs index 84918d8..2795e05 100644 --- a/Server/Services/AnalyticalService.cs +++ b/Server/Services/AnalyticalService.cs @@ -1,14 +1,9 @@ -using Azure.Core; -using Biskilog_Accounting.Server.POSModels; -using Biskilog_Accounting.ServiceRepo; +using Biskilog_Accounting.Server.POSModels; using Biskilog_Accounting.Shared.CustomModels; using Biskilog_Accounting.Shared.Interfaces; using Biskilog_Accounting.Shared.POSModels; -using Microsoft.EntityFrameworkCore; using Microsoft.Net.Http.Headers; using System.Data.Entity; -using System.Runtime.CompilerServices; -using static Microsoft.EntityFrameworkCore.DbLoggerCategory; namespace Biskilog_Accounting.Server.Services { @@ -19,135 +14,89 @@ namespace Biskilog_Accounting.Server.Services { private readonly BiskAcdbContext m_context; private readonly ITokenService m_tokenService; - private bool m_comparisonMode; - private string m_activeBranch; - public AnalyticalService(BiskAcdbContext a_context, ITokenService a_tokenService) + private readonly HttpContext m_httpContext; + + public AnalyticalService(BiskAcdbContext a_context, ITokenService a_tokenService, IHttpContextAccessor a_httpContextAccessor) { m_context = a_context; m_tokenService = a_tokenService; + m_httpContext = a_httpContextAccessor?.HttpContext; } public IEnumerable GetCancelledSales(DateTime a_start, DateTime a_end) { - //If in comparison mode, the list is fetched from all branchid of the business - if (m_comparisonMode) - { - return from cSale in m_context.Tblcancelledtransactions - join aSale in m_context.Tblcarts on cSale.Transno equals aSale.Transno into activeSale - join cPurchase in m_context.Tblcustomerpurchases on cSale.Transno equals cPurchase.TransactionId into customerSales - from c in customerSales.DefaultIfEmpty() - join customer in m_context.Tblcustomers on c.CustomerId equals customer.CustomerId into Customers - from cc in Customers.DefaultIfEmpty() - where cSale.DateCancelled >= a_start && cSale.DateCancelled <= a_end - select new CancelledSales - { - CancelledTransaction = cSale, - Value = activeSale.Sum(s => s.Total), - Customer = !String.IsNullOrEmpty(cc.CustomerId) ? $"{cc.Firstname} {cc.Surname}" : "Walk-IN Purchase" - }; - } - else - { - return from cSale in m_context.Tblcancelledtransactions - join aSale in m_context.Tblcarts on cSale.Transno equals aSale.Transno into activeSale - join cPurchase in m_context.Tblcustomerpurchases on cSale.Transno equals cPurchase.TransactionId into customerSales - from c in customerSales.DefaultIfEmpty() - join customer in m_context.Tblcustomers on c.CustomerId equals customer.CustomerId into Customers - from cc in Customers.DefaultIfEmpty() - where cSale.DateCancelled >= a_start && cSale.DateCancelled <= a_end && cSale.BranchId == m_activeBranch - select new CancelledSales - { - CancelledTransaction = cSale, - Value = (from a in activeSale where a.BranchId == m_activeBranch select a.Total).Sum(), - Customer = !String.IsNullOrEmpty(cc.CustomerId) ? $"{cc.Firstname} {cc.Surname}" : "Walk-IN Purchase" - }; + string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!; + IEnumerable accessiblebranches = m_tokenService.BranchIds(token); + + return from cSale in m_context.Tblcancelledtransactions + join aSale in m_context.Tblcarts on cSale.Transno equals aSale.Transno into activeSale + join cPurchase in m_context.Tblcustomerpurchases on cSale.Transno equals cPurchase.TransactionId into customerSales + from c in customerSales.DefaultIfEmpty() + join customer in m_context.Tblcustomers on c.CustomerId equals customer.CustomerId into Customers + from cc in Customers.DefaultIfEmpty() + where cSale.DateCancelled >= a_start && cSale.DateCancelled <= a_end && accessiblebranches.Contains(cSale.BranchId) + select new CancelledSales + { + CancelledTransaction = cSale, + Value = (from a in activeSale where accessiblebranches.Contains(a.BranchId) select a.Total).Sum(), + Customer = !String.IsNullOrEmpty(cc.CustomerId) ? $"{cc.Firstname} {cc.Surname}" : "Walk-IN Purchase" + }; - } } public Dictionary> GetEmployeeSales(DateTime a_start, DateTime a_end) { - Dictionary> sales = new Dictionary>(); - if (m_comparisonMode) - { - var employeeSales = m_context.Tblcarts.Where(c => c.Date >= a_start && c.Date <= a_end).Select(e => e.Cashier).Distinct(); + string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!; + IEnumerable accessiblebranches = m_tokenService.BranchIds(token); - foreach (string employeeName in employeeSales) - { - var list = (from a in employeeSales - join c in m_context.Tblcarts on a equals c.Cashier into Sales - from s in Sales - group s by s.Transno into saleItem - select new SaleItem - { - Total = saleItem.Sum(c => c.Total), - Transno = saleItem.Key, - Cashier = employeeName, - Date = saleItem.First().Date, - Status = saleItem.First().Status, - BranchId = saleItem.First().BranchId - }).ToList(); - sales.Add(employeeName, list); + Dictionary> sales = new Dictionary>(); + var employeeSales = m_context.Tblcarts.Where(c => c.Date >= a_start && c.Date <= a_end + && accessiblebranches.Contains(c.BranchId)).Select(e => e.Cashier).Distinct().ToList(); - } - } - else + foreach (var employeeName in employeeSales) { - var employeeSales = m_context.Tblcarts.Where(c => c.Date >= a_start && c.Date <= a_end && c.BranchId == m_activeBranch).Select(e => e.Cashier).Distinct().ToList(); - - foreach (var employeeName in employeeSales) - { - var list = (from a in employeeSales - join c in m_context.Tblcarts on a equals c.Cashier into Sales - from s in Sales - group s by s.Transno into saleItem - select new SaleItem - { - Total = saleItem.Sum(c => c.Total), - Transno = saleItem.Key, - Cashier = employeeName, - Date = saleItem.First().Date, - Status = saleItem.First().Status, - BranchId = saleItem.First().BranchId - }).ToList(); - sales.Add(employeeName, list); - } + var list = (from a in employeeSales + join c in m_context.Tblcarts on a equals c.Cashier into Sales + from s in Sales + group s by s.Transno into saleItem + select new SaleItem + { + Total = saleItem.Sum(c => c.Total), + Transno = saleItem.Key, + Cashier = employeeName, + Date = saleItem.First().Date, + Status = saleItem.First().Status, + BranchId = saleItem.First().BranchId + }).ToList(); + sales.Add(employeeName, list); } return sales; } public IEnumerable GetInDebtCustomers() { - if (m_comparisonMode) - { - var listDebts = m_context.Customeraccounts.Where(t => t.Balance < 0).OrderByDescending(d => d.Date).Select(t => t.CustomerId).Distinct().ToList(); - foreach (var customerId in listDebts) - { - yield return new InDebtCustomers - { - Customer = m_context.Tblcustomers.FirstOrDefault(i => i.CustomerId == customerId), - Debt = m_context.Customeraccounts.OrderByDescending(d => d.Date).FirstOrDefault(t => t.Balance < 0 && t.CustomerId == customerId).Balance, - }; - } - } - else + string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!; + IEnumerable accessiblebranches = m_tokenService.BranchIds(token); + + var listDebts = m_context.Customeraccounts.Where(t => t.Balance < 0 && accessiblebranches.Contains(t.BranchId)).OrderByDescending(d => d.Date).Select(t => t.CustomerId).Distinct().ToList(); + foreach (var customerId in listDebts) { - var listDebts = m_context.Customeraccounts.Where(t => t.Balance < 0 && t.BranchId == m_activeBranch).OrderByDescending(d => d.Date).Select(t => t.CustomerId).Distinct().ToList(); - foreach (var customerId in listDebts) + yield return new InDebtCustomers { - yield return new InDebtCustomers - { - Customer = m_context.Tblcustomers.FirstOrDefault(i => i.CustomerId == customerId), - Debt = m_context.Customeraccounts.OrderByDescending(d => d.Date).FirstOrDefault(t => t.Balance < 0 && t.CustomerId == customerId).Balance, - }; - } + Customer = m_context.Tblcustomers.FirstOrDefault(i => i.CustomerId == customerId), + Debt = m_context.Customeraccounts.OrderByDescending(d => d.Date).FirstOrDefault(t => t.Balance < 0 && t.CustomerId == customerId).Balance, + }; } } public IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end) { + //TODO either rewrite query or increase memory on server to deal with comparison mode performance issue + string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!; + IEnumerable accessiblebranches = m_tokenService.BranchIds(token); + var items = (from s in m_context.Tblcarts join p in m_context.Tblproducts on s.Id equals p.Pcode - where s.Date >= a_start && s.Date <= a_end + where s.Date >= a_start && s.Date <= a_end && s.BranchId == accessiblebranches.First() group s by p into g orderby g.Count() descending select new MostPurchasedItem @@ -162,100 +111,54 @@ namespace Biskilog_Accounting.Server.Services public IEnumerable GetOutOfStockItems() { - if (m_comparisonMode) - { - return (from item in m_context.Tblinventories - join p in m_context.Tblproducts on item.Pcode equals p.Pcode - join pu in m_context.Productaltunits on item.Pcode equals pu.Pcode into AltUnit - from au in AltUnit.DefaultIfEmpty() - join rs in m_context.Restocklevels on item.Pcode equals rs.ProductId - join un in m_context.Unitofmeasures on p.BaseUnit equals un.UnitCode - where p.Status!.ToLower() != "inactive" && - ((rs.WarnLevel >= item.Quantity && rs.Unit == p.BaseUnit) || (rs.WarnLevel >= (item.Quantity / au.QuantityUnit) && - rs.Unit == au.UnitCode) - ) - select new ProductItem - { - Product = p, - Stock = item, - BaseUnit = un.Unitshort! - }); - } - else - { - return (from item in m_context.Tblinventories - join p in m_context.Tblproducts on item.Pcode equals p.Pcode - join pu in m_context.Productaltunits on item.Pcode equals pu.Pcode into AltUnit - from au in AltUnit.DefaultIfEmpty() - join rs in m_context.Restocklevels on item.Pcode equals rs.ProductId - join un in m_context.Unitofmeasures on p.BaseUnit equals un.UnitCode - where p.Status!.ToLower() != "inactive" && item.BranchId == m_activeBranch && - ((rs.WarnLevel >= item.Quantity && rs.Unit == p.BaseUnit) || (rs.WarnLevel >= (item.Quantity / au.QuantityUnit) && - rs.Unit == au.UnitCode) - ) - select new ProductItem - { - Product = p, - Stock = item, - BaseUnit = un.Unitshort! - }); - } - //return null; + string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!; + IEnumerable accessiblebranches = m_tokenService.BranchIds(token); + + return (from item in m_context.Tblinventories + join p in m_context.Tblproducts on item.Pcode equals p.Pcode + join pu in m_context.Productaltunits on item.Pcode equals pu.Pcode into AltUnit + from au in AltUnit.DefaultIfEmpty() + join rs in m_context.Restocklevels on item.Pcode equals rs.ProductId + join un in m_context.Unitofmeasures on p.BaseUnit equals un.UnitCode + where p.Status!.ToLower() != "inactive" && accessiblebranches.Contains(item.BranchId) && + ((rs.WarnLevel >= item.Quantity && rs.Unit == p.BaseUnit) || (rs.WarnLevel >= (item.Quantity / au.QuantityUnit) && + rs.Unit == au.UnitCode) + ) + select new ProductItem + { + Product = p, + Stock = item, + BaseUnit = un.Unitshort! + }); } public IEnumerable GetPriceChanges(DateTime a_start, DateTime a_end) { - if (m_comparisonMode) - { - return from change in m_context.Tblpricechanges - join p in m_context.Tblproducts on change.Pcode equals p.Pcode - where change.ChangeDate >= a_start && change.ChangeDate <= a_end - select new ProductPriceChange - { - BranchId = change.BranchId, - ChangeDate = change.ChangeDate, - Pcode = change.Pcode, - CountId = change.CountId, - CurrentPrice = change.CurrentPrice, - PreviousPrice = change.PreviousPrice, - ProductName = p.ProductName - }; - } - else - { - return from change in m_context.Tblpricechanges - join p in m_context.Tblproducts on change.Pcode equals p.Pcode - where change.ChangeDate >= a_start && change.ChangeDate <= a_end && change.BranchId == m_activeBranch - select new ProductPriceChange - { - BranchId = change.BranchId, - ChangeDate = change.ChangeDate, - Pcode = change.Pcode, - CountId = change.CountId, - CurrentPrice = change.CurrentPrice, - PreviousPrice = change.PreviousPrice, - ProductName = p.ProductName - }; - } + string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!; + IEnumerable accessiblebranches = m_tokenService.BranchIds(token); + + return from change in m_context.Tblpricechanges + join p in m_context.Tblproducts on change.Pcode equals p.Pcode + where change.ChangeDate >= a_start && change.ChangeDate <= a_end && accessiblebranches.Contains(change.BranchId) + select new ProductPriceChange + { + BranchId = change.BranchId, + ChangeDate = change.ChangeDate, + Pcode = change.Pcode, + CountId = change.CountId, + CurrentPrice = change.CurrentPrice, + PreviousPrice = change.PreviousPrice, + ProductName = p.ProductName + }; } public IEnumerable GetSalesTransaction(DateTime a_start, DateTime a_end) { - //If in comparison mode, the list is fetched from all branchid of the business - if (m_comparisonMode) - { - return m_context.Tblcarts.Where(t => t.Date >= a_start && t.Date <= a_end); - } - else - { - return m_context.Tblcarts.Where(t => t.Date >= a_start && t.Date <= a_end && t.BranchId == m_activeBranch); - } - } + string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!; + IEnumerable accessiblebranches = m_tokenService.BranchIds(token); + + return m_context.Tblcarts.Where(t => t.Date >= a_start && t.Date <= a_end && accessiblebranches.Contains(t.BranchId)); - public void SetContraints(string a_token) - { - m_comparisonMode = m_tokenService.GetComparison(a_token)!.Value; - m_activeBranch = m_tokenService.GetBaseBranch(a_token)!; } } } diff --git a/Server/Services/AuthenticationService.cs b/Server/Services/AuthenticationService.cs index 6ca95f6..c1ae403 100644 --- a/Server/Services/AuthenticationService.cs +++ b/Server/Services/AuthenticationService.cs @@ -55,12 +55,12 @@ namespace Biskilog_Accounting.Server.Services List businessIds = GetSiteaccesspermission(user.ClientId, user.UserId).Select(t => t.BusinessId).ToList(); Contract? contract = GetContract(user.ClientId, businessIds); - List businesses = GetClientbusiness(user.ClientId); + List businesses = GetClientbusiness(user.ClientId, user.UserId).Select(t=>t.BusinessExternalId).ToList(); if (contract == null) return AuthEnums.Invalid.ToString(); - return m_tokenService.GenerateToken(user, contract, databasemap, businesses[0], false); + return m_tokenService.GenerateToken(user, contract, databasemap, businesses, false); } /// @@ -95,9 +95,12 @@ namespace Biskilog_Accounting.Server.Services throw new NotImplementedException(); } - public List GetClientbusiness(int a_clientId) + public List GetClientbusiness(int a_clientId, int userId) { - return m_context.Clientbusinesses.Where(i => i.ClientId == a_clientId).ToList(); + return (from b in m_context.Clientbusinesses + join p in m_context.Siteaccesspermissions on new {b.ClientId, b.BusinessId} equals new {p.ClientId, p.BusinessId} + where p.UserId == userId && p.ClientId == a_clientId + select b).ToList(); } public Databasemap GetClientDB(int a_clientId) diff --git a/Server/appsettings.json b/Server/appsettings.json index 140c6ab..984fb06 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -7,7 +7,7 @@ }, "ConnectionStrings": { "Connection": "server=54.37.19.162;database=dev_biskilogclients;user=biskilog;password=mefbuk-6niFsu-fytrew", - "PrivateConnection": "server={0};database={1};user=biskilog;password=mefbuk-6niFsu-fytrew" + "PrivateConnection": "server={0};database={1};user=biskilog;password=mefbuk-6niFsu-fytrew;default command timeout=0;" }, "AllowedHosts": "*", "JWT": { diff --git a/Shared/Interfaces/IAnalytics.cs b/Shared/Interfaces/IAnalytics.cs index c1f0102..fb00c93 100644 --- a/Shared/Interfaces/IAnalytics.cs +++ b/Shared/Interfaces/IAnalytics.cs @@ -27,7 +27,7 @@ namespace Biskilog_Accounting.Shared.Interfaces /// /// /// - IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end); + IEnumerable GetMostPurchasedItem(DateTime a_start, DateTime a_end); /// /// Fetches a collection of cancelled transaction within a specified date range /// @@ -49,6 +49,6 @@ namespace Biskilog_Accounting.Shared.Interfaces /// /// IEnumerable GetPriceChanges(DateTime a_start, DateTime a_end); - void SetContraints(string a_token); + //void SetContraints(string a_token); } } diff --git a/Shared/Interfaces/IAuthService.cs b/Shared/Interfaces/IAuthService.cs index 70a8960..f0f93b9 100644 --- a/Shared/Interfaces/IAuthService.cs +++ b/Shared/Interfaces/IAuthService.cs @@ -14,6 +14,6 @@ namespace Biskilog_Accounting.Shared.Interfaces Contract? GetContract(int a_clientId, List a_businessId); Databasemap GetClientDB(int a_clientId); List GetSiteaccesspermission(int a_clientId, int a_userId); - List GetClientbusiness(int a_clientId); + List GetClientbusiness(int a_clientId, int userId); } } diff --git a/Shared/Interfaces/ITokenService.cs b/Shared/Interfaces/ITokenService.cs index 27cdf82..f4dc1f7 100644 --- a/Shared/Interfaces/ITokenService.cs +++ b/Shared/Interfaces/ITokenService.cs @@ -6,12 +6,13 @@ namespace Biskilog_Accounting.Shared.Interfaces public interface ITokenService { AuthEnums ValidateToken(string a_token); - string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database, Clientbusiness a_business, bool a_comparison); + string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database, List a_business, bool a_comparison); int? GetDatabaseIdFromToken(string a_token); int? GetUserIdFromToken(string a_token); string? GetUserNameFromToken(string a_token); string? GetBaseBranch(string a_token); bool? GetComparison(string a_token); - + IEnumerable BranchIds(string a_token); + string? GetAllBranch(string a_token); } } diff --git a/Shared/ServiceRepo/TokenService.cs b/Shared/ServiceRepo/TokenService.cs index 022137e..d86df04 100644 --- a/Shared/ServiceRepo/TokenService.cs +++ b/Shared/ServiceRepo/TokenService.cs @@ -43,7 +43,7 @@ namespace Biskilog_Accounting.ServiceRepo /// Generates an access token based on the user /// /// A tokenized string - public string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database, Clientbusiness a_business, bool a_comparison) + public string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database, List a_business, bool a_comparison) { try { @@ -57,7 +57,8 @@ namespace Biskilog_Accounting.ServiceRepo new Claim("Username", a_user.Username.ToString()), new Claim("DbId",a_database.DbNo.ToString()), new Claim("ComparisonMode",a_comparison.ToString()), - new Claim("BranchId",a_business.BusinessExternalId.ToString()), + new Claim("BranchId",a_business[0].ToString()), + new Claim("BranchAccess",string.Join(", ", a_business.ToArray())), new Claim("ClientId", a_user.ClientId.ToString()), }; @@ -150,5 +151,50 @@ namespace Biskilog_Accounting.ServiceRepo } return null; } + /// + ///Deserializes the token string if valid to return the specified list of branches a user has access to in the token string + /// + /// + /// Username + public string? GetAllBranch(string a_token) + { + if (ValidateToken(a_token) == AuthEnums.Valid) + { + string token = a_token.Substring(6).Trim(); + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtToken = (JwtSecurityToken)handler.ReadToken(token); + return jwtToken.Claims.First(claim => claim.Type == "BranchAccess").Value; + } + return null; + } + /// + /// Return a specified list of branches a user has access if comparison mode is set otherwise returns only the + /// active branch on the list + /// + /// + /// + public IEnumerable BranchIds(string a_token) + { + List branchIds = new List(); + if (ValidateToken(a_token) == AuthEnums.Valid) + { + bool comparison = GetComparison(a_token)!.Value; + if (comparison) + { + string? branches = GetAllBranch(a_token); + if (branches != null) + { + string[] branchArray = branches!.Split(); + branchIds.AddRange(branchArray); + } + } + else + { + string? baseBranch = GetBaseBranch(a_token); + branchIds.Add(baseBranch!); + } + } + return branchIds.AsEnumerable(); + } } }