mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-11-05 10:36:30 +01:00
591 lines
19 KiB
C#
591 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Serilog;
|
|
using ShareeBike.Model.Connector;
|
|
using ShareeBike.Repository.Response;
|
|
using ShareeBike.Repository.Response.Stations;
|
|
using ShareeBike.Repository.Response.Stations.Station;
|
|
|
|
namespace ShareeBike.Repository
|
|
{
|
|
[Flags]
|
|
public enum UpdateTarget
|
|
{
|
|
None = 0,
|
|
BikesAvailableResponse = 1,
|
|
BikesReservedOccupiedResponse = 2,
|
|
StationsAvailableResponse = 4,
|
|
All = BikesAvailableResponse | BikesReservedOccupiedResponse | StationsAvailableResponse
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manages station and bikes information.
|
|
/// Bike is kept in a response- oriented way which is redundant an has to be synchronized for this reason when updated.
|
|
/// </summary>
|
|
public class CopriResponseModel
|
|
{
|
|
public CopriResponseModel(
|
|
BikesAvailableResponse bikesAll,
|
|
BikesReservedOccupiedResponse bikesReservedOccupied,
|
|
StationsAvailableResponse stations)
|
|
{
|
|
BikesAll = bikesAll ?? new BikesAvailableResponse();
|
|
BikesReservedOccupied = bikesReservedOccupied ?? new BikesReservedOccupiedResponse();
|
|
Stations = stations ?? new StationsAvailableResponse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Holds
|
|
/// - part of/ or all available bikes and
|
|
/// - part of/ or all reserved bikes of logged in user if user is logged in and
|
|
/// - part of/ or all occupied bikes of logged in user if user is logged in.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Available bikes: Full information is only contained in <see cref="BikesAll"/>.
|
|
/// Available bikes: Count of available bikes per station is kept in <see cref="Stations"/>.
|
|
/// Reserved and occupied bikes: Are contained as well in <see cref="BikesReservedOccupied"/>.
|
|
/// Reserved and occupied bikes: Are contained as well in <see cref="Stations"/>.
|
|
/// </remarks>
|
|
public BikesAvailableResponse BikesAll { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Holds all reserved and occupied bikes of logged in user if not used without being logged in.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Reserved and occupied bikes: Are contained as well in <see cref="BikesAll"/>.
|
|
/// Reserved and occupied bikes: Are contained as well in <see cref="Stations"/>.
|
|
/// </remarks>
|
|
public BikesReservedOccupiedResponse BikesReservedOccupied { get; private set; }
|
|
|
|
/// <summary>
|
|
/// All bike stations, count of available bikes at each station
|
|
/// and all reserved and occupied bikes of logged in user if not used without being logged in.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Count of available bikes a each station: Information contained in <see cref="BikesAll"/>.
|
|
/// Reserved and occupied bikes: Are contained as well in <see cref="BikesReservedOccupied"/>.
|
|
/// </remarks>
|
|
public StationsAvailableResponse Stations { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Updates model from response.
|
|
/// </summary>
|
|
/// <param name="bikesAll">
|
|
/// Holds
|
|
/// - part of/ or all available bikes and
|
|
/// - part of/ or all reserved bikes of logged in user if user is logged in and
|
|
/// - part of/ or all occupied bikes of logged in user if user is logged in.
|
|
/// </param>
|
|
/// <returns>Parts of model which have to be updated.</returns>
|
|
public UpdateTarget Update(
|
|
BikesAvailableResponse bikesAllResponse,
|
|
string stationId = null,
|
|
string bikeId = null)
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(stationId))
|
|
{
|
|
return UpdateFromBikesAtStation(
|
|
bikesAllResponse,
|
|
stationId);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(bikeId))
|
|
{
|
|
return UpdateFromBikesAtStation(
|
|
bikesAllResponse,
|
|
bikesAllResponse.bikes.Values.FirstOrDefault(x => x.bike.ToUpper() == bikeId.ToUpper())?.station);
|
|
}
|
|
|
|
return UpdateTarget.None;
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Log.ForContext<CopriResponseModel>().Error(
|
|
"Updating response model form {@Type} for station id {@StationId} and bike id {BikeId} failed. {@Exception}",
|
|
nameof(BikesAvailableResponse),
|
|
stationId,
|
|
bikeId,
|
|
ex);
|
|
return UpdateTarget.None;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates cache from bike which changed rental state from available or reserved to booked.
|
|
/// </summary>
|
|
/// <param name="response"></param>
|
|
public UpdateTarget Update(BikeInfoReservedOrBooked response)
|
|
{
|
|
try
|
|
{
|
|
var updateTarget = UpdateTarget.None;
|
|
if (response == null || string.IsNullOrEmpty(response.bike))
|
|
{
|
|
// Can nor update station or remove bike
|
|
return updateTarget;
|
|
}
|
|
|
|
// Update station if required i.e. decrease count of available bikes if required.
|
|
updateTarget = UpdateAvailableBikesCount(
|
|
BikesAll.bikes.Values.FirstOrDefault(b => b.bike == response?.bike)?.station,
|
|
i => i - 1, // Decrease count of available bikes.
|
|
updateTarget);
|
|
|
|
// A booked bike is no more available.
|
|
BikesAll.bikes.Remove(response.bike);
|
|
|
|
// Update Stations with latest reserved/ rented bike.
|
|
Stations.bikes_occupied.Remove(response.bike);
|
|
Stations.bikes_occupied.Add(response.bike, response);
|
|
|
|
// Update BikesAll with latest reserved/ rented bike.
|
|
BikesAll.bikes_occupied.Remove(response.bike);
|
|
BikesAll.bikes_occupied.Add(response.bike, response);
|
|
|
|
// Update BikesReservedOccupied with latest reserved/ rented bike.
|
|
BikesReservedOccupied.bikes_occupied.Remove(response.bike);
|
|
BikesReservedOccupied.bikes_occupied.Add(response.bike, response);
|
|
|
|
return updateTarget | UpdateTarget.BikesAvailableResponse | UpdateTarget.BikesReservedOccupiedResponse;
|
|
} catch (System.Exception ex)
|
|
{
|
|
Log.ForContext<CopriResponseModel>().Error(
|
|
"Updating response model form {@Type} failed. {@Exception}",
|
|
nameof(BikeInfoReservedOrBooked),
|
|
ex);
|
|
|
|
return UpdateTarget.None;
|
|
}
|
|
}
|
|
|
|
/// <summary> Updates cache from bike which changed rental state (reservation/ booking canceled). </summary>
|
|
public UpdateTarget Update(BookingActionResponse response)
|
|
{
|
|
try
|
|
{
|
|
if (response == null || string.IsNullOrEmpty(response.bike))
|
|
{
|
|
// Can nor update station or remove bike
|
|
return UpdateTarget.None;
|
|
}
|
|
|
|
// Remove available bike from Stations.
|
|
Stations.bikes_occupied.Remove(response.bike);
|
|
|
|
// Remove available bike from BikesAll.
|
|
BikesAll.bikes_occupied.Remove(response.bike);
|
|
|
|
// Remove available bike from BikesReservedOccupied.
|
|
BikesReservedOccupied.bikes_occupied.Remove(response.bike);
|
|
|
|
return UpdateTarget.StationsAvailableResponse | UpdateTarget.BikesAvailableResponse | UpdateTarget.BikesReservedOccupiedResponse;
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Log.ForContext<CopriResponseModel>().Error(
|
|
"Updating response model form {@Type} failed. {@Exception}",
|
|
nameof(BookingActionResponse),
|
|
ex);
|
|
|
|
return UpdateTarget.None;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates model from response.
|
|
/// </summary>
|
|
/// <param name="bikesAll">
|
|
/// Holds
|
|
/// - all available bikes located at given station and
|
|
/// - all reserved bikes of logged in user if user is logged in located at given and
|
|
/// - all occupied bikes of logged in user if user is logged in located at given .
|
|
/// </param>
|
|
/// <returns>Parts of model which have to be updated.</returns>
|
|
private UpdateTarget UpdateFromBikesAtStation(
|
|
BikesAvailableResponse bikesAllResponse,
|
|
string stationId)
|
|
{
|
|
/// <summary>
|
|
/// Synchronizes a dictionary with another, i.e.
|
|
/// - adds values which do not yet exist
|
|
/// - updates existing values with new ones and
|
|
/// - removes bikes which do no more exist
|
|
/// </summary>
|
|
/// <param name="source">Source dictionary to be read from.</param>
|
|
/// <param name="target">Target directory to be written to.</param>
|
|
void Synchonize<T>(
|
|
ComparableDictionary<T> sourceDict,
|
|
ComparableDictionary<T> targetDict,
|
|
Action updateRequiredAction) where T : BikeInfoBase, IEquatable<T>
|
|
{
|
|
if (targetDict == null)
|
|
{
|
|
// Nothing to do if target dictionary is empty.
|
|
return;
|
|
}
|
|
|
|
// Add/ update bikes
|
|
foreach (var sourceBike in sourceDict ?? new ComparableDictionary<T>())
|
|
{
|
|
// Check if bikes already exists.
|
|
var targetBike = targetDict.Values.FirstOrDefault(x => x.bike == sourceBike.Value.bike);
|
|
if (targetBike != null)
|
|
{
|
|
// Check if bikes equals
|
|
if (sourceBike.Value.Equals(targetBike))
|
|
{
|
|
// Bikes equal, process next.
|
|
continue;
|
|
}
|
|
|
|
// Remove all bikes from target which part of source to be added later.
|
|
targetDict.Remove(sourceBike.Key);
|
|
}
|
|
|
|
targetDict.Add(sourceBike.Key, sourceBike.Value);
|
|
updateRequiredAction?.Invoke();
|
|
}
|
|
|
|
// Remove bikes from target dictionary which are no more at station
|
|
var bikesToRemove = targetDict
|
|
.Where(x => x.Value.station == stationId)
|
|
.Where(x => sourceDict?.Values?.FirstOrDefault(y => y.station == stationId) == null);
|
|
|
|
foreach (var bike in bikesToRemove)
|
|
{
|
|
targetDict.Remove(bike.Key);
|
|
updateRequiredAction?.Invoke();
|
|
}
|
|
}
|
|
|
|
if (BikesAll == bikesAllResponse
|
|
|| bikesAllResponse == null)
|
|
{
|
|
return UpdateTarget.None;
|
|
}
|
|
|
|
var updateTarget = UpdateTarget.None;
|
|
|
|
// Update bikes all by updating existing bikes and adding new ones.
|
|
Synchonize(
|
|
bikesAllResponse.bikes,
|
|
BikesAll.bikes,
|
|
() => { updateTarget |= UpdateTarget.BikesAvailableResponse; });
|
|
|
|
Synchonize(
|
|
bikesAllResponse.bikes_occupied,
|
|
BikesAll.bikes_occupied,
|
|
() => { updateTarget |= UpdateTarget.BikesAvailableResponse; });
|
|
|
|
// Update stations property count of available bikes for selected station.
|
|
UpdateAvailableBikesCount(
|
|
bikesAllResponse.bikes.Count,
|
|
Stations.stations.Values.FirstOrDefault(x => x.station == stationId),
|
|
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
|
|
|
|
// Add/ update stations bikes reserved and stations bikes occupied.
|
|
Synchonize(
|
|
bikesAllResponse.bikes_occupied,
|
|
Stations.bikes_occupied,
|
|
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
|
|
|
|
// Add/ update reserved and occupied bikes.
|
|
Synchonize(
|
|
bikesAllResponse.bikes_occupied,
|
|
BikesReservedOccupied.bikes_occupied,
|
|
() => updateTarget |= UpdateTarget.BikesReservedOccupiedResponse);
|
|
|
|
// Remove available bikes from bikes available response if there are: A bike reserved or occupied can not be available at the same time.
|
|
Remove(
|
|
bikesAllResponse.bikes_occupied.Values.Select(x => x.bike).ToList(),
|
|
BikesAll.bikes,
|
|
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
|
|
|
|
// Remove reserved and occupied bikes from bikes available response if there are: A bike available can not be reserved or occupied at the same time.
|
|
Remove(
|
|
bikesAllResponse.bikes.Values.Select(x => x.bike).ToList(),
|
|
BikesAll.bikes_occupied,
|
|
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
|
|
|
|
// Remove reserved and occupied bikes from stations response if there are: A bike available can not be reserved or occupied at the same time.
|
|
Remove(
|
|
bikesAllResponse.bikes.Values.Select(x => x.bike).ToList(),
|
|
Stations.bikes_occupied,
|
|
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
|
|
|
|
return updateTarget;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates model from reserved and occupied bikes.
|
|
/// </summary>
|
|
/// <param name="bikesReservedOccupiedResponse">Bikes to update with.</param>
|
|
/// <returns>Parts of model which have to be updated.</returns>
|
|
public UpdateTarget Update(BikesReservedOccupiedResponse bikesReservedOccupiedResponse)
|
|
{
|
|
try
|
|
{
|
|
if (bikesReservedOccupiedResponse == BikesReservedOccupied
|
|
|| bikesReservedOccupiedResponse == null)
|
|
{
|
|
return UpdateTarget.None;
|
|
}
|
|
|
|
var updateTarget = UpdateTarget.BikesReservedOccupiedResponse;
|
|
BikesReservedOccupied = bikesReservedOccupiedResponse; ;
|
|
|
|
// Update bikes reserved or occupied from bikes reserved or occupied response.
|
|
Write(
|
|
bikesReservedOccupiedResponse.bikes_occupied,
|
|
BikesAll.bikes_occupied,
|
|
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
|
|
|
|
// Update stations from bikes reserved or occupied response.
|
|
Write(
|
|
bikesReservedOccupiedResponse.bikes_occupied,
|
|
Stations.bikes_occupied,
|
|
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
|
|
|
|
// Remove available bikes from bikes available response: A bike reserved/ occupied can not be available at the same time.
|
|
Remove(
|
|
bikesReservedOccupiedResponse.bikes_occupied.Values.Select(x => x.bike).ToList(),
|
|
BikesAll.bikes,
|
|
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
|
|
|
|
// Due to possible change of bikes available from bikes available response (step above), count of bikes from stations might need to be updated.
|
|
UpdateAvailableBikesCount(
|
|
BikesAll.bikes,
|
|
Stations.stations,
|
|
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
|
|
|
|
return updateTarget;
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Log.ForContext<CopriResponseModel>().Error(
|
|
"Updating response model form {@Type} failed. {@Exception}",
|
|
nameof(BikesReservedOccupiedResponse),
|
|
ex);
|
|
|
|
return UpdateTarget.None;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates model from stations response.
|
|
/// </summary>
|
|
/// <param name="stationsResponse">Response to update from.</param>
|
|
/// <returns></returns>
|
|
public UpdateTarget Update(StationsAvailableResponse stationsResponse)
|
|
{
|
|
try
|
|
{
|
|
// Remove bike from dictionary if located at a list of stations.
|
|
// This can only be done randomly because only the count of bikes without bike ids is known (task #901).
|
|
void LimitAvailableBikesCountByStation(
|
|
IList<StationInfo> stations,
|
|
ComparableBikeDictionary<BikeInfoAvailable> bikes,
|
|
Action updateRequiredAction)
|
|
{
|
|
// Group available bikes by station.
|
|
var groupedBikes = bikes.GroupBy(x => x.Value.station).ToList();
|
|
|
|
foreach (var station in stations)
|
|
{
|
|
var bikesAtStation = groupedBikes.FirstOrDefault(x => x.Key == station.station);
|
|
|
|
var toReviseCount = bikesAtStation?.ToList()?.Count ?? 0;
|
|
if (string.IsNullOrEmpty(bikesAtStation?.Key)
|
|
|| !station.TryGetBikesAvailableCount(out var trueCount)
|
|
|| toReviseCount <= trueCount)
|
|
{
|
|
// Either
|
|
// - there is no bikes at current station or
|
|
// - invalid format detected
|
|
// - count of bikes is ok
|
|
continue;
|
|
}
|
|
|
|
var surplus = toReviseCount - trueCount;
|
|
|
|
// There are move bikes available at current station in bikes available response than the station object reports.
|
|
// Randomly remove surplus bikes.
|
|
var keys = bikesAtStation.Select(x => x.Key).ToList();
|
|
for (var i = 0; i < surplus; i++)
|
|
{
|
|
bikes.Remove(keys[i]);
|
|
}
|
|
|
|
updateRequiredAction?.Invoke();
|
|
}
|
|
}
|
|
|
|
if (Stations == stationsResponse
|
|
|| stationsResponse == null)
|
|
{
|
|
return UpdateTarget.None;
|
|
}
|
|
|
|
var updateTarget = UpdateTarget.StationsAvailableResponse;
|
|
Stations = stationsResponse;
|
|
|
|
// Ensure that bikes which have become occupied are no more in list of available bikes
|
|
foreach (var bike in Stations.bikes_occupied.Values)
|
|
{
|
|
if (!BikesAll.bikes.ContainsKey(bike.bike)) continue;
|
|
|
|
BikesAll.bikes.Remove(bike.bike);
|
|
}
|
|
|
|
// Ensure that there are not more bikes assigned to one station in bikes available object than there are reported in station response.
|
|
// An example for this case is that a bike is brought to garage.
|
|
LimitAvailableBikesCountByStation(
|
|
Stations.stations.Values.ToList(),
|
|
BikesAll.bikes,
|
|
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
|
|
|
|
// Update bikes reserved and bikes occupied from stations response.
|
|
Write(
|
|
Stations.bikes_occupied,
|
|
BikesAll.bikes_occupied,
|
|
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
|
|
|
|
// Update bikes reserved or occupied from stations response.
|
|
Write(
|
|
Stations.bikes_occupied,
|
|
BikesReservedOccupied.bikes_occupied,
|
|
() => updateTarget |= UpdateTarget.BikesReservedOccupiedResponse);
|
|
|
|
return updateTarget;
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Log.ForContext<CopriResponseModel>().Error(
|
|
"Updating response model form {@Type} failed. {@Exception}",
|
|
nameof(StationsAvailableResponse),
|
|
ex);
|
|
|
|
return UpdateTarget.None;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes bikes from a dictionary.
|
|
/// </summary>
|
|
/// <typeparam name="T">Type of dictionary values to remove.</typeparam>
|
|
/// <param name="bikesToRemove">Bikes to remove.</param>
|
|
/// <param name="target">Dictionary to removed bikes from.</param>
|
|
/// <param name="updateRequiredAction">Action to be invoked if any bike gets removed from dictionary.</param>
|
|
private void Remove<T>(
|
|
IList<string> bikesToRemove,
|
|
ComparableDictionary<T> target,
|
|
Action updateRequiredAction) where T : BikeInfoBase
|
|
{
|
|
if (target == null)
|
|
{
|
|
// Nothing to do.
|
|
return;
|
|
}
|
|
|
|
foreach (var bike in bikesToRemove)
|
|
{
|
|
if (!target.Remove(bike))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
updateRequiredAction?.Invoke();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a dictionary to another if required.
|
|
/// </summary>
|
|
/// <param name="source">Source dictionary to be read from.</param>
|
|
/// <param name="target">Target directory to be written to.</param>
|
|
void Write(
|
|
ComparableDictionary<BikeInfoReservedOrBooked> source,
|
|
ComparableDictionary<BikeInfoReservedOrBooked> target,
|
|
Action updateRequiredAction)
|
|
{
|
|
if (target == null || source == target)
|
|
{
|
|
return;
|
|
}
|
|
|
|
target.Clear();
|
|
|
|
foreach (var bike in source ?? new ComparableBikeDictionary<BikeInfoReservedOrBooked>())
|
|
{
|
|
target.Add(bike.Key, bike.Value);
|
|
}
|
|
|
|
updateRequiredAction?.Invoke();
|
|
}
|
|
|
|
private void UpdateAvailableBikesCount(
|
|
ComparableBikeDictionary<BikeInfoAvailable> bikesAvailable,
|
|
ComparableDictionary<StationInfo> stations,
|
|
Action updateRequiredAction)
|
|
{
|
|
foreach (var station in stations.Values)
|
|
{
|
|
UpdateAvailableBikesCount(
|
|
bikesAvailable.Values.Where(x => x.station == station.station).Count(),
|
|
station,
|
|
updateRequiredAction);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the count of available bikes of a given station.
|
|
/// </summary>
|
|
/// <param name="id">Id of the station to update count for.</param>
|
|
/// <param name="countDelegate">Delegate to return the new count.</param>
|
|
/// <param name="updateRequiredAction">Update action required</param>
|
|
private UpdateTarget UpdateAvailableBikesCount(
|
|
string id,
|
|
Func<int, int> countDelegate,
|
|
UpdateTarget currentUpdateTarget)
|
|
{
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
// Bike was not available before.
|
|
return currentUpdateTarget;
|
|
}
|
|
|
|
var station = Stations.stations.Values.FirstOrDefault(s => s.station == id);
|
|
if (station == null)
|
|
{
|
|
// There is no matching station.
|
|
return currentUpdateTarget;
|
|
}
|
|
|
|
// Decrement count of stations.
|
|
station.SetBikesAvailableCount(countDelegate(station.GetBikesAvailableCount() ?? 1));
|
|
return currentUpdateTarget | UpdateTarget.StationsAvailableResponse;
|
|
}
|
|
private void UpdateAvailableBikesCount(
|
|
int bikesAvailableCount,
|
|
StationInfo station,
|
|
Action updateRequiredAction)
|
|
{
|
|
if (station == null
|
|
|| !station.TryGetBikesAvailableCount(out var targetBikesCount)
|
|
|| targetBikesCount == bikesAvailableCount)
|
|
{
|
|
// Either
|
|
// - there is no matching station or
|
|
// - station count if not numeric or
|
|
// - count matches
|
|
return;
|
|
}
|
|
|
|
station.SetBikesAvailableCount(bikesAvailableCount);
|
|
updateRequiredAction?.Invoke();
|
|
}
|
|
}
|
|
}
|