From ee14a6cb16bae8124a312982f9bf97eb3f6c9c5e Mon Sep 17 00:00:00 2001 From: barhen Date: Sun, 28 May 2023 23:11:57 -0500 Subject: [PATCH] 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; + } } }