From 03b07adcb23f5791996780a6babbfb791b2109ee Mon Sep 17 00:00:00 2001 From: Stephen Adams Date: Thu, 24 Aug 2017 15:54:25 -0400 Subject: [PATCH 1/3] Fixes for issue #77 (ability to exclude rates that aren't uniform across packages) In situations where mail services aren't available on package 1 but are on package 2...we'll now exclude these if the requireUniformMailServices parameter is set to true on the USPS Domestic and International providers. --- .../Features/USPSDomesticRates.cs | 21 +++++++ .../Features/USPSInternationalRates.cs | 1 + DotNetShipping/DotNetShipping.csproj | 3 + DotNetShipping/Enums.cs | 20 +++++++ DotNetShipping/Helpers/USPSHelpers.cs | 27 +++++++++ DotNetShipping/InfoMessage.cs | 27 +++++++++ DotNetShipping/Shipment.cs | 19 ++++++- .../USPSInternationalProvider.cs | 53 ++++++++++++++++- .../ShippingProviders/USPSProvider.cs | 57 +++++++++++++++++-- 9 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 DotNetShipping/Enums.cs create mode 100644 DotNetShipping/Helpers/USPSHelpers.cs create mode 100644 DotNetShipping/InfoMessage.cs diff --git a/DotNetShipping.Tests/Features/USPSDomesticRates.cs b/DotNetShipping.Tests/Features/USPSDomesticRates.cs index 89fd45c..c5555b4 100644 --- a/DotNetShipping.Tests/Features/USPSDomesticRates.cs +++ b/DotNetShipping.Tests/Features/USPSDomesticRates.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.Linq; @@ -170,5 +172,24 @@ public void Can_Get_Different_Rates_For_Signature_Required_Lookup() } } } + + [Fact] + public void USPS_Domestic_Will_Remove_Rates_That_Are_Not_Uniform_If_RequiredUniformRates_Enabled() + { + var rateManager = new RateManager(); + rateManager.AddProvider(new USPSProvider(USPSUserId, "ALL", String.Empty, true)); + + var package1 = new Package(14, 14, 6, 2, 0); + var package2 = new Package(16, 16, 48, 16, 0); + + var origin = new Address("", "", "21401", "US"); + var destination = new Address("", "", "54937", "US"); + + var response = rateManager.GetRates(origin, destination, new List() { package1, package2 } ); + + Assert.True(response.RatesExcluded); + Assert.NotNull(response.InfoMessages); + Assert.True(response.InfoMessages.Count > 0); + } } } \ No newline at end of file diff --git a/DotNetShipping.Tests/Features/USPSInternationalRates.cs b/DotNetShipping.Tests/Features/USPSInternationalRates.cs index 826ca17..7df1871 100644 --- a/DotNetShipping.Tests/Features/USPSInternationalRates.cs +++ b/DotNetShipping.Tests/Features/USPSInternationalRates.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; diff --git a/DotNetShipping/DotNetShipping.csproj b/DotNetShipping/DotNetShipping.csproj index 83eaa37..52e3ec6 100644 --- a/DotNetShipping/DotNetShipping.csproj +++ b/DotNetShipping/DotNetShipping.csproj @@ -66,8 +66,11 @@ + + + diff --git a/DotNetShipping/Enums.cs b/DotNetShipping/Enums.cs new file mode 100644 index 0000000..e4fa1d5 --- /dev/null +++ b/DotNetShipping/Enums.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetShipping +{ + public class Enums + { + public enum ShippingProvider + { + UPS, + USPS, + USPSInternational, + FedEx, + FedExSmartPost + } + } +} diff --git a/DotNetShipping/Helpers/USPSHelpers.cs b/DotNetShipping/Helpers/USPSHelpers.cs new file mode 100644 index 0000000..f39379a --- /dev/null +++ b/DotNetShipping/Helpers/USPSHelpers.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using static System.String; + +namespace DotNetShipping.Helpers +{ + public static class USPSHelpers + { + /// + /// Removes encoded characters from mail service name for human consumption + /// + /// + /// + public static String SanitizeMailServiceName(this String mailServiceName) + { + if (!IsNullOrEmpty(mailServiceName)) + return Regex.Replace(mailServiceName, "<.*>", ""); + + return mailServiceName; + } + } +} diff --git a/DotNetShipping/InfoMessage.cs b/DotNetShipping/InfoMessage.cs new file mode 100644 index 0000000..76650bc --- /dev/null +++ b/DotNetShipping/InfoMessage.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetShipping +{ + public class InfoMessage + { + /// + /// Shipping provider that generated the message + /// + public Enums.ShippingProvider ShippingProvider { get; set; } + + /// + /// Message + /// + public String Message { get; set; } + + public InfoMessage(Enums.ShippingProvider shippingProvider, String message) + { + ShippingProvider = shippingProvider; + Message = message; + } + } +} diff --git a/DotNetShipping/Shipment.cs b/DotNetShipping/Shipment.cs index bab69ae..f0a4a9a 100644 --- a/DotNetShipping/Shipment.cs +++ b/DotNetShipping/Shipment.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -13,9 +14,15 @@ public class Shipment public ICollection RateAdjusters; private readonly List _rates; private readonly List _serverErrors; + + /// + /// Will contain any informative messages + /// + private readonly List _infoMessages; + public readonly Address DestinationAddress; public readonly Address OriginAddress; - + public Shipment(Address originAddress, Address destinationAddress, List packages) { OriginAddress = originAddress; @@ -23,6 +30,7 @@ public Shipment(Address originAddress, Address destinationAddress, List Packages = packages.AsReadOnly(); _rates = new List(); _serverErrors = new List(); + _infoMessages = new List(); } public int PackageCount @@ -41,5 +49,14 @@ public List ServerErrors { get { return _serverErrors; } } + public List InfoMessages + { + get { return _infoMessages; } + } + + /// + /// If true, some rates were excluded. See InfoMessages for more information. + /// + public bool RatesExcluded { get; set; } } } diff --git a/DotNetShipping/ShippingProviders/USPSInternationalProvider.cs b/DotNetShipping/ShippingProviders/USPSInternationalProvider.cs index d2f3ce8..d98394f 100644 --- a/DotNetShipping/ShippingProviders/USPSInternationalProvider.cs +++ b/DotNetShipping/ShippingProviders/USPSInternationalProvider.cs @@ -10,6 +10,8 @@ using System.Xml; using System.Xml.Linq; +using DotNetShipping.Helpers; + namespace DotNetShipping.ShippingProviders { /// @@ -19,6 +21,16 @@ public class USPSInternationalProvider : AbstractShippingProvider private const string PRODUCTION_URL = "http://production.shippingapis.com/ShippingAPI.dll"; private readonly string _service; private readonly string _userId; + + /// + /// If set to true, the rates that are returned and combined across packages will only be done if even package has the same mail service available across returned packages. + /// + /// If 2 packages are calculated and the first package has rates for Priority Mail 2 Day and Standard Post, but the second package only supports Standard Post, then only Standard Post rates would be returned. + /// In instances where this happens, ignored shipping rates will be populated in the InfoMessages property of the Shipment. + /// + /// + private readonly bool _requireUniformMailServices; + private readonly Dictionary _serviceCodes = new Dictionary { {"Priority Mail Express International","Priority Mail Express International"}, @@ -76,6 +88,17 @@ public USPSInternationalProvider(string userId, string service) _service = service; } + /// + /// + /// + public USPSInternationalProvider(string userId, string service, bool requireUniformMailServices) + { + Name = "USPS"; + _userId = userId; + _service = service; + _requireUniformMailServices = requireUniformMailServices; + } + public bool Commercial { get; set; } /// @@ -183,8 +206,36 @@ public bool IsDomesticUSPSAvailable() private void ParseResult(string response) { var document = XDocument.Load(new StringReader(response)); + var excludedMailServices = new List(); + + var rates = document.Descendants("Service").GroupBy(item => (string) item.Element("SvcDescription")).Select(g => new {Name = g.Key, TotalCharges = g.Sum(x => Decimal.Parse((string) x.Element("Postage")))}).ToList(); - var rates = document.Descendants("Service").GroupBy(item => (string) item.Element("SvcDescription")).Select(g => new {Name = g.Key, TotalCharges = g.Sum(x => Decimal.Parse((string) x.Element("Postage")))}); + if (_requireUniformMailServices) + { + // Put together a list of excluded mail services by getting a count of packages and a count of each mail service returned + var totalPackages = document.Descendants("Package").Count(); + var mailServices = from item in document.Descendants("Postage") + group item by (string)item.Element("MailService") + into g + select new + { + Name = g.Key, + Count = g.Count() + }; + + excludedMailServices.AddRange(from mailService in mailServices where mailService.Count < totalPackages select mailService.Name); + } + + // Remove excluded rates + if (excludedMailServices.Count > 0) + { + rates.RemoveAll(x => excludedMailServices.Contains(x.Name)); + + var message = $"Removed {String.Join(", ", excludedMailServices.Select(x => x.SanitizeMailServiceName()))} from returned rates. Rate not available on all packages in Shipment."; + + Shipment.InfoMessages.Add(new InfoMessage(Enums.ShippingProvider.USPSInternational, message)); + Shipment.RatesExcluded = true; + } if (_service == "ALL") { diff --git a/DotNetShipping/ShippingProviders/USPSProvider.cs b/DotNetShipping/ShippingProviders/USPSProvider.cs index f21283f..edf5afd 100644 --- a/DotNetShipping/ShippingProviders/USPSProvider.cs +++ b/DotNetShipping/ShippingProviders/USPSProvider.cs @@ -10,6 +10,8 @@ using System.Xml.Linq; using System.Xml.XPath; +using DotNetShipping.Helpers; + namespace DotNetShipping.ShippingProviders { /// @@ -17,7 +19,6 @@ namespace DotNetShipping.ShippingProviders public class USPSProvider : AbstractShippingProvider { private const string PRODUCTION_URL = "http://production.shippingapis.com/ShippingAPI.dll"; - private const string REMOVE_FROM_RATE_NAME = "<sup>&reg;</sup>"; /// /// If set to ALL, special service types will not be returned. This is a limitation of the USPS API. @@ -27,6 +28,15 @@ public class USPSProvider : AbstractShippingProvider private readonly string _shipDate; private readonly string _userId; + /// + /// If set to true, the rates that are returned and combined across packages will only be done if even package has the same mail service available across returned packages. + /// + /// If 2 packages are calculated and the first package has rates for Priority Mail 2 Day and Standard Post, but the second package only supports Standard Post, then only Standard Post rates would be returned. + /// In instances where this happens, ignored shipping rates will be populated in the InfoMessages property of the Shipment. + /// + /// + private readonly bool _requireUniformMailServices; + /// /// Service codes. {0} is a placeholder for 1-Day, 2-Day, 3-Day, Military, DPO or a space /// @@ -84,7 +94,7 @@ public class USPSProvider : AbstractShippingProvider {"Priority Mail Express {0} Padded Flat Rate Envelope Hold For Pickup","Priority Mail Express {0} Padded Flat Rate Envelope Hold For Pickup"}, {"Priority Mail Express {0} Sunday/Holiday Delivery Padded Flat Rate Envelope","Priority Mail Express {0} Sunday/Holiday Delivery Padded Flat Rate Envelope"} }; - + public USPSProvider() { Name = "USPS"; @@ -120,6 +130,15 @@ public USPSProvider(string userId, string service, string shipDate) _shipDate = shipDate; } + public USPSProvider(string userId, string service, string shipDate, bool requireUniformMailServices) + { + Name = "USPS"; + _userId = userId; + _service = service; + _shipDate = shipDate; + _requireUniformMailServices = requireUniformMailServices; + } + /// /// Returns the supported service codes /// @@ -271,18 +290,46 @@ public bool IsPackageMachinable(Package package) return (package.Width <= 27 && package.Height <= 17 && package.Length <= 17) || (package.Width <= 17 && package.Height <= 27 && package.Length <= 17) || (package.Width <= 17 && package.Height <= 17 && package.Length <= 27); } - + private void ParseResult(string response, IList includeSpecialServiceCodes = null) { var document = XElement.Parse(response, LoadOptions.None); + var excludedMailServices = new List(); - var rates = from item in document.Descendants("Postage") + var rates = (from item in document.Descendants("Postage") group item by (string) item.Element("MailService") into g select new {Name = g.Key, TotalCharges = g.Sum(x => Decimal.Parse((string) x.Element("Rate"))), DeliveryDate = g.Select(x => (string) x.Element("CommitmentDate")).FirstOrDefault(), - SpecialServices = g.Select(x => x.Element("SpecialServices")).FirstOrDefault() }; + SpecialServices = g.Select(x => x.Element("SpecialServices")).FirstOrDefault() }).ToList(); + + if (_requireUniformMailServices) + { + // Put together a list of excluded mail services by getting a count of packages and a count of each mail service returned + var totalPackages = document.Descendants("Package").Count(); + var mailServices = from item in document.Descendants("Postage") + group item by (string)item.Element("MailService") + into g + select new + { + Name = g.Key, + Count = g.Count() + }; + + excludedMailServices.AddRange(from mailService in mailServices where mailService.Count < totalPackages select mailService.Name); + } + + // Remove excluded rates + if (excludedMailServices.Count > 0) + { + rates.RemoveAll(x => excludedMailServices.Contains(x.Name)); + + var message = $"Removed {String.Join(", ", excludedMailServices.Select(x => x.SanitizeMailServiceName()))} from returned rates. Rate not available on all packages in Shipment."; + + Shipment.InfoMessages.Add(new InfoMessage(Enums.ShippingProvider.USPS, message)); + Shipment.RatesExcluded = true; + } foreach (var r in rates) { From 5bac3cb15ccb658238b1e9ed3e3586fb0b0e2a52 Mon Sep 17 00:00:00 2001 From: Stephen Adams Date: Thu, 24 Aug 2017 16:05:29 -0400 Subject: [PATCH 2/3] Nixed Enums class, renamed ShippingProvider --- DotNetShipping/DotNetShipping.csproj | 2 +- DotNetShipping/Enums.cs | 20 ------------------- DotNetShipping/InfoMessage.cs | 4 ++-- DotNetShipping/ShippingProvider.cs | 17 ++++++++++++++++ .../USPSInternationalProvider.cs | 2 +- .../ShippingProviders/USPSProvider.cs | 2 +- 6 files changed, 22 insertions(+), 25 deletions(-) delete mode 100644 DotNetShipping/Enums.cs create mode 100644 DotNetShipping/ShippingProvider.cs diff --git a/DotNetShipping/DotNetShipping.csproj b/DotNetShipping/DotNetShipping.csproj index 52e3ec6..a6c4e66 100644 --- a/DotNetShipping/DotNetShipping.csproj +++ b/DotNetShipping/DotNetShipping.csproj @@ -66,7 +66,7 @@ - + diff --git a/DotNetShipping/Enums.cs b/DotNetShipping/Enums.cs deleted file mode 100644 index e4fa1d5..0000000 --- a/DotNetShipping/Enums.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace DotNetShipping -{ - public class Enums - { - public enum ShippingProvider - { - UPS, - USPS, - USPSInternational, - FedEx, - FedExSmartPost - } - } -} diff --git a/DotNetShipping/InfoMessage.cs b/DotNetShipping/InfoMessage.cs index 76650bc..f8bdba3 100644 --- a/DotNetShipping/InfoMessage.cs +++ b/DotNetShipping/InfoMessage.cs @@ -11,14 +11,14 @@ public class InfoMessage /// /// Shipping provider that generated the message /// - public Enums.ShippingProvider ShippingProvider { get; set; } + public ShippingProvider ShippingProvider { get; set; } /// /// Message /// public String Message { get; set; } - public InfoMessage(Enums.ShippingProvider shippingProvider, String message) + public InfoMessage(ShippingProvider shippingProvider, String message) { ShippingProvider = shippingProvider; Message = message; diff --git a/DotNetShipping/ShippingProvider.cs b/DotNetShipping/ShippingProvider.cs new file mode 100644 index 0000000..380ab55 --- /dev/null +++ b/DotNetShipping/ShippingProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetShipping +{ + public enum ShippingProvider + { + UPS, + USPS, + USPSInternational, + FedEx, + FedExSmartPost + } +} diff --git a/DotNetShipping/ShippingProviders/USPSInternationalProvider.cs b/DotNetShipping/ShippingProviders/USPSInternationalProvider.cs index d98394f..1b6b38a 100644 --- a/DotNetShipping/ShippingProviders/USPSInternationalProvider.cs +++ b/DotNetShipping/ShippingProviders/USPSInternationalProvider.cs @@ -233,7 +233,7 @@ into g var message = $"Removed {String.Join(", ", excludedMailServices.Select(x => x.SanitizeMailServiceName()))} from returned rates. Rate not available on all packages in Shipment."; - Shipment.InfoMessages.Add(new InfoMessage(Enums.ShippingProvider.USPSInternational, message)); + Shipment.InfoMessages.Add(new InfoMessage(ShippingProvider.USPSInternational, message)); Shipment.RatesExcluded = true; } diff --git a/DotNetShipping/ShippingProviders/USPSProvider.cs b/DotNetShipping/ShippingProviders/USPSProvider.cs index edf5afd..dd09eed 100644 --- a/DotNetShipping/ShippingProviders/USPSProvider.cs +++ b/DotNetShipping/ShippingProviders/USPSProvider.cs @@ -327,7 +327,7 @@ into g var message = $"Removed {String.Join(", ", excludedMailServices.Select(x => x.SanitizeMailServiceName()))} from returned rates. Rate not available on all packages in Shipment."; - Shipment.InfoMessages.Add(new InfoMessage(Enums.ShippingProvider.USPS, message)); + Shipment.InfoMessages.Add(new InfoMessage(ShippingProvider.USPS, message)); Shipment.RatesExcluded = true; } From 157137eab66d4c08ea252990a90f8a464d191c09 Mon Sep 17 00:00:00 2001 From: Stephen Adams Date: Sat, 3 Feb 2018 18:45:11 -0500 Subject: [PATCH 3/3] Unit tests for residential vs. business rates --- .../Features/FedExShipRates.cs | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/DotNetShipping.Tests/Features/FedExShipRates.cs b/DotNetShipping.Tests/Features/FedExShipRates.cs index 7e464a3..855828e 100644 --- a/DotNetShipping.Tests/Features/FedExShipRates.cs +++ b/DotNetShipping.Tests/Features/FedExShipRates.cs @@ -36,9 +36,9 @@ public class FedExShipRates : FedExShipRatesTestsBase [Fact] public void FedExReturnsRates() { - var from = new Address("Annapolis", "MD", "21401", "US"); - var to = new Address("Fitchburg", "WI", "53711", "US"); - var package = new Package(7, 7, 7, 6, 0); + var from = new Address("", "", "60084", "US"); + var to = new Address("", "", "80465", "US") { IsResidential = true }; + var package = new Package(24, 24, 13, 50, 0); var r = _rateManager.GetRates(from, to, package); var fedExRates = r.Rates.ToList(); @@ -52,6 +52,51 @@ public void FedExReturnsRates() } } + [Fact] + public void FedExReturnsDifferentRatesForResidentialAndBusiness() + { + var from = new Address("Annapolis", "MD", "21401", "US"); + var residentialTo = new Address("Fitchburg", "WI", "53711", "US") { IsResidential = true }; + var businessTo = new Address("Fitchburg", "WI", "53711", "US") { IsResidential = false }; + var package = new Package(7, 7, 7, 6, 0); + + var residential = _rateManager.GetRates(from, residentialTo, package); + var residentialRates = residential.Rates.ToList(); + + var business = _rateManager.GetRates(from, businessTo, package); + var businessRates = business.Rates.ToList(); + + var homeFound = false; + var groundFound = false; + + // FedEx Home should come back for Residential + foreach (var rate in residentialRates) + { + if (rate.ProviderCode.Equals("GROUND_HOME_DELIVERY")) + homeFound = true; + if (rate.ProviderCode.Equals("FEDEX_GROUND")) + groundFound = true; + } + + Assert.True(homeFound); + Assert.True(!groundFound); + + homeFound = false; + groundFound = false; + + // FedEx Ground should come back for Business + foreach (var rate in businessRates) + { + if (rate.ProviderCode.Equals("GROUND_HOME_DELIVERY")) + homeFound = true; + if (rate.ProviderCode.Equals("FEDEX_GROUND")) + groundFound = true; + } + + Assert.True(!homeFound); + Assert.True(groundFound); + } + [Fact] public void FedExReturnsDifferentRatesForSignatureOnDelivery() {