using Raven.Client.Documents; using System.Text.RegularExpressions; namespace zero.Commerce.Numbers; public class _NumberService : I_NumberService { /// public IEnumerable RegisteredTypes { get; private set; } protected INumberStore Store { get; set; } const string DEFAULT_COUNTER = "Default"; Regex templateRegex = new("{(date|number|year|random):?([a-zA-Z0-9-_]*)}", RegexOptions.IgnoreCase); Regex containsYearRegex = new("{(date|year|date:([^}]*y+[^}]*))}", RegexOptions.IgnoreCase); Random random = new(); public _NumberService(INumberStore store, IZeroOptions options) { Store = store; RegisteredTypes = options.For().GetAll().Select(x => x as NumberType); } /// public async Task Get(string alias) { NumberType type = RegisteredTypes.FirstOrDefault(x => x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase)); return await Load(type, () => type.Construct(type) as Number); } /// public async Task Get(string alias = null) where T : Number, new() { NumberType type = RegisteredTypes.FirstOrDefault(x => x.FlavorType == typeof(T) && (alias.IsNullOrEmpty() || x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase))); return await Load(type); } /// public async Task> GetCounters(string alias) => await GetCounters(await Get(alias)); /// public async Task> GetCounters(string alias = null) where T : Number, new() => await GetCounters(await Get(alias)); protected async Task> GetCounters(Number number) { if (number == null || number.Hash.IsNullOrEmpty()) { return new(0, 1, 1); } List items = new(); Dictionary counters = await Store.Session.CountersFor(number).GetAllAsync(); foreach (var counter in counters) { long value = counter.Value ?? 0; int.TryParse(counter.Key, out int year); bool isPassed = year > 0 && year < DateTimeOffset.Now.Year; items.Add(new() { Count = value, Key = counter.Key, Value = isPassed ? "@shop.number.fields.counterlist.yearpassed" : Compile(number, value + 1) }); } return new(items, items.Count, 1, items.Count); } /// public async Task> GetAll(ListQuery query = default) { query ??= new(); List result = new(); List numbers = await Store.LoadAll(); foreach (NumberType type in RegisteredTypes) { Number number = numbers.FirstOrDefault(x => x.Flavor == type.Alias); if (number == null) { number = type.Construct(type) as Number; number.Flavor = type.Alias; } number.Name = type.Name; number.IsActive = true; result.Add(number); } return new(result.Paging(query.Page, query.PageSize).ToList(), result.Count, query.Page, query.PageSize); } /// public async Task Next(string alias = null, DateTimeOffset? date = null) where T : Number, new() { T preset = await Get(alias); if (!IsStored(preset)) { NumberType type = RegisteredTypes.FirstOrDefault(x => x.FlavorType == typeof(T) && (alias.IsNullOrEmpty() || x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase))); preset = await Create(type); } string counterName = GetCounterName(preset); // get a matching counter var counters = Store.Session.CountersFor(preset); long? storedCount = await counters.GetAsync(counterName); // calculate new value bool hasValue = storedCount.HasValue; long oldValue = hasValue ? storedCount.Value : preset.StartNumber; long incrementBy = hasValue ? 1 : 0; long newValue = oldValue + incrementBy; // increment the value and store it in database counters.Increment(counterName, hasValue ? incrementBy : newValue); await Store.Session.SaveChangesAsync(); // compiles the template and returns the rendered result return Compile(preset, newValue, date); } /// public async Task Reset(string alias) => await Reset(await Get(alias)); /// public async Task Reset(string alias = null) where T : Number, new() => await Reset(await Get(alias)); /// protected async Task Reset(Number preset) { if (!IsStored(preset)) { return; } string counterName = GetCounterName(preset); // get a matching counter var counters = Store.Session.CountersFor(preset); // delete the counter counters.Delete(counterName); await Store.Session.SaveChangesAsync(); } /// public async Task> Update(Number preset) { return IsStored(preset) ? await Store.Update(preset) : await Store.Create(preset); } /// public async Task Delete(string alias) { Number preset = await Get(alias); if (IsStored(preset)) { await Store.Delete(preset); } } /// public async Task Delete(string alias = null) where T : Number, new() { T preset = await Get(alias); if (IsStored(preset)) { await Store.Delete(preset); } } /// public string Compile(T number, long value, DateTimeOffset? date = null) where T : Number { string output = number.Template; MatchCollection matches = templateRegex.Matches(output); DateTimeOffset usedDate = date ?? DateTimeOffset.Now; foreach (Match match in matches) { if (!match.Success) { continue; } string original = match.Value; string type = match.Groups[1].Value; string options = match.Groups[2].Value; string result = String.Empty; if (type == "year") { result = usedDate.Year.ToString(); } else if (type == "date") { result = usedDate.ToString(options.IsNullOrWhiteSpace() ? "dd-MM-yyyy" : options); } else if (type == "number") { int minLength = number.MinLength > 0 && number.MinLength <= 16 ? number.MinLength : 1; result = value.ToString("D" + minLength); } else if (type == "random") { int length = 5; if (!options.IsNullOrWhiteSpace() && Int32.TryParse(options, out int oLength) && oLength > 0 && oLength <= 10) { length = oLength; } result = random.Next(0, Int32.MaxValue).ToString("D10").Substring(0, length); } output = output.ReplaceFirst(original, result); } return output; } /// /// Get integration data from database /// protected async Task Load(NumberType type, Func createInstance = null) where T : Number, new() { if (type == null) { return default; } T entity = await Store.Session.Query().FirstOrDefaultAsync(x => x.Flavor == type.Alias && x.IsActive); if (entity == null) { entity = createInstance != null ? createInstance() : new T(); } entity.Name = type.Name; entity.IsActive = true; entity.Flavor = type.Alias; return entity; } /// /// Saves a new number to the database /// protected async Task Create(NumberType type, Action modify = null) where T : Number, new() { T entity = new(); entity.Flavor = type.Alias; entity.Name = null; entity.IsActive = true; modify?.Invoke(entity); Result result = await Store.Create(entity); if (!result.IsSuccess) { throw new Exception("Could not store number in database"); // TODO add details for logging } return (T)result.Model; } /// /// Get the name for the current counter /// protected virtual string GetCounterName(T number) where T : Number { if (!number.ResetNumberPerYear || !containsYearRegex.IsMatch(number.Template)) { return DEFAULT_COUNTER; } return DateTimeOffset.Now.Year.ToString(); } /// /// Determines if this number comes from the database /// protected virtual bool IsStored(T number) where T : Number { return number != null && !number.Hash.IsNullOrEmpty(); } } public interface I_NumberService { /// /// Get a number config by an alias /// Task Get(string alias); /// /// Get the number config of a certain type with an optional alias /// Task Get(string alias = null) where T : Number, new(); /// /// Get all counters for a number /// Task> GetCounters(string alias); /// /// Get all counters for a number /// Task> GetCounters(string alias = null) where T : Number, new(); /// /// Get all numbers /// Task> GetAll(ListQuery query = default); /// /// Get the next available number for the specified template /// Task Next(string alias = null, DateTimeOffset? date = null) where T : Number, new(); /// /// Resets the current counter for the specified template /// Task Reset(string alias); /// /// Resets the current counter for the specified template /// Task Reset(string alias = null) where T : Number, new(); /// /// Updates number properties /// Task> Update(Number preset); /// /// Resets the whole number configuration including changes and all attached counters /// Task Delete(string alias); /// /// Resets the whole number configuration including changes and all attached counters /// Task Delete(string alias = null) where T : Number, new(); /// /// Renders the final output from the template and the value /// string Compile(T number, long value, DateTimeOffset? date = null) where T : Number; }