/// <summary> /// /// </summary> /// <param name="ca"></param> /// <param name="wellIdsInsideCA">The well IDs for the CA as returned by a spatial query.</param> /// <returns></returns> private Dictionary<int, JsonErrorCondition> GetMeterInstallationErrors(ContiguousAcres ca, IEnumerable<int> wellIdsInsideCA) { var ret = new Dictionary<int, JsonErrorCondition>(); var mdalc = new MeterDalc(); var rptgDalc = new ReportingDalc(); if (ca.Wells == null) { ca.Wells = new WellDalc().GetWells(wellIdsInsideCA.ToArray()).ToArray(); } // Check for irregularities in meter installations. // 1. If a meter has a well associated with it that's outside the CA, note it. foreach (var well in ca.Wells) { try { foreach (var miid in well.MeterInstallationIds) { var wids = mdalc.GetAssociatedWells(miid).Select(x => x.WellId).Except(wellIdsInsideCA); if (wids.Count() > 0) { // There's a well associated that's not in the CA. // Check to see if there's a user response. ret[miid] = new JsonErrorCondition() { wellIds = wids.ToArray(), errorCondition = "Associted well IDs " + string.Join(", ", wids.Select(x => "#" + x.ToString())) + " are outside the contiguous area.", userResponse = rptgDalc.GetMeterInstallationErrorResponse(miid, ActualUser.ActingAsUserId ?? ActualUser.Id) }; } } } catch (MeterNotFoundException ex) { // Suggests that the meter installation was deleted, // but is still associated with the well for some reason. // TODO: Not sure how to handle this. } } return ret; }
/// <summary> /// Creates a dynamic object representing the page state. /// This method will attempt to load and deserialize JSON /// from the database if it exists for this user (updating /// relevant properties appropriately), and if no existing /// JSON exists will construct a new object with which the /// user can report water usage. /// </summary> /// <returns></returns> protected dynamic GetPageStateJson(User user) { // Only update the _current_ reporting year or unsubmitted var pageStateJson = new ReportingDalc().GetReportingSummary(user.IsAdmin ? (user.ActingAsUserId ?? user.Id) : user.Id); ReportingSummary pageState = null; try { pageState = JsonConvert.DeserializeObject<ReportingSummary>(pageStateJson); } catch (Exception ex) { // There was a problem deserializing. // TODO: Notify user, possibly admins! throw; } if (pageState == null) { pageState = new ReportingSummary(); } /// Returns the volume (Item1) and a bool indicating whether rollover occurred (Item2) /// Args: meterInstallationId, meterreadings Func<int, IEnumerable<JsonMeterReading>, Tuple<int, bool>> calculateVolume = (meterInstallationId, readings) => { double runningTotal = 0d; if (readings.Count() == 0) { return new Tuple<int, bool>((int)runningTotal, false); } bool rolledOver = false; var prevReading = readings.First(); foreach (var currentReading in readings.Skip(1)) { double delta = 0; if (currentReading.reading.GetValueOrDefault() < prevReading.reading.GetValueOrDefault()) { // Rollover occurred rolledOver = true; // Retrieve the rollover value for this meter installation var rollover = new MeterDalc().GetMeterInstallations(meterInstallationId).First().RolloverValue; delta = (rollover - prevReading.reading.Value) + currentReading.reading.Value; } else { delta = currentReading.reading.GetValueOrDefault() - prevReading.reading.GetValueOrDefault(); } runningTotal += delta * (prevReading.rate.HasValue ? prevReading.rate.Value : 1); prevReading = currentReading; } return new Tuple<int, bool>((int)runningTotal, rolledOver); }; // Only show Approved CAs unless the user is an admin var contigAcres = new GisDalc().GetContiguousAcres(user, true).Where(ca => user.IsAdmin || ca.IsApproved); // Key: CA ID, Value: well ids var wellIds = new Dictionary<int, HashSet<int>>(); Dictionary<int, ContiguousAcres> validAcres = new Dictionary<int, ContiguousAcres>(); // There may be load errors that got persisted from a previous page load, but we // only want to show current ones so clear the list. pageState.loadErrors.Clear(); if (contigAcres.Count() > 0) { // Only create the service if contigAcres has stuff, because SOAP takes time var service = new GisServiceSoapClient(); foreach (var ca in contigAcres) { string result = ""; // This call is prone to failure whenever the GIS service is down. try { result = service.GetWellIDsByCA(ca.OBJECTID); } catch (Exception ex) { pageState.loadErrors.Add("Unable to load well associations for any CAs: the GIS service appears to be down!"); } try { int[] ids = JsonConvert.DeserializeObject<int[]>(result.Trim('{', '}')); wellIds[ca.caID] = new HashSet<int>(ids); validAcres[ca.caID] = ca; } catch (JsonException ex) { pageState.loadErrors.Add("Error loading CA ID " + ca.caID + ": " + ex.Message + " Service response: " + result); } } } var mdalc = new MeterDalc(); var rptgDalc = new ReportingDalc(); var wellDalc = new WellDalc(); var wells = wellDalc.GetWells(wellIds.SelectMany(x => x.Value).ToArray()); var meterInstallations = mdalc.GetMeterInstallations(wells.SelectMany(x => x.MeterInstallationIds).ToArray()).GroupBy(x => x.Id).Select(x => x.First()).ToDictionary(x => x.Id, x => x); // In addition to the wells retrieved by the spatial query, we also need to // retrieve wells attached to the meters on that initial set. wells = wells.Concat(wellDalc.GetWellsByMeterInstallationIds(meterInstallations.Select(x => x.Key).ToArray())) .Distinct((a,b) => a.WellId == b.WellId); Func<int, Dictionary<int, JsonBankedWaterRecord>> getBankedWaterTable = caId => { return rptgDalc.GetHistoricalBankedWater(caId).ToDictionary(x => x.year, x => x); }; // func to populate a JsonCA in the current reporting year Func<ContiguousAcres, JsonContiguousAcres> createNewJsonCA = ca => { ca.Wells = wellDalc.GetWells(wellIds[ca.caID].ToArray()).ToArray(); var reportedVolumes = mdalc.GetReportedVolumeGallons(ca.Wells.SelectMany(w => w.MeterInstallationIds), ca.caID, CurrentReportingYear); JsonContiguousAcres ret = new JsonContiguousAcres(ca, CurrentReportingYear); ReportedVolume rvol; ret.meterReadings = new Dictionary<int, JsonMeterReadingContainer>(); MeterInstallation meter = null; foreach (var miid in ret.wells.SelectMany(x => x.meterInstallationIds).Distinct()) { meterInstallations.TryGetValue(miid, out meter); var container = new JsonMeterReadingContainer() { meterInstallationId = miid, calculatedVolumeGallons = reportedVolumes.TryGetValue(miid, out rvol) ? rvol.CalculatedVolumeGallons : 0, acceptCalculation = rvol == null || !rvol.UserRevisedVolumeUnitId.HasValue, userRevisedVolume = rvol != null && rvol.UserRevisedVolume.HasValue ? rvol.UserRevisedVolume.Value : 0, userRevisedVolumeUnitId = rvol != null && rvol.UserRevisedVolumeUnitId.HasValue ? rvol.UserRevisedVolumeUnitId.Value : new int?(), totalVolumeAcreInches = 0, isNozzlePackage = meter != null ? meter.MeterType.Description().ToLower() == "nozzle package" : false, isElectric = meter != null ? meter.MeterType.Description().ToLower() == "electric" : false, isNaturalGas = meter != null ? meter.MeterType.Description().ToLower() == "natural gas" : false, isThirdParty = meter != null ? meter.MeterType.Description().ToLower() == "bizarro-meter" : false, depthToRedbed = -99999, //squeezeValue = 0, unitId = meter != null ? meter.UnitId : null, county = meter != null ? meter.County : "", rolloverValue = meter != null ? meter.RolloverValue : 0, countyId = meter != null ? meter.CountyId : -1, meterType = meter != null ? meter.MeterType.Description() : "", nonStandardUnits = meter != null && meter.MeterType != HPEntities.Entities.Enums.MeterType.standard ? new MeterDalc().GetUnits(meter.MeterType).Description() : "", meterMultiplier = meter.Multiplier != 0 ? meter.Multiplier : 1.0d // Make sure multiplier is not 0. NULL value is already converted to 1.0d in Dalc }; // Add depth to redbed if the meter is of 1) natural gas or 2) electric if (container.isNaturalGas || container.isElectric) { /* //ned skip this lookup until we really need to implement it var service1 = new GisServiceSoapClient(); string depthInString = service1.GetDepthToRedbedByMeter(miid); string depthInString = "Implement this later" double depth = -99999; if (!Double.TryParse(depthInString, out depth)) depth = -99999; */ double depth = 100; //take this out when the above works container.depthToRedbed = depth; } container.readings = (from mr in mdalc.GetReadings(miid, CurrentReportingYear).Reverse() // By default, they're ordered by reading date desc select new JsonMeterReading() { reading = mr.Reading, rate = mr.Rate, isValidBeginReading = ReportingDalc.IsMeterReadingValidBeginReading(mr, CurrentReportingYear), isValidEndReading = ReportingDalc.IsMeterReadingValidEndReading(mr, CurrentReportingYear), isAnnualTotalReading = ReportingDalc.IsMeterReadingAnnualTotal(mr, CurrentReportingYear), meterInstalltionReadingID = mr.MeterInstallationReadingId }).ToArray(); ret.meterReadings[miid] = container; } ret.meterInstallationErrors = GetMeterInstallationErrors(ca, wellIds[ca.caID]); ret.bankedWaterHistory = getBankedWaterTable(ca.caID); JsonBankedWaterRecord bankedWaterLastYear; ret.annualUsageSummary = new JsonAnnualUsageSummary() { contiguousArea = ca.AreaInAcres, annualVolume = 0, // this will only be populated after submittal allowableApplicationRate = rptgDalc.GetAllowableProductionRate(CurrentReportingYear), bankedWaterFromPreviousYear = ret.bankedWaterHistory.TryGetValue(CurrentReportingYear - 1, out bankedWaterLastYear) ? bankedWaterLastYear.bankedInches : 0 }; ret.isSubmitted = rptgDalc.IsSubmitted(ca.caID, CurrentReportingYear); ret.ownerClientId = ca.OwnerClientId; ret.isCurrentUserOwner = (this.ActualUser.ActingAsUserId ?? this.ActualUser.Id) == ca.OwnerClientId; return ret; }; JsonReportingYear year; if (pageState.years.TryGetValue(CurrentReportingYear, out year)) { // Check the existing year object and update as necessary // For now, wholly reload any CAs that have not yet been submitted. List<JsonContiguousAcres> removals = year.contiguousAcres.Where(ca => !ca.isSubmitted).ToList(); HashSet<int> submittedCaIds = new HashSet<int>(year.contiguousAcres.Where(ca => ca.isSubmitted).Select(x => x.number)); // Ensure that all the CAs are still applicable to this account - the CA IDs need // to match what's presently associated with the user account, else they're removed. foreach (var ca in removals) { year.contiguousAcres.Remove(ca); } // Add any CAs that were missing from the original state foreach (var kv in validAcres) { if (!submittedCaIds.Contains(kv.Value.caID)) { year.contiguousAcres.Add(createNewJsonCA(kv.Value)); } for (int i = 0; i < year.contiguousAcres.Count; i++) { if (year.contiguousAcres[i].number == kv.Value.caID) { year.contiguousAcres[i].meterInstallationErrors = GetMeterInstallationErrors(kv.Value, wellIds[kv.Value.caID]); year.contiguousAcres[i].wells = kv.Value.Wells.Select(w => new JsonWell(w)).ToArray(); } } //year.contiguousAcres.Where(jca => jca.number == kv.Value.caID).First().meterInstallationErrors = GetMeterInstallationErrors(kv.Value, wellIds[kv.Value.caID]); } foreach (var ca in year.contiguousAcres) { // This is used on the frontend to decide whether to show the user an editable form. // Only CA owners get the editable form. ca.isCurrentUserOwner = (this.ActualUser.ActingAsUserId ?? this.ActualUser.Id) == ca.ownerClientId; } } else { // No existing year record exists; create a new one year = new JsonReportingYear(); year.contiguousAcres = validAcres.Values.Select(ca => { return createNewJsonCA(ca); }).ToList(); pageState.years[CurrentReportingYear] = year; } // Populate meterInstallations by retrieving metadata for all used meter installations HashSet<int> meterInstallationIds = new HashSet<int>((from y in pageState.years from ca in y.Value.contiguousAcres from w in ca.wells select w.meterInstallationIds).SelectMany(x => x)); // Converting this directly to a dictionary will fail in the event that a meter // is associated with wells in multiple counties. However, that case doesn't really // make any sense and would break all sorts of other things (such as meter unit conversion factors // that are specific to counties), so we're going to assume that there's always // a single county. pageState.meterInstallations = mdalc.GetMeterInstallations(meterInstallationIds.ToArray()) .GroupBy(x => x.Id) .Select(mi => new JsonMeterInstallation(mi.First())) .ToDictionary( x => x.id, x => x ); pageState.currentReportingYear = CurrentReportingYear; pageState.isReportingAllowed = IsReportingAllowed; pageState.adminOverride = ((!IsReportingAllowed) && this.ActualUser.IsAdmin); pageState.cafoLookups = rptgDalc.GetCafoLookups(); // Go through and check all the submittal states of the CAs foreach (var ca in pageState.years[CurrentReportingYear].contiguousAcres) { ca.isSubmitted = rptgDalc.IsSubmitted(ca.number, CurrentReportingYear); if (ca.isSubmitted) { // If it was a submitted CA, the banked water history table // still needs to be loaded because it wasn't set above. ca.bankedWaterHistory = getBankedWaterTable(ca.number); } } return pageState; }
/// <summary> /// Validates CA for usage submittal. Assumes current reporting year. /// </summary> /// <param name="ca"></param> /// <param name="errors"></param> /// <returns></returns> protected bool ValidateContiguousAcres(JsonContiguousAcres ca, out List<string> errors) { errors = new List<string>(); /* MWinckler.20130202: This set of validation is written as CA-specific, but it is * attempting to validate meter-specific conditions. This validation needs to be * rewritten to validate meter readings, and also not rely on calculated boolean * values sent from the (untrustworthy) client. Rely on the actual submitted meter * readings and user inputs - we have them all here. if (ca.isFakingValidReadings) { if(!ca.userRevisedVolume) errors.Add("The end reading for one of the meters is not in the valid reporting range of December 15 to January 15."); } if (ca.notEnoughReadingsForAllMeters) // This also allows user override { if (!ca.userRevisedVolume) errors.Add("One of the meters does not have enough valid readings for volume calculation."); } if (!ca.hasValidBeginReadings) // This also allows user override { if (!ca.userRevisedVolume) errors.Add("One of the meters does not have valid begin readings for volume calculation."); } */ foreach (var mr in ca.meterReadings) { // If this is an annual reading or if the user supplied a revised volume, this validation doesn't matter. if (!mr.Value.readings.Any(x => x.isAnnualTotalReading) && mr.Value.userRevisedVolume.GetValueOrDefault(0) <= 0) { if (mr.Value.readings.Length < 2) { // Too few readings. errors.Add("One of the meters does not have enough valid readings for volume calculation."); } else if (!mr.Value.readings.Any(x => x.isValidBeginReading)) { // Need both a valid begin and a valid end reading. // Note that the above check is not good - it depends on a value // sent by the client. // TODO: Refactor this to not rely on .isValidBeginReading; get // the values needed to validate meter reading against the database // using the methods in ReportingDalc. errors.Add("One of the meters does not have valid begin readings for volume calculation."); } else if (!mr.Value.readings.Any(x => x.isValidEndReading)) { // TODO: See above comment regarding trusting vlient values. errors.Add("One of the meters does not have valid ending readings for volume calculation."); } } } // Ensure the CA actually exists in the database. if (!new GisDalc().ContiguousAcresExists(ca.number)) { errors.Add("The specified contiguous area (ID: " + ca.number + ") does not exist."); } else { // (the remainder of validation is only useful if the CA actually exists) // Verify that the user is actually associated with the CA if (!new GisDalc().OwnsContiguousAcres(ActualUser, ca.number)) { errors.Add("Records show that you do not own the specified contiguous acres. You may not record banked water for this area."); return false; } // Ensure the CA is for the current reporting year. if (ca.year != CurrentReportingYear) { errors.Add("You cannot submit usage reports for previous operating years."); } // Check to see if the CA has already been submitted. if (new ReportingDalc().IsSubmitted(ca.number, CurrentReportingYear)) { errors.Add("The usage report has already been submitted for this contiguous area in this reporting year."); } if (ca.annualUsageSummary.contiguousArea <= 0) { errors.Add("The contiguous area has no acreage defined!"); } foreach (var mr in ca.meterReadings) { if (mr.Value.acceptCalculation) { if (mr.Value.calculatedVolumeGallons < 0) { errors.Add("Meter ID " + mr.Key.ToString() + " has an invalid volume."); } } else { if (mr.Value.userRevisedVolume <= 0) { errors.Add("Meter ID " + mr.Key.ToString() + " has an invalid user-revised volume."); } } } if (ca.annualUsageSummary.desiredBankInches < 0) { errors.Add("Desired water bank value cannot be negative."); } // Retrieve allowable application rate for this year // It's also on the submitted CA JSON, but we cannot // trust the client here! var rdalc = new ReportingDalc(); var allowableRate = rdalc.GetAllowableProductionRate(CurrentReportingYear); if (ca.annualUsageSummary.desiredBankInches > allowableRate) { errors.Add(string.Format("You cannot bank more than {0} inches for this reporting year.", allowableRate)); } // Check all associated wells foreach (var well in ca.wells) { if (well.meterInstallationIds.Length == 0) { // Check to see if there's already been a user error response if (!rdalc.IsWellErrorResponseRecorded(well.id, ActualUser.ActingAsUserId ?? ActualUser.Id)) { errors.Add("Well #" + well.permitNumber + " has no associated meters."); } } } // Retrieve previous banked water values for the CA. // (These are on the ca object, but we can't trust the client.) var bankHistory = new ReportingDalc().GetHistoricalBankedWater(ca.number); double prevBank = bankHistory.Where(x => x.year == CurrentReportingYear - 1).Select(x => x.bankedInches).DefaultIfEmpty(0).First();//mjia: round banked water to the 10ths. int avgAppRate = (int)(ca.annualUsageSummary.annualVolume / ca.annualUsageSummary.contiguousArea); int surplusSubtotal = ca.annualUsageSummary.allowableApplicationRate - avgAppRate; var surplusTotal = prevBank + surplusSubtotal; if (ca.annualUsageSummary.desiredBankInches > 0 && ca.annualUsageSummary.desiredBankInches > surplusTotal) { errors.Add("You cannot bank more water than your cumulative available total (previous year's banked inches plus this year's surplus/deficit)."); } } return errors.Count == 0; }
public JsonResult SubmitUsageReport(string state, string ca) { var contigAcres = JsonConvert.DeserializeObject<JsonContiguousAcres>(ca); AutosaveState(state); List<string> validationErrors; if (!ValidateContiguousAcres(contigAcres, out validationErrors)) { return Json(new JsonResponse(false, validationErrors.ToArray())); } // Save the reporting summary values independently of the json. // Important stuff: // - Contiguous Area // - Annual Volume // - Banked water from previous year // - Desired bank inches // For each meter: // - meter installation id // - calculated volume (gallons) // - user revised volume (gallons) bool success = true; string error = ""; try { success = new ReportingDalc().SaveAnnualUsageReport(ActualUser, contigAcres, out error); } catch (Exception ex) { success = false; error = "Server error: " + ex.Message; } if (success) { // Update the state try { var pageState = JsonConvert.DeserializeObject<ReportingSummary>(state); // Find the index of the CA we care about foreach (var acres in pageState.years[contigAcres.year].contiguousAcres) { if (acres.number == contigAcres.number) { acres.isSubmitted = true; break; } } SaveState(JsonConvert.SerializeObject(pageState)); } catch (Exception ex) { // TODO: Not sure how we should handle an exception here. // The report was submitted but the "isSubmitted" flag was // not set to true on the state object...which will only ever // matter if the CA is deleted in this same year (after being submitted) // and in that case it wouldn't show up on the page due to the // way CAs are loaded during the page state load. // // For now I'll assume this case is so rare that it does not // warrant extra effort to handle this error in a special way. } } return Json(new JsonResponse(success, error)); }