sharee.bike-App/SharedBusinessLogic/Repository/CopriResponseModel.cs

592 lines
19 KiB
C#
Raw Normal View History

2024-04-09 12:53:23 +02:00
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();
}
}
}