using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using gehGassi.Core.Interfaces; using gehGassi.Domain.Advertisements; using gehGassi.Domain.Common; using gehGassi.Domain.News; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using NetTopologySuite.Geometries; using NetTopologySuite; namespace gehGassi.Core.Services { /// /// Service für die Verwaltung von Listungen /// public class ListingService : ServiceBase, IListingService { private readonly IOptions _listingOptions; private readonly IRepository _namesRepository; /// /// Erstellt eine Instanz /// /// Instanz einer IUnitOfWork /// Instanz von ListingOptions public ListingService(IUnitOfWork unitOfWork, IOptions listingOptions) : base(unitOfWork) { _listingOptions = listingOptions; _namesRepository = unitOfWork.GetRepository(); } /// /// Gibt eine gefilterte Liste von Listings zurück /// /// Filterbegriff der in bestimmten Feldern gesucht wird /// List betroffener Listings public IQueryable Filter(string filter) { return Repository.Query(c => c.NameLocalized.Contains(filter)); } /// /// Erstellen einer Listung /// /// Listung public Listing Create() { var item = new Listing() { Id = Guid.NewGuid().ToString("N"), Created = DateTimeOffset.UtcNow }; return item; } /// /// Gibt eine gefilterte Liste von Listings zurück. /// /// Filterbegriff der in bestimmten Feldern gesucht wird /// List betroffener Listings public async Task> SearchAsync(string filter) { return (await Repository.FindAsync(c => c.NameLocalized.Contains(filter))).ToList(); } /// /// Gibt eine gefilterte Liste von Listings mit Namen zurück in einer gewählten Sprache /// /// Filterbegriff der in bestimmten Feldern gesucht wird /// Gewünschte Sprache /// Fallback Sprache /// Sollen gelöschte inkludiert werden? /// Sollen Entitäten im Draft-Status (Cart) ausgeblendet werden? /// Liste betroffener Listings public IQueryable FilterWithNames(string filter, string language, string fallback, bool includeDeleted, bool excludeDraft) { language = language.ToUpperInvariant(); fallback = fallback.ToUpperInvariant(); var baseQuery = _namesRepository.QueryStoredProcedure("SELECT * FROM tv_ListingsWithNames({0},{1})", c => c.Name.Contains(filter) || c.CustomerName.Contains(filter), language, fallback); if (!includeDeleted) baseQuery = baseQuery.Where(c => c.Deleted == false); if (excludeDraft) baseQuery = baseQuery.Where(c => c.Status != ListingStatus.Draft); return baseQuery; } /// /// Gibt eine gefilterte Liste von Listings mit Namen für Kunden zurück in einer gewählten Sprache /// /// Id des Kunden /// Filterbegriff der in bestimmten Feldern gesucht wird /// Gewünschte Sprache /// Fallback Sprache /// Sollen gelöschte inkludiert werden? /// Sollen Entitäten im Draft-Status (Cart) ausgeblendet werden? /// Liste betroffener Listings public IQueryable FilterWithNamesCustomer(long customerId, string filter, string language, string fallback, bool includeDeleted, bool excludeDraft) { language = language.ToUpperInvariant(); fallback = fallback.ToUpperInvariant(); var baseQuery = _namesRepository.QueryStoredProcedure("SELECT * FROM tv_ListingsWithNames({0},{1})", c => c.CustomerId == customerId && (c.Name.Contains(filter) || c.CustomerName.Contains(filter)), language, fallback); if (!includeDeleted) baseQuery = baseQuery.Where(c => c.Deleted == false); if (excludeDraft) baseQuery = baseQuery.Where(c => c.Status != ListingStatus.Draft); return baseQuery; } /// /// Löschen von Listungen basierend auf einer Branche /// /// Id der Branche /// Wenn true, dann wirklich löschen, auf gelöscht setzen sonst /// Task public async Task DeleteByBranchAsync(string branchId, bool forceDelete) { var items = await Repository.FindAsync(c => c.BranchId == branchId).ConfigureAwait(false); foreach (var listing in items) { if(!forceDelete) listing.Deleted = true; else Repository.Remove(listing); } } /// /// Prüfen der Verfügbarkeit einer Listung für eine Buchung /// /// Id des Kunden /// Id der Branche /// Listungs-Typ /// Startdatum /// Enddatum /// ISO2 des Landes /// Id die ignoriert werden soll /// ListingValidationResult public async Task IsAvailableAsync(long customerId, string branchId, ListingType listingType, DateTimeOffset startDate, DateTimeOffset endDate, string countryIso, string ignoreId) { var result = new ListingValidationResult() { Valid = true, Booked = 0, Reserved = 0, ErrorCode = ListingValidationError.None }; //anpassung --> alle Listungstypen berücksichtigen. Es darf nur eine Listung je Kunde je Land und Branche geben. Der Listungstyp ist dabei egal. //var customerCount = await Repository.CountAsync(c => c.Id != ignoreId && c.Deleted == false && c.BranchId == branchId && c.ListingType == listingType && c.CustomerId == customerId && c.Address.CountryCode == countryIso && (c.Status == ListingStatus.ToPay || c.Status == ListingStatus.Booked || c.Status == ListingStatus.Running) && ((startDate >= c.StartDate && startDate <= c.EndDate) || (endDate >= c.StartDate && endDate <= c.EndDate))); var customerCount = await Repository.CountAsync(c => c.Id != ignoreId && c.Deleted == false && c.BranchId == branchId && c.CustomerId == customerId && c.Address.CountryCode == countryIso && (c.Status == ListingStatus.ToPay || c.Status == ListingStatus.Booked || c.Status == ListingStatus.Running) && ((startDate >= c.StartDate && startDate <= c.EndDate) || (endDate >= c.StartDate && endDate <= c.EndDate))); var bookedCount = await Repository.CountAsync(c => c.Id != ignoreId && c.Deleted == false && c.BranchId == branchId && c.ListingType == listingType && c.CustomerId != customerId && c.Address.CountryCode == countryIso && (c.Status == ListingStatus.ToPay || c.Status == ListingStatus.Booked || c.Status == ListingStatus.Running) && ((startDate >= c.StartDate && startDate <= c.EndDate) || (endDate >= c.StartDate && endDate <= c.EndDate))); var reservedCount = await Repository.CountAsync(c => c.Id != ignoreId && c.Deleted == false && c.BranchId == branchId && c.ListingType == listingType && c.CustomerId != customerId && c.Address.CountryCode == countryIso && c.ReservedUntil != null); result.Booked = bookedCount; result.Reserved = reservedCount; var total = bookedCount + reservedCount; if (customerCount > 0) { result.ErrorCode = ListingValidationError.AlreadyBooked; result.Valid = false; } else { switch (listingType) { case ListingType.Base: if (total >= _listingOptions.Value.Base) { result.ErrorCode = ListingValidationError.NoSlotsAvailable; result.Valid = false; } break; case ListingType.Silver: if (total >= _listingOptions.Value.Silver) { result.ErrorCode = ListingValidationError.NoSlotsAvailable; result.Valid = false; } break; case ListingType.Gold: if (total >= _listingOptions.Value.Gold) { result.ErrorCode = ListingValidationError.NoSlotsAvailable; result.Valid = false; } break; case ListingType.Platin: if (total >= _listingOptions.Value.Platin) { result.ErrorCode = ListingValidationError.NoSlotsAvailable; result.Valid = false; } break; default: throw new ArgumentOutOfRangeException(nameof(listingType), listingType, null); } } return result; } /// /// Gibt die Anzahl von Listungen zurück /// /// Sollen gelöschte inkludiert werden? /// Sollen Entitäten im Draft-Status (Cart) ausgeblendet werden? /// Anzahl Listungen public async Task CountAsync(bool includeDeleted, bool excludeDraft) { var query = Repository.Query(c => c.Id != ""); if (!includeDeleted) query = query.Where(c => c.Deleted == false); if(excludeDraft) query = query.Where(c => c.Status != ListingStatus.Draft); return await query.CountAsync().ConfigureAwait(false); } /// /// Gibt die Anzahl von Listungen in einem Status zurück /// /// Gesuchter Status /// Sollen gelöschte inkludiert werden? /// Anzahl Listungen public async Task CountAsync(ListingStatus status, bool includeDeleted) { var query = Repository.Query(c => c.Status == status); if (!includeDeleted) query = query.Where(c => c.Deleted == false); return await query.CountAsync().ConfigureAwait(false); } /// /// Prüft ob sich eine Listung bereits im Warenkorb befindet. /// Listungen mit bestimmten Parametern dürfen nur einmal vorkommen /// /// Id des Warenkorb /// Id des Kunden /// Id der Branche /// Listungs-Typ /// Startdatum /// Enddatum /// ISO2 des Landes /// Id die ignoriert werden soll /// True wenn die Listung gefunden wurde, false sonst public async Task CheckListingInCartAsync(long cartId, long customerId, string branchId, ListingType listingType, DateTimeOffset startDate, DateTimeOffset endDate, string countryIso, string ignoreId) { //anpassung --> alle Listungstypen berücksichtigen. Es darf nur eine Listung je Kunde je Land und Branche geben. Der Listungstyp ist dabei egal. //var count = await Repository.CountAsync(c => c.Id != ignoreId && c.Deleted == false && c.CartId == cartId && c.BranchId == branchId && c.ListingType == listingType && c.CustomerId == customerId && c.Address.CountryCode == countryIso && c.Status == ListingStatus.Draft && ((startDate >= c.StartDate && startDate <= c.EndDate) || (endDate >= c.StartDate && endDate <= c.EndDate))); var count = await Repository.CountAsync(c => c.Id != ignoreId && c.Deleted == false && c.CartId == cartId && c.BranchId == branchId && c.CustomerId == customerId && c.Address.CountryCode == countryIso && c.Status == ListingStatus.Draft && ((startDate >= c.StartDate && startDate <= c.EndDate) || (endDate >= c.StartDate && endDate <= c.EndDate))); return count != 0; } /// /// Gibt eine Listung für einen Artikel im Warenkorb zurück /// /// Id des Artikels im Warenkorb /// Listing oder null, wenn nicht gefunden public async Task GetByCartItemAsync(long cartItemId) { return await Repository.FirstOrDefaultAsync(c => c.CartItemId == cartItemId && c.Status == ListingStatus.Draft); } /// /// Gibt eine Listung für einen Artikel in einer Bestellung zurück /// /// Id des Artikels in der Bestellung /// Listing oder null, wenn nicht gefunden public async Task GetByOrderItemAsync(long orderItemId) { return await Repository.FirstOrDefaultAsync(c => c.OrderItemId == orderItemId); } /// /// Setzen des Status von Listungen nach Bestellung /// /// Id der Bestellung /// Zu setzender Status /// Datum das für die Reservierung gesetzt werden soll /// Anzahl betroffener Listungen public async Task SetStatusByOrderAsync(long orderId, ListingStatus status, DateTimeOffset? reservedDate) { var listings = await Repository.FindAsync(c => c.OrderId == orderId); foreach (var listing in listings) { listing.Status = status; listing.ReservedUntil = reservedDate; } return listings.Count(); } /// /// Setzen des Bezahlstatus von Listingen nach Bestellung /// /// Id der Bestellung /// Zu setzender Status /// Anzahl betroffener Listungen public async Task SetPaymentStatusByOrderAsync(long orderId, PaymentStatus status) { var listings = await Repository.FindAsync(c => c.OrderId == orderId); foreach (var listing in listings) { listing.PaymentStatus = status; } return listings.Count(); } /// /// Zurücksetzen von Reservierungen wenn eine Zeit erreicht ist /// /// Obere Grenze der Reservierung /// Anzahl betroffener Listungen public async Task ResetReservedAsync(DateTimeOffset reservedDate) { var listings = await Repository.FindAsync(c => c.Status == ListingStatus.Reserverd && ((c.ReservedUntil.HasValue && c.ReservedUntil < reservedDate) || c.ReservedUntil == null)); foreach (var listing in listings) { listing.ReservedUntil = null; listing.Status = ListingStatus.Draft; } return listings.Count(); } /// /// Gibt eine Liste von Listungen zurück für die Anzeige der Partner in der Nähe /// /// Land /// Bundesland /// Optional: Breitengrad /// Optional: Längengrad /// Gewünschte Sprache /// Fallback Sprache /// Datensätze auslassen /// Datensätze nehmen /// Liste passender Listungen und gesamt verfügbar public async Task<(int total, List list)> GetNearForAppAsync(string country, string state, double lat, double lng, string language, string fallback, int skip, int take) { language = language.ToUpperInvariant(); fallback = fallback.ToUpperInvariant(); if (!string.IsNullOrWhiteSpace(country)) country = country.ToUpperInvariant(); if (!string.IsNullOrWhiteSpace(state)) state = state.ToUpperInvariant(); var geometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); var location = geometryFactory.CreatePoint(new Coordinate(lng, lat)); var baseQuery = _namesRepository.QueryStoredProcedure("SELECT * FROM tv_ListingsWithNames({0},{1})", c => c.Deleted == false && c.Status == ListingStatus.Running && c.BranchOnlineStatus == OnlineStatus.Online && (c.GeoMode == GeoMode.None || (c.GeoMode == GeoMode.Country && c.Address_CountryCode == country) || (c.GeoMode == GeoMode.CountryAndState && c.Address_CountryCode == country && c.Address_State == state) || (c.GeoMode == GeoMode.Location && c.Location.IsWithinDistance(location, c.Radius * 1000))), language, fallback); var total = await baseQuery.CountAsync().ConfigureAwait(false); //TODO: noch überlegen wie wir eine Lokalisierung machen können baseQuery = baseQuery.OrderBy(c => c.ListingLocation.Distance(location)); baseQuery = baseQuery.Skip(skip).Take(take); var list = await baseQuery.ToListAsync().ConfigureAwait(false); return (total, list); } /// /// Gibt eine Liste von Listungen zurück für eine Branche zurück. /// Die Sortierung erfolgt nach Listungstyp und dann nach Distanz /// /// Id der Branche /// Optional: Filter für Name und/oder Ort /// Optional: Listings-Land /// Land /// Bundesland /// Optional: Breitengrad /// Optional: Längengrad /// Gewünschte Sprache /// Fallback Sprache /// Datensätze auslassen /// Datensätze nehmen /// Liste passender Listungen public async Task<(int total, List list)> GetByBranchForAppAsync(string branchId, string filter, string listingCountry, string country, string state, double lat, double lng, string language, string fallback, int skip, int take) { language = language.ToUpperInvariant(); fallback = fallback.ToUpperInvariant(); if (!string.IsNullOrWhiteSpace(country)) country = country.ToUpperInvariant(); if (!string.IsNullOrWhiteSpace(state)) state = state.ToUpperInvariant(); if (!string.IsNullOrWhiteSpace(listingCountry)) listingCountry = listingCountry.ToUpperInvariant(); var geometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); var location = geometryFactory.CreatePoint(new Coordinate(lng, lat)); var baseQuery = _namesRepository.QueryStoredProcedure("SELECT * FROM tv_ListingsWithNames({0},{1})", c => c.BranchId == branchId && c.Deleted == false && c.Status == ListingStatus.Running && c.BranchOnlineStatus == OnlineStatus.Online && (c.GeoMode == GeoMode.None || (c.GeoMode == GeoMode.Country && c.Address_CountryCode == country) || (c.GeoMode == GeoMode.CountryAndState && c.Address_CountryCode == country && c.Address_State == state) || (c.GeoMode == GeoMode.Location && c.Location.IsWithinDistance(location, c.Radius * 1000))), language, fallback); if (!string.IsNullOrWhiteSpace(filter)) { baseQuery = baseQuery.Where(c => c.Name.Contains(filter) || c.ListingAddress_City.Contains(filter)); } if (!string.IsNullOrWhiteSpace(listingCountry)) { baseQuery = baseQuery.Where(c => c.ListingAddress_CountryCode == listingCountry); } var total = await baseQuery.CountAsync().ConfigureAwait(false); var list = await baseQuery.OrderByDescending(c => c.ListingType).ThenBy(c => c.ListingLocation.Distance(location)).Skip(skip).Take(take).ToListAsync().ConfigureAwait(false); return (total, list); } /// /// Gibt eine Listung in einer gewünschten Sprache zurück /// /// Id der Listung /// Gewünschte Sprache /// Fallback Sprache /// Gefundene Werbung, null sonst public async Task GetRunningWithNamesAsync(string id, string language, string fallback) { language = language.ToUpperInvariant(); fallback = fallback.ToUpperInvariant(); var baseQuery = _namesRepository.QueryStoredProcedure("SELECT * FROM tv_ListingsWithNames({0},{1})", c => c.Id == id && c.Deleted == false && c.Status == ListingStatus.Running && c.EndDate > DateTimeOffset.UtcNow, language, fallback); var result = await baseQuery.FirstOrDefaultAsync().ConfigureAwait(false); return result; } /// /// Starten von gebuchten Listungen wenn diese das Start-Datum erreicht haben /// /// Anzahl gestartete Listungen public async Task StartBookedAsnyc() { var listings = await Repository.FindAsync(c => c.Status == ListingStatus.Booked && c.Deleted == false && c.StartDate <= DateTimeOffset.UtcNow && c.EndDate.Value >= DateTimeOffset.UtcNow); foreach (var listing in listings) { listing.Status = ListingStatus.Running; } return listings.Count(); } /// /// Stoppen von abgelaufenen Listungen wenn diese das Enddatum erreicht haben /// /// Anzahl gestoppte Listungen public async Task StopRunningAsync() { var listings = await Repository.FindAsync(c => c.Status == ListingStatus.Running && c.Deleted == false && c.EndDate.Value <= DateTimeOffset.UtcNow); foreach (var listing in listings) { listing.Status = ListingStatus.Ended; } return listings.Count(); } } }