diff --git a/Sloth.Core/Domain/Transfer.cs b/Sloth.Core/Domain/Transfer.cs index f365906e..e6743ebc 100644 --- a/Sloth.Core/Domain/Transfer.cs +++ b/Sloth.Core/Domain/Transfer.cs @@ -163,6 +163,8 @@ public string FullObjectToString() } return result; } + + public string ShortFinancialSegmentString => $"{FinancialSegmentString?[0..13]}..."; //Takes the first 13 characters of the FinancialSegmentString. Works with nulls #endregion public enum CreditDebit diff --git a/Sloth.Web/Controllers/ReportsController.cs b/Sloth.Web/Controllers/ReportsController.cs index 05d380ab..ddfd0f6d 100644 --- a/Sloth.Web/Controllers/ReportsController.cs +++ b/Sloth.Web/Controllers/ReportsController.cs @@ -20,6 +20,7 @@ using Sloth.Web.Models.ReportViewModels; using Sloth.Web.Models.TransactionViewModels; using Sloth.Web.Resources; +using Sloth.Web.Helpers; namespace Sloth.Web.Controllers { @@ -65,6 +66,34 @@ public async Task FailedTransactions() return View("FailedTransactions", model); } + [Route("{team}/reports/downloadabletransactions/{filter?}")] + [HttpGet] + public async Task DownloadableTransactions(TransactionsFilterModel filter = null) + { + if (string.IsNullOrWhiteSpace(TeamSlug)) + { + return BadRequest("TeamSlug is required"); + } + + if (filter == null) + filter = new TransactionsFilterModel(); + + FilterHelpers.SanitizeTransactionsFilter(filter); + + var model = new TransfersReportViewModel + { + Filter = filter, + }; + + model.Transactions = await DbContext.Transactions.Include(a => a.Transfers).Include(a => a.Metadata) + .Where(t => t.Source.Team.Slug == TeamSlug && t.TransactionDate >= filter.From && t.TransactionDate <= filter.To).OrderBy(a => a.Id).ThenBy(a => a.TransactionDate).Select(TransactionWithTransfers.Projection()).ToListAsync(); + + var team = await DbContext.Teams.FirstAsync(t => t.Slug == TeamSlug); + ViewBag.Title = $"Transactions with Transfers - {team.Name}"; + + return View(model); + } + [Authorize(Roles = Roles.SystemAdmin)] public async Task FailedTransactionsAllTeams() { diff --git a/Sloth.Web/Controllers/TransactionsController.cs b/Sloth.Web/Controllers/TransactionsController.cs index 55ce62ff..992ef075 100644 --- a/Sloth.Web/Controllers/TransactionsController.cs +++ b/Sloth.Web/Controllers/TransactionsController.cs @@ -20,6 +20,7 @@ using Sloth.Web.Models.BlobViewModels; using Sloth.Web.Models.TransactionViewModels; using Sloth.Web.Resources; +using Sloth.Web.Helpers; namespace Sloth.Web.Controllers { @@ -44,7 +45,7 @@ public async Task Index(TransactionsFilterModel filter = null) if (filter == null) filter = new TransactionsFilterModel(); - SanitizeTransactionsFilter(filter); + FilterHelpers.SanitizeTransactionsFilter(filter); IQueryable query; @@ -683,35 +684,5 @@ public async Task Search(TransactionsFilterModel filter = null) return RedirectToAction("Index"); } - - - private static void SanitizeTransactionsFilter(TransactionsFilterModel model) - { - var fromUtc = (model.From ?? DateTime.Now.AddMonths(-1)).ToUniversalTime().Date; - var throughUtc = (model.To ?? DateTime.Now).ToUniversalTime().AddDays(1).Date; - - if (fromUtc > DateTime.UtcNow || fromUtc < DateTime.UtcNow.AddYears(-100)) - { - // invalid, so default to filtering from one month ago - var from = DateTime.Now.AddMonths((-1)).Date; - model.From = from; - fromUtc = from.ToUniversalTime(); - } - else - { - model.From = fromUtc.ToLocalTime(); - } - - if (fromUtc >= throughUtc) - { - // invalid, so default to filtering through one month after fromUtc - throughUtc = fromUtc.AddMonths(1).AddDays(1).Date; - model.To = throughUtc.AddDays(-1).ToLocalTime(); - } - else - { - model.To = throughUtc.ToLocalTime(); - } - } } } diff --git a/Sloth.Web/Helpers/FilterHelpers.cs b/Sloth.Web/Helpers/FilterHelpers.cs new file mode 100644 index 00000000..b4ca4f67 --- /dev/null +++ b/Sloth.Web/Helpers/FilterHelpers.cs @@ -0,0 +1,37 @@ +using Sloth.Web.Models.TransactionViewModels; +using System; + +namespace Sloth.Web.Helpers +{ + public class FilterHelpers + { + public static void SanitizeTransactionsFilter(TransactionsFilterModel model) + { + var fromUtc = (model.From ?? DateTime.Now.AddMonths(-1)).ToUniversalTime().Date; + var throughUtc = (model.To ?? DateTime.Now).ToUniversalTime().AddDays(1).Date; + + if (fromUtc > DateTime.UtcNow || fromUtc < DateTime.UtcNow.AddYears(-100)) + { + // invalid, so default to filtering from one month ago + var from = DateTime.Now.AddMonths((-1)).Date; + model.From = from; + fromUtc = from.ToUniversalTime(); + } + else + { + model.From = fromUtc.ToLocalTime(); + } + + if (fromUtc >= throughUtc) + { + // invalid, so default to filtering through one month after fromUtc + throughUtc = fromUtc.AddMonths(1).AddDays(1).Date; + model.To = throughUtc.AddDays(-1).ToLocalTime(); + } + else + { + model.To = throughUtc.ToLocalTime(); + } + } + } +} diff --git a/Sloth.Web/Models/ReportViewModels/TransfersReportViewModel.cs b/Sloth.Web/Models/ReportViewModels/TransfersReportViewModel.cs new file mode 100644 index 00000000..17c0e64e --- /dev/null +++ b/Sloth.Web/Models/ReportViewModels/TransfersReportViewModel.cs @@ -0,0 +1,76 @@ +using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations.Schema; +using Sloth.Core.Models; +using System.Collections.Generic; +using Sloth.Web.Models.TransactionViewModels; +using System.Linq.Expressions; +using System.Linq; + +namespace Sloth.Web.Models.ReportViewModels +{ + public class TransfersReportViewModel + { + public TransactionsFilterModel Filter { get; set; } + + public IList Transactions { get; set; } + } + + public class TransactionWithTransfers + { + [DisplayName("Txn Id")] + public string Id { get; set; } + [DisplayName("Txn Id")] + public string DisplayId => $"...{Id[^4..]}"; // last 4 characters of the id + + public string Status { get; set; } + + [DisplayName("Transaction Date")] + public DateTime TransactionDate { get; set; } + + [DisplayName("Kfs Tracking Number")] + public string KfsTrackingNumber { get; set; } + [DisplayName("Document Number")] + public string DocumentNumber { get; set; } + + [DisplayName("Processor Tracking Number")] + public string ProcessorTrackingNumber { get; set; } + + [DisplayName("Merchant Tracking Number")] + public string MerchantTrackingNumber { get; set; } + [DisplayName("Transaction Description")] + public string TxnDescription { get; set; } + + public string MetaDataString { get; set; } + + [DisplayName("Transaction Amount")] + public decimal Amount { get; set; } + + [DisplayName("Transfer Count")] + public int TransferCount { get; set; } + + public IList Transfers { get; set; } // don't need all the info in here, but it shouldn't be too big. + + public static Expression> Projection() + { + + return txn => new TransactionWithTransfers + { + Id = txn.Id, + Status = txn.Status, + TransactionDate = txn.TransactionDate, + KfsTrackingNumber = txn.KfsTrackingNumber, + DocumentNumber = txn.DocumentNumber, + ProcessorTrackingNumber = txn.ProcessorTrackingNumber, + MerchantTrackingNumber = txn.MerchantTrackingNumber, + TxnDescription = txn.Description, + MetaDataString = string.Join(", ", txn.Metadata.Select(kv => $"{kv.Name}: {kv.Value}")), + Amount = txn.Transfers.Where(a => a.Direction == Transfer.CreditDebit.Credit).Sum(a => a.Amount), + TransferCount = txn.Transfers.Count, + Transfers = txn.Transfers.OrderBy(a => a.Direction).ToList(), + }; + + } + } +} diff --git a/Sloth.Web/Views/Reports/DownloadableTransactions.cshtml b/Sloth.Web/Views/Reports/DownloadableTransactions.cshtml new file mode 100644 index 00000000..164e1fd4 --- /dev/null +++ b/Sloth.Web/Views/Reports/DownloadableTransactions.cshtml @@ -0,0 +1,128 @@ +@using Humanizer +@using Sloth.Core.Extensions +@using Sloth.Core.Resources +@model Sloth.Web.Models.ReportViewModels.TransfersReportViewModel + +
+

@ViewBag.Title

+ +

Filters

+
+
+
+
+ + to + +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + @foreach (var txn in Model.Transactions) + { + @foreach (var transfer in txn.Transfers) + { + + + + + + + + + + + + + + + + + + + + } + } + +
@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().Id)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().DisplayId)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().Status)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().TransactionDate)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().KfsTrackingNumber)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().DocumentNumber)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().ProcessorTrackingNumber)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().MerchantTrackingNumber)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().TxnDescription)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().MetaDataString)@Html.DisplayNameFor(x => x.Transactions.FirstOrDefault().TransferCount)Total AmountDirectionDescriptionAmountChart StringChart String
@txn.Id@txn.DisplayId@txn.Status.Humanize(LetterCasing.Title)@txn.TransactionDate.ToPacificTime()@txn.KfsTrackingNumber@txn.DocumentNumber@txn.ProcessorTrackingNumber@txn.MerchantTrackingNumber@txn.TxnDescription@txn.MetaDataString@txn.TransferCount@txn.Amount@transfer.Direction@transfer.Description@transfer.Amount@transfer.ShortFinancialSegmentString@transfer.FinancialSegmentString
+ +
+ +
+ +@section AdditionalScripts { + + + +} diff --git a/Sloth.Web/Views/Reports/Index.cshtml b/Sloth.Web/Views/Reports/Index.cshtml index 22d47335..0badf5f6 100644 --- a/Sloth.Web/Views/Reports/Index.cshtml +++ b/Sloth.Web/Views/Reports/Index.cshtml @@ -34,6 +34,18 @@ + }