diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5a73418 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Addy Rock RMS Plugin")] +[assembly: AssemblyDescription("Standardizes and Geocodes a Rock RMS address using the Addy service")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Addy")] +[assembly: AssemblyCopyright("Copyright © Stewmystre 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("21f3eadd-9c49-4313-a112-a48b02398a05")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/README.md b/README.md index 24f1b7f..2318e2c 100644 --- a/README.md +++ b/README.md @@ -1 +1,33 @@ -# Addy \ No newline at end of file +# Rock Addy Location Service + +## Intro +This is a location service for [Rock](http://rockrms.com) that verifies, standardises and geocodes New Zealand (NZ) addresses using the [Addy](http://mappify.io) API. The generous 500 free requests per month on the free signup option will meet most small churches needs. + +This plugin is available on Github to help the New Zealand churches using Rock RMS. The repository includes the C# source for use with the [Rockit SDK](http://www.rockrms.com/Rock/Developer). To download the latest release of the plugin in .dll format for quick install into the Rock bin folder click [here](https://github.com/stewmystre/Addy/releases/latest). Checkout the [wiki](https://github.com/stewmystre/Addy/wiki) for more detailed install instructions. + +## A Quick Explanation +This location service will pass the values (if any are present) of the address line 1, address line 2, city, state, and postal code fields from Rock to the Addy Address Validation API service. If values are present in the response, it will either: +1. confirm verification and replace the address values stored in Rock with the standardised response values, including geocode coordinates, or +2. deny verification, due to multiple matches, and instead provide the first few listed matches provided by Addy. If this happens, check the recommended match details provided and if suitable update the address to one of the options and verify again. + +The Rock Data Field is updated to match the Addy Address Details Metadata Properties as per the table: + +Rock Location Data Field | Addy Address Details Property +---- | ---- +Street1| address1 +Street2 | address2 +City | city +PostalCode | postcode + +## Addy Data +Addy uses New Zealand addresses sourced from the PAF, GeoPAF and LINZ, more [info](https://www.addy.co.nz/faq-where-does-address-lookup-data-come-from). + +## Contribute +If anything looks broken or you think of an improvement please flag up an issue. + +## Thanks +Thanks to [Porirua Elim](https://www.porirua.elim.org.nz/) for sponsoring this plugin after seeing the work done for [Hope Central](https://hopecentral.melbourne/) in Australia whose [mappify.io](https://github.com/hopecentral/mappify.io) plugin was used as the base for this one. +Thanks to [Bricks and Mortar Studio](https://bricksandmortarstudio.com/) whose [IdealPostcodes](https://github.com/BricksandMortar/IdealPostcodes) plugin was where internationalisation of Rock got a start. +Thanks to the [Spark Development Network](https://sparkdevnetwork.org/) for creating [Rock](https://github.com/SparkDevNetwork/Rock) and making it so accessible. + +This project is licensed under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.html). diff --git a/com.stewmystre.Addy.cs b/com.stewmystre.Addy.cs new file mode 100644 index 0000000..08319c5 --- /dev/null +++ b/com.stewmystre.Addy.cs @@ -0,0 +1,420 @@ +// +// Copyright 2020 Stewmystre +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Composition; +using System.Linq; +using System.Net; + +using Newtonsoft.Json; +using RestSharp; + +using Rock; +using Rock.Address; +using Rock.Attribute; + +namespace com.stewmystre.Addy.Address +{ + /// + /// An address lookup and geocoding service using Addy + /// + [Description("An address verification and geocoding service from Addy")] + [Export(typeof(VerificationComponent))] + [ExportMetadata("ComponentName", "Addy")] + [TextField("API Key", "Your Addy API Key", true, "", "", 2)] + [TextField("API Secret", "Your Addy API Secret", true, "", "", 2)] + public class Addy : VerificationComponent + { + /// + /// Standardizes and Geocodes an address using the Addy service + /// + /// The location + /// The result + /// + /// True/False value of whether the verification was successful or not + /// + public override VerificationResult Verify(Rock.Model.Location location, out string resultMsg) + { + resultMsg = string.Empty; + VerificationResult result = VerificationResult.None; + + string apiKey = GetAttributeValue("APIKey"); + string apiSecret = GetAttributeValue("APISecret"); + + //Create input streetAddress string to send to Addy + var addressParts = new[] { location.Street1, location.Street2, location.City, location.State, location.PostalCode }; + string streetAddress = string.Join(" ", addressParts.Where(s => !string.IsNullOrEmpty(s))); + + //Restsharp API request + var client = new RestClient("https://api.addy.co.nz/"); + var request = BuildRequest(streetAddress, apiKey, apiSecret); + var response = client.Execute(request); + + if (response.StatusCode == HttpStatusCode.OK) + //Deserialize response into object + { + var settings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + MissingMemberHandling = MissingMemberHandling.Ignore + }; + var addyResponse = JsonConvert.DeserializeObject(response.Content, settings); + var addyAddress = addyResponse.address; + var addyAddressAlternatives = addyResponse.alternatives; + if (addyAddress.IsNotNull()) + { + location.StandardizeAttemptedResult = addyAddress.linzid.ToString(); + + if (addyAddressAlternatives.IsNull() || addyAddressAlternatives.Count() == 0 ) + { + bool updateResult = UpdateLocation(location, addyAddress); + var generalMsg = string.Format("Verified with Addy to match LINZ: {0}. Input address: {1}. ", addyAddress.linzid.ToString(), streetAddress); + var standardisedMsg = "Coordinates NOT updated."; + var geocodedMsg = "Coordinates updated."; + + if (updateResult) + { + //result = VerificationResult.Geocoded; + resultMsg = generalMsg + geocodedMsg; + } + else + { + //result = VerificationResult.Standardized; + resultMsg = generalMsg + standardisedMsg; + } + result |= VerificationResult.Standardized; + } + else + { + resultMsg = string.Format("Not verified: {0}", addyResponse.reason); + if (addyAddressAlternatives.Count() > 0) + { + var tooManyMsg = "Too many to display..."; + foreach (AddressReference alternate in addyAddressAlternatives) + { + if (resultMsg.Length + alternate.a.Length >= 195) + { + if (resultMsg.Length + tooManyMsg.Length <= 200) + { + resultMsg += tooManyMsg; + } + else + { + resultMsg += "..."; + } + + break; + } else + { + resultMsg += alternate.a + "; "; + } + } + } + } + } + else + { + resultMsg = addyResponse.reason; + } + } + else + { + result = VerificationResult.ConnectionError; + resultMsg = response.StatusDescription; + } + + location.StandardizeAttemptedServiceType = "Addy"; + location.StandardizeAttemptedDateTime = RockDateTime.Now; + + location.GeocodeAttemptedServiceType = "Addy"; + location.GeocodeAttemptedDateTime = RockDateTime.Now; + return result; + } + + /// + /// Builds a REST request + /// + /// + /// + /// + /// + private static IRestRequest BuildRequest(string streetAddress, string apiKey, string apiSecret) + { + var request = new RestRequest("validation", Method.GET); + request.RequestFormat = DataFormat.Json; + request.AddHeader("Accept", "application/json"); + request.AddParameter("address", streetAddress); + request.AddParameter("key", apiKey); + request.AddParameter("secret", apiSecret); + + return request; + } + + /// + /// Updates a Rock location to match a Addy AddressDetail + /// + /// The Rock location to be modified + /// The Addy AddressDetail to copy the data from + /// Whether the Location was succesfully geocoded + public bool UpdateLocation(Rock.Model.Location location, AddressDetail address) + { + location.Street1 = address.address1; + location.Street2 = address.address2; + location.City = address.city; + location.State = String.Empty; + location.PostalCode = address.postcode; + location.StandardizedDateTime = RockDateTime.Now; + + // If AddressDetail has geocoding data set it on Location + if (address.x.IsNotNullOrWhiteSpace() && address.y.IsNotNullOrWhiteSpace()) + { + bool setLocationResult = location.SetLocationPointFromLatLong(Convert.ToDouble(address.y), Convert.ToDouble(address.x)); + if (setLocationResult) + { + location.GeocodedDateTime = RockDateTime.Now; + } + return setLocationResult; + } + + return false; + } + +#pragma warning disable + + /// + /// Address metadata and details. See: https://www.addy.co.nz/address-details-api + /// + public class AddressDetail + { + /// + /// Unique Addy identifier + /// + public int id { get; set; } + + /// + /// Unique NZ Post identifier.This property can be null for a non-mail deliverable address + /// + public int dpid { get; set; } + + /// + /// Unique Land Information New Zealand(LINZ) identifier + /// + public int linzid { get; set; } + + /// + /// Unique Parcel identifier + /// + public int parcelid { get; set; } + + /// + /// Unique Statistics New Zealand(Stats NZ) identifier to match census data + /// + public int meshblock { get; set; } + + /// + /// Street number.Street number will be "80" in case of "80A Queen Street" + /// + public string number { get; set; } + + /// + /// Rural delivery number (postal only) for rural addresses + /// + public string rdnumber { get; set; } + + /// + /// Street alpha e.g. "A" in the case of "80A Queen Street" + /// + public string alpha { get; set; } + + /// + /// Type of unit e.g. "FLAT" in "FLAT 3, 80 Queen Street" + /// + public string unittype { get; set; } + + /// + /// Unit number e.g. "3" in "FLAT 3, 80 Queen Street" + /// + public string unitnumber { get; set; } + + /// + /// Floor number e.g. "Floor 5" in "Floor 5, 80 Queen Street" + /// + public string floor { get; set; } + + /// + /// Street name.The name of the street / road, including prefix + /// + public string street { get; set; } + + /// + /// Suburb name string (max 60) + /// + public string suburb { get; set; } + + /// + /// Name of the town or city provided by Land Information New Zealand (LINZ) (max 60) + /// + public string city { get; set; } + + /// + /// Name of the town or city provided by NZ Post (max 60) + /// + public string mailtown { get; set; } + + /// + /// Territorial authority of the address (max 20) + /// + public string territory { get; set; } + + /// + /// Regional authority of the address.See regions of NZ (max 20) + /// + public string region { get; set; } + + /// + /// NZ Post code used for defining an area string (max 4) + /// + public string postcode { get; set; } + + /// + /// Name of the building string (max 60) + /// + public string building { get; set; } + + /// + /// Full display name or label for an address (max 90) + /// + public string full { get; set; } + + /// + /// One line address display name (max 70) + /// + public string displayline { get; set; } + + /// + /// Line 1 in a 4 address field form string (max 60) + /// + public string address1 { get; set; } + + /// + /// Line 2 in a 4 address field form string (max 60) + /// + public string address2 { get; set; } + + /// + /// Line 3 in a 4 address field form string (max 60) + /// + public string address3 { get; set; } + + /// + /// Line 4 in a 4 address field form string (max 60) + /// + public string address4 { get; set; } + + /// + /// Address Type (Urban, Rural, PostBox, NonPostal) + /// + public string type { get; set; } + + /// + /// The PO Box number for PO Box addresses + /// + public string boxbagnumber { get; set; } + + /// + /// NZ Post outlet or agency where the PO Box is located + /// + public string boxbaglobby { get; set; } + + /// + /// Longitude coordinates in WGS84 format (max 20) + /// + public string x { get; set; } + + /// + /// Latitude coordinates in WGS84 format (max 20) + /// + public string y { get; set; } + + /// + /// Last updated date + /// + public string modified { get; set; } + + /// + /// True/False to indicate if the address was sourced from PAF (or LINZ = false) + /// + public bool paf { get; set; } + + /// + /// True/False to indicate if the address was deleted from the source (PAF or LINZ) + /// + public bool deleted { get; set; } + } + + /// + /// Address reference + /// + public class AddressReference + { + /// + /// Unique identifier for the address + /// + public int id { get; set; } + + /// + /// Full display name of the address + /// + public string a { get; set; } + } + + /// + /// Address Validation Result + /// + public class AddressVerificationResult + { + /// + /// The matched address. + /// + public AddressDetail address { get; set; } + + /// + /// Alternative address matches. + /// + public List alternatives { get; set; } + + /// + /// The reason for the match result. + /// + public string reason { get; set; } + + /// + /// True if a prefix was found. + /// + public bool foundPrefix { get; set; } + + /// + /// Found a prefix, such as "Front Door" or "Rear Unit" + /// + public string prefix { get; set; } + } + +#pragma warning restore + + } +} diff --git a/com.stewmystre.Addy.csproj b/com.stewmystre.Addy.csproj new file mode 100644 index 0000000..5c67807 --- /dev/null +++ b/com.stewmystre.Addy.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {21F3EADD-9C49-4313-A112-A48B02398A05} + Library + Properties + com.stewmystre.Addy.Address + com.stewmystre.Addy + v4.5.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\RockWeb\Bin\DotLiquid.dll + + + False + ..\RockWeb\Bin\Newtonsoft.Json.dll + + + ..\RockWeb\Bin\RestSharp.dll + + + ..\RockWeb\Bin\Rock.dll + + + ..\RockWeb\Bin\Rock.Version.dll + + + + + + + + + + + + + + + + + + + + \ No newline at end of file