using gehGassi.Core.Interfaces; using gehGassi.Domain.Vouchers; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using gehGassi.Domain.Common; using Microsoft.EntityFrameworkCore; using System.Security.Cryptography; using NetTopologySuite.Algorithm; using static Org.BouncyCastle.Asn1.Cmp.Challenge; namespace gehGassi.Core.Services { /// /// Service der die Verwaltung von Gutscheinkampagnen übernimmt /// public class VoucherCampaignService : ServiceBase, IVoucherCampaignService { private readonly IRepository _namesRepository; private readonly IRepository _vouchersRepository; private readonly IRepository _voucherUsagesRepository; /// /// Erstellt eine Instanz /// /// Instanz eines IUnitOfWork public VoucherCampaignService(IUnitOfWork unitOfWork) : base(unitOfWork) { _namesRepository = unitOfWork.GetRepository(); _vouchersRepository = unitOfWork.GetRepository(); _voucherUsagesRepository = unitOfWork.GetRepository(); } /// /// Gibt eine gefilterte Liste von Gutschein-Kampagnen zurück /// /// Filterbegriff der in bestimmten Feldern gesucht wird /// List betroffener Gutschein-Kampagnen public IQueryable Filter(string filter) { return Repository.Query(c => c.Title.Contains(filter) || c.NameLocalized.Contains(filter)); } /// /// Erstellen einer Gutschein-Kampagne /// /// Gutschein-Kampagne public VoucherCampaign Create() { var item = new VoucherCampaign() { Id = Guid.NewGuid().ToString("N"), Created = DateTimeOffset.UtcNow }; return item; } /// /// Gibt eine gefilterte Liste von Gutschein-Kampagnen zurück. /// /// Filterbegriff der in bestimmten Feldern gesucht wird /// List betroffener Gutschein-Kampagnen public async Task> SearchAsync(string filter) { return (await Repository.FindAsync(c => c.Title.Contains(filter) || c.NameLocalized.Contains(filter))).ToList(); } /// /// Gibt eine gefilterte Liste von Gutschein-Kampagnen 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 Gutschein-Kampagnen 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_VoucherCampignsWithNames({0},{1})", c => c.Title.Contains(filter) || 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 != VoucherCampaignStatus.Draft); return baseQuery; } /// /// Gibt eine gefilterte Liste von Gutschein-Kampagnen 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 Gutschein-Kampagnen 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_VoucherCampignsWithNames({0},{1})", c => c.CustomerId == customerId && (c.Title.Contains(filter) || 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 != VoucherCampaignStatus.Draft); return baseQuery; } /// /// Gibt die Anzahl von Gutschein-Kampagnen zurück /// /// Sollen gelöschte inkludiert werden? /// Sollen Entitäten im Draft-Status (Cart) ausgeblendet werden? /// Anzahl Gutschein-Kampagnen 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 != VoucherCampaignStatus.Draft); return await query.CountAsync().ConfigureAwait(false); } /// /// Gibt die Anzahl von Gutschein-Kampagnen in einem Status zurück /// /// Gesuchter Status /// Sollen gelöschte inkludiert werden? /// Anzahl Gutschein-Kampagnen public async Task CountAsync(VoucherCampaignStatus 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); } /// /// Gibt eine Gutschein-Kampagne für einen Artikel im Warenkorb zurück /// /// Id des Artikels im Warenkorb /// Gutschein-Kampagne oder null, wenn nicht gefunden public async Task GetByCartItemAsync(long cartItemId) { return await Repository.FirstOrDefaultAsync(c => c.CartItemId == cartItemId && c.Status == VoucherCampaignStatus.Draft); } /// /// Gibt einen Gutschein-Kampagne für einen Artikel in einer Bestellung zurück /// /// Id des Artikels in der Bestellung /// Gutschein-Kampagne oder null, wenn nicht gefunden public async Task GetByOrderItemAsync(long orderItemId) { return await Repository.FirstOrDefaultAsync(c => c.OrderItemId == orderItemId); } /// /// Setzen des Status von Gutschein-Kampagnen nach Bestellung /// /// Id der Bestellung /// Zu setzender Status /// Anzahl betroffener Gutschein-Kampagnen public async Task SetStatusByOrderAsync(long orderId, VoucherCampaignStatus status) { var items = await Repository.FindAsync(c => c.OrderId == orderId); foreach (var item in items) { item.Status = status; } return items.Count(); } /// /// Setzen des Bezahlstatus von Gutschein-Kampagnen nach Bestellung /// /// Id der Bestellung /// Zu setzender Status /// Anzahl betroffener Gutschein-Kampagnen public async Task SetPaymentStatusByOrderAsync(long orderId, PaymentStatus status) { var items = await Repository.FindAsync(c => c.OrderId == orderId); foreach (var item in items) { item.PaymentStatus = status; } return items.Count(); } /// /// Starten von gebuchten Gutschein-Kampagnen wenn diese das Start-Datum erreicht haben /// /// Anzahl gestartete Gutschein-Kampagnen public async Task StartBookedAsnyc() { var items = await Repository.FindAsync(c => c.Status == VoucherCampaignStatus.Booked && c.Deleted == false && c.StartDate <= DateTimeOffset.UtcNow); foreach (var item in items) { item.Status = VoucherCampaignStatus.Running; } return items.Count(); } /// /// Stoppen von abgelaufenen Gutschein-Kampagnen wenn diese das Enddatum erreicht haben /// /// Anzahl gestoppte Gutschein-Kampagnen public async Task StopRunningAsync() { var items = await Repository.FindAsync(c => (c.Status == VoucherCampaignStatus.Running && c.Deleted == false && c.BudgetUsed >= c.Budget) || (c.Status == VoucherCampaignStatus.Running && c.Deleted == false && c.EndDate.Value <= DateTimeOffset.UtcNow)); foreach (var item in items) { item.EndDate = DateTimeOffset.UtcNow; item.Status = VoucherCampaignStatus.Ended; } return items.Count(); } /// /// Erstellen von Gutscheinen für eine Gutschein-Kampagne /// /// Id der Kampagne /// Der Benutzer der die Gutscheine angelegt hat /// Anzahl der erstellten Gutscheine public async Task CreateVouchersForCampaign(string voucherCampaignId, string userName) { var campaign = await Repository.FirstOrDefaultAsync(c => c.Id == voucherCampaignId); if (campaign != null) { int sufficientBufferSizeInBytes = (8 * 6 + 7) / 8; for (var i = 0; i < campaign.NumberOfVouchers; i++) { var buffer = new byte[sufficientBufferSizeInBytes]; buffer = RandomNumberGenerator.GetBytes(8); string code = Convert.ToBase64String(buffer).Substring(0, 8); var voucher = new Voucher() { Id = Guid.NewGuid().ToString("N"), VoucherCampaignId = campaign.Id, Code = $"{campaign.Prefix}-" + GenerateManualCode(8), QrCode = Guid.NewGuid().ToString("N").ToUpperInvariant(), UsedCount = 0, VoucherAmmount = campaign.VoucherAmmount, IsValid = true }; _vouchersRepository.Add(voucher); if (i % 100 == 0) { await CommitAsync(userName, noAudit: true); } } await CommitAsync(userName, noAudit: true); return campaign.NumberOfVouchers; } return 0; } /// /// Prüft ob ein Präfix für eine Gutschein-Kampagne verfügbar ist /// /// Präfix der geprüft werden soll /// Id der Kampagne die ignoriert werden soll /// true wenn verfügbar, false sonst public async Task IsPrefixAvailableAsync(string prefix, string voucherCampaignId) { var itemFound = await Repository.FirstOrDefaultAsync(c => c.Id != voucherCampaignId && c.Prefix == prefix).ConfigureAwait(false); return itemFound == null; } /// /// Aktualisiert den Gutschein-Betrag für alle noch nicht eingelösten oder deaktivierten Gutscheine einer Kampagne /// /// Id der Kampagne /// Wert der gesetzt werden soll /// Anzahl Gutscheine die betroffen sind public async Task UpdateVoucherAmmountAsync(string voucherCampaignId, decimal voucherAmmount) { var vouchers = await _vouchersRepository.FindAsync(c => c.VoucherCampaignId == voucherCampaignId && c.UsedCount == 0 && c.IsValid == true); foreach (var voucher in vouchers) { voucher.VoucherAmmount = voucherAmmount; } return vouchers.Count(); } /// /// Löschen der Gutscheine und deren Verwendung für eine Kampagne /// /// Id der Kampagne /// Anzahl gelöscht public async Task RemoveVouchersAsync(string voucherCampaignId) { var vouchers = await _vouchersRepository.FindAsync(c => c.VoucherCampaignId == voucherCampaignId); foreach (var voucher in vouchers) { var usages = await _voucherUsagesRepository.FindAsync(c => c.VoucherId == voucher.Id); foreach (var usage in usages) { _voucherUsagesRepository.Remove(usage); } _vouchersRepository.Remove(voucher); } return vouchers.Count(); } /// /// Gibt eine Liste von Gutscheinen für eine Kampagne zurück /// /// Id der Kampagne /// Liste Gutscheine public async Task> GetVouchersForCampaignAsync(string voucherCampaignId) { var vouchers = await _vouchersRepository.FindAsync(c => c.VoucherCampaignId == voucherCampaignId).ConfigureAwait(false); return vouchers.ToList(); } /// /// Prüft ob ein manueller Gutscheincode für einen Benutzer gültig ist und eingesetzt werden kann /// /// Code /// Id des AppUsers /// Validierungs-Ergebnis public async Task ValidateVoucherCodeAsync(string code, string appUserId) { var voucher = await _vouchersRepository.FirstOrDefaultAsync(c => c.Code == code).ConfigureAwait(false); if (voucher != null) { var voucherCampaign = await Repository.FirstOrDefaultAsync(c => c.Id == voucher.VoucherCampaignId).ConfigureAwait(false); if (voucherCampaign != null) { //Läuft die Kampange noch nicht? if(voucherCampaign.Status < VoucherCampaignStatus.Running) return VoucherValidationResult.CampaignNotStarted; //Ist die Kampagne schon beendet? if (voucherCampaign.Status > VoucherCampaignStatus.Running) return VoucherValidationResult.CampaignEnded; //Wurde das Budget der Kampagne schon aufgebraucht? if (voucherCampaign.BudgetUsed >= voucherCampaign.Budget) return VoucherValidationResult.CampaignBudgetUsedUp; //Ist der Gutschein noch gültig? if(voucher.IsValid == false) return VoucherValidationResult.VoucherAlreadyUsed; //Wurde der Gutschein schon zu oft verwendet? var usedByUserCount = await _voucherUsagesRepository.CountAsync(c => c.VoucherId == voucher.Id && c.AppUserId == appUserId).ConfigureAwait(false); if(usedByUserCount >= voucherCampaign.MaxUsagePerUser) return VoucherValidationResult.MaxUsageReached; return VoucherValidationResult.Valid; } } return VoucherValidationResult.Invalid; } /// /// Gibt einen Gutschein anhand des Codes zurück /// /// Code des Gutscheins /// Gutschein oder null, wenn nicht gefunden public async Task GetVoucherByCodeAsync(string code) { var voucher = await _vouchersRepository.FirstOrDefaultAsync(c => c.Code == code).ConfigureAwait(false); return voucher; } /// /// Gibt einen Gutschein anhand des QR-Codes zurück /// /// QR-Code des Gutscheins /// Gutschein oder null, wenn nicht gefunden public async Task GetVoucherByQrCodeAsync(string qrCode) { var voucher = await _vouchersRepository.FirstOrDefaultAsync(c => c.QrCode == qrCode).ConfigureAwait(false); return voucher; } /// /// Reserviert einen Gutschein für einen Benutzer und einen Walk /// /// Id des Vouchers /// Id des AppUsers /// Id des Walks /// Wert des Gutscheins /// Wert der verbraucht wurde. Kann kleiner Gutscheinwert sein /// VoucherUsage public async Task ReserveVoucherAsync(string voucherId, string appUserId, string walkId, decimal voucherAmmount, decimal voucherAmmountUsed) { var voucherUsage = new VoucherUsage() { VoucherId = voucherId, AppUserId = appUserId, WalkId = walkId, VoucherAmmount = voucherAmmount, VoucherAmmountUsed = voucherAmmountUsed, UsageStatus = VoucherUsageStatus.Reserved, Created = DateTimeOffset.UtcNow }; _voucherUsagesRepository.Add(voucherUsage); return voucherUsage; } /// /// Löschen der Verwendung eines Gutscheins /// /// VoucherUsage public void RemoveUsage(VoucherUsage voucherUsage) { _voucherUsagesRepository.Remove(voucherUsage); } /// /// Gibt die Verwendung eines Gutscheins zurück /// /// Id des Gutscheins /// Id des AppUsers /// Id des Walks /// VoucherUsage oder null, wenn nicht gefunden public async Task GetUsageAsync(string voucherId, string appUserId, string walkId) { var voucherUsage = await _voucherUsagesRepository.FirstOrDefaultAsync(c => c.VoucherId == voucherId && c.AppUserId == appUserId && c.WalkId == walkId).ConfigureAwait(false); return voucherUsage; } /// /// Gibt einen Gutschein zurück /// /// Id des Gutscheins /// Voucher oder null, wenn nicht gefunden public async Task GetVoucherAsync(string voucherId) { var voucher = await _vouchersRepository.FirstOrDefaultAsync(c => c.Id == voucherId).ConfigureAwait(false); return voucher; } /// /// Löschen von Voucher-Usages für einen AppUser /// /// Id des AppUsers /// Anzahl gelöscht public async Task RemoveVoucherUsageForAppUserAsync(string appUserId) { var usages = await _voucherUsagesRepository.FindAsync(c => c.AppUserId == appUserId).ConfigureAwait(false); foreach (var voucherUsage in usages) { _voucherUsagesRepository.Remove(voucherUsage); } return usages.Count(); } #region Helper /// /// Erstellt einen zufälligen Code in einer bestimmten Länge /// /// Länge des Codes /// Code private string GenerateManualCode(int length) { var sufficientBufferSizeInBytes = (length * 6 + 7) / 8; var buffer = RandomNumberGenerator.GetBytes(sufficientBufferSizeInBytes); var code = Convert.ToBase64String(buffer)[..length]; code = code.ToUpperInvariant(); var finalCode = $"{code.Substring(0,length / 2)}-{code.Substring(length/2, length/2)}"; if (finalCode.Contains("+") || finalCode.Contains("/") || finalCode.Contains("=")) { return GenerateManualCode(length); } return finalCode; } #endregion } }