public async Task <IActionResult> Upload( [FromBody] DailyStats stats ) { if (!ModelState.IsValid) { Logger.LogError("Failed to parse input data: {0}", ModelState); return(BadRequest(ModelState)); } Logger.LogInformation("Receiving daily stats from device {0} for {1}", stats.InstallationId, stats.Date.ToString("d", CultureInfo.InvariantCulture)); // Safety checks if (stats.Date < MinDate) { Logger.LogError("Daily statistics for unacceptable date {0}", stats.Date); return(UnprocessableEntity(ProblemDetailsFactory.CreateProblemDetails(HttpContext, title: "Unacceptable date (out of valid range)", type: "https://arianna.digit.srl/api/problems/invalid-date" ))); } if (stats.TotalMinutesTracked > MinutesADay) { Logger.LogError("Total minutes tracked ({0}) exceeds minutes in a day", stats.TotalMinutesTracked); return(UnprocessableEntity(ProblemDetailsFactory.CreateProblemDetails(HttpContext, title: "Total minutes tracked exceeds minutes in a day", type: "https://arianna.digit.srl/api/problems/invalid-data" ))); } if (stats.Date >= DateTime.UtcNow.Date) { Logger.LogError("Daily statistics for non-elapsed day {0}", stats.Date.Date); return(UnprocessableEntity(ProblemDetailsFactory.CreateProblemDetails(HttpContext, title: "Unacceptable date (future date)", type: "https://arianna.digit.srl/api/problems/invalid-date" ))); } GeoJsonPoint <GeoJson2DGeographicCoordinates> position; string geohash; try { geohash = stats.CentroidHash.Substring(0, 5); var decoded = Geohasher.Decode(geohash); position = new GeoJsonPoint <GeoJson2DGeographicCoordinates>(new GeoJson2DGeographicCoordinates(decoded.Item2, decoded.Item1)); Logger.LogInformation("GeoHash {0} decoded as {1:F5},{2:F5}", geohash, position.Coordinates.Latitude, position.Coordinates.Longitude); } catch (Exception ex) { return(UnprocessableEntity(ProblemDetailsFactory.CreateProblemDetails(HttpContext, title: "Cannot decode geohash", type: "https://arianna.digit.srl/api/problems/invalid-data", detail: ex.Message ))); } if (stats.LocationTracking == null) { Logger.LogError("Payload does not contain location tracking section"); return(UnprocessableEntity(ProblemDetailsFactory.CreateProblemDetails(HttpContext, title: "Payload does not contain location tracking section", type: "https://arianna.digit.srl/api/problems/invalid-data" ))); } if (stats.LocationTracking.MinutesAtHome < 0 || stats.LocationTracking.MinutesAtWork < 0 || stats.LocationTracking.MinutesAtSchool < 0 || stats.LocationTracking.MinutesAtOtherKnownLocations < 0 || stats.LocationTracking.MinutesElsewhere < 0) { Logger.LogError("Location tracking minutes cannot be negative"); return(UnprocessableEntity(ProblemDetailsFactory.CreateProblemDetails(HttpContext, title: "Negative location tracking value", type: "https://arianna.digit.srl/api/problems/invalid-data" ))); } if (stats.LocationTracking.MinutesAtHome > MinutesADay || stats.LocationTracking.MinutesAtWork > MinutesADay || stats.LocationTracking.MinutesAtSchool > MinutesADay || stats.LocationTracking.MinutesAtOtherKnownLocations > MinutesADay || stats.LocationTracking.MinutesElsewhere > MinutesADay) { Logger.LogError("One entry in the location tracking section exceeds minutes in a day"); return(UnprocessableEntity(ProblemDetailsFactory.CreateProblemDetails(HttpContext, title: "One entry in the location tracking section exceeds minutes in a day", type: "https://arianna.digit.srl/api/problems/invalid-data" ))); } // Check for duplicates var existingStats = await Mongo.GetDailyStats(stats.InstallationId, stats.Date); if (existingStats != null) { Logger.LogError("Duplicate statistics from device ID {0} for date {1}", stats.InstallationId, stats.Date.ToString("d", CultureInfo.InvariantCulture)); return(Conflict(ProblemDetailsFactory.CreateProblemDetails(HttpContext, title: "Duplicate statistics for date", type: "https://arianna.digit.srl/api/problems/duplicate" ))); } // Compute voucher amounts int stayAtHomeBonus = 0; int womCount = (int)Math.Ceiling(stats.TotalMinutesTracked / 60.0) + stayAtHomeBonus; Logger.LogInformation("Generating {0} WOM vouchers for {1} total minutes and {2} minutes at home ({3} stay at home bonus)", womCount, stats.TotalMinutesTracked, stats.LocationTracking.MinutesAtHome, stayAtHomeBonus); var voucherRequest = await Wom.Instrument.RequestVouchers(new VoucherCreatePayload.VoucherInfo[] { new VoucherCreatePayload.VoucherInfo { Aim = "P", Count = womCount, Latitude = position.Coordinates.Latitude, Longitude = position.Coordinates.Longitude, Timestamp = stats.Date.Date.AddHours(23.999) } }); // OK-dokey await Mongo.AddDailyStats(new DataModels.DailyStats { InstallationId = stats.InstallationId, Date = stats.Date.Date, TotalMinutesTracked = stats.TotalMinutesTracked, TotalWomVouchersEarned = womCount, Centroid = position, CentroidHash = geohash, LocationCount = stats.LocationCount, VehicleCount = stats.VehicleCount, EventCount = stats.EventCount, SampleCount = stats.SampleCount, DiscardedSampleCount = stats.DiscardedSampleCount, BoundingBoxDiagonal = stats.BoundingBoxDiagonal, LocationTracking = new DataModels.LocationTrackingStats { MinutesAtHome = stats.LocationTracking.MinutesAtHome, MinutesAtWork = stats.LocationTracking.MinutesAtWork, MinutesAtSchool = stats.LocationTracking.MinutesAtSchool, MinutesAtOtherKnownLocations = stats.LocationTracking.MinutesAtOtherKnownLocations, MinutesElsewhere = stats.LocationTracking.MinutesElsewhere } }); return(Ok(new UploadConfirmation { WomLink = voucherRequest.Link, WomPassword = voucherRequest.Password, WomCount = womCount })); }