diff --git a/zero.Raven/DbProviders/RavenNumberStoreDbProvider.cs b/zero.Raven/DbProviders/RavenNumberStoreDbProvider.cs new file mode 100644 index 00000000..c1d1a4bf --- /dev/null +++ b/zero.Raven/DbProviders/RavenNumberStoreDbProvider.cs @@ -0,0 +1,41 @@ +using Raven.Client.Documents; +using System.Linq.Expressions; +using zero.Numbers; + +namespace zero.Raven; + +public class RavenNumberStoreDbProvider : IZeroNumberStoreDbProvider +{ + protected IRavenOperations Ops { get; set; } + + + public RavenNumberStoreDbProvider(IRavenOperations ops) + { + Ops = ops; + } + + + public Task Load(string id, CancellationToken ct = default) where T : ZeroEntity, new() => + Ops.Load(id); + + + public Task Find(Expression> expression, CancellationToken ct = default) where T : ZeroEntity => + Ops.Session.Query().FirstOrDefaultAsync(expression, ct); + + + public async Task> FindAll(Expression> expression, CancellationToken ct = default) + where T : ZeroEntity => + await Ops.Session.Query().Where(expression).ToListAsync(ct); + + + public Task> Create(T model, CancellationToken ct = default) where T : ZeroEntity, new() => + Ops.Create(model); + + + public Task> Update(T model, CancellationToken ct = default) where T : ZeroEntity, new() => + Ops.Update(model); + + + public Task> Delete(T model, CancellationToken ct = default) where T : ZeroEntity, new() => + Ops.Delete(model); +} \ No newline at end of file diff --git a/zero.Raven/ZeroRavenModule.cs b/zero.Raven/ZeroRavenModule.cs index ffce9b58..40b7b361 100644 --- a/zero.Raven/ZeroRavenModule.cs +++ b/zero.Raven/ZeroRavenModule.cs @@ -6,6 +6,7 @@ using Raven.Client.Documents.Indexes; using Raven.Client.Http; using zero.Identity; using zero.Media; +using zero.Numbers; namespace zero.Raven; @@ -32,6 +33,7 @@ internal class ZeroRavenModule : ZeroModule services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddOptions(); services.AddOptions().Bind(configuration.GetSection("Zero:Raven")); diff --git a/zero/Numbers/Number.cs b/zero/Numbers/Number.cs index f84c2837..14df7200 100644 --- a/zero/Numbers/Number.cs +++ b/zero/Numbers/Number.cs @@ -41,7 +41,5 @@ public class NumberCounter { public string Key { get; set; } - public string Value { get; set; } - public long Count { get; set; } } \ No newline at end of file diff --git a/zero/Numbers/NumberInterceptor.cs b/zero/Numbers/NumberInterceptor.cs deleted file mode 100644 index fa7ee314..00000000 --- a/zero/Numbers/NumberInterceptor.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Raven.Client.Documents; - -namespace zero.Commerce.Numbers; - -public class NumberInterceptor : Interceptor -{ - protected IZeroStore Store { get; private set; } - - - public NumberInterceptor(IZeroStore store) - { - Store = store; - } - - - /// - public override Task> Creating(InterceptorParameters args, Number model) => Saving(args, model, false); - - /// - public override Task> Updating(InterceptorParameters args, Number model) => Saving(args, model, true); - - - protected async Task> Saving(InterceptorParameters args, Number model, bool update) - { - string flavor = model.Flavor; - string existingId = await args.Operations.Session.Query().Where(x => x.Flavor == flavor).Select(x => x.Id).FirstOrDefaultAsync(); - - if (!existingId.IsNullOrEmpty() && existingId != model.Id) - { - return new() - { - Result = Result.Fail("@shop.errors.number.multiplenotallowed") - }; - } - - model.Alias = flavor; - model.IsActive = true; - model.Name = null; - - return default; - } -} \ No newline at end of file diff --git a/zero/Numbers/NumberOptions.cs b/zero/Numbers/NumberOptions.cs index ead86c64..b11ab523 100644 --- a/zero/Numbers/NumberOptions.cs +++ b/zero/Numbers/NumberOptions.cs @@ -1,14 +1,12 @@ namespace zero.Numbers; -public class NumberOptions : List +public class NumberOptions : Dictionary { } public class NumberOptionsItem { - public string Alias { get; set; } - public string Template { get; set; } = "{number}"; public long StartNumber { get; set; } = 1; diff --git a/zero/Numbers/NumberService.cs b/zero/Numbers/NumberService.cs deleted file mode 100644 index 92060686..00000000 --- a/zero/Numbers/NumberService.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Text.RegularExpressions; -using zero.Numbers; - -namespace zero.Numbers; - -public class NumberService : INumberService -{ - protected IZeroNumberStoreDbProvider Db { 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(IZeroNumberStoreDbProvider db, IZeroOptions options) - { - Db = db; - //RegisteredTypes = options.For().GetAll().Select(x => x as NumberType); - } - - - /// - public async Task Next(string alias, DateTimeOffset? date = null, bool store = true) - { - Number number = await Get(alias); - throw new NotImplementedException(); - } - - - /// - public Task Reset(string alias) - { - throw new NotImplementedException(); - } - - - /// - public Task ResetAll(string alias) - { - throw new NotImplementedException(); - } - - - /// - public string Compile(string alias, long value, DateTimeOffset? date = null) - { - throw new NotImplementedException(); - } - - - /// - /// Create a new number implementation - /// - protected async Task Get(string alias, string template = "{number}", long startNumber = 1, int minLength = 1) - { - string id = Id(alias); - - Number number = await Db.Load(id); - bool exists = number != null; - - if (!exists) - { - number = new() - { - Id = id, - Template = template, - StartNumber = startNumber, - MinLength = minLength - }; - - await Db.Create(number); - } - else - { - bool changed = false; - - if (!number.Template.Equals(template)) - { - number.Template = template; - changed = true; - } - if (!number.StartNumber.Equals(startNumber)) - { - number.StartNumber = startNumber; - changed = true; - } - if (!number.MinLength.Equals(minLength)) - { - number.MinLength = minLength; - changed = true; - } - - if (changed) - { - await Db.Update(number); - } - } - - return number; - } - - - /// - /// Generate number ID - /// - protected string Id(string alias) - { - return "numbers." + alias; - } -} - - -public interface INumberService -{ - /// - /// Get the next available number for the specified template - /// - Task Next(string alias, DateTimeOffset? date = null, bool store = true); - - /// - /// Resets the current counter for the specified template - /// - Task Reset(string alias); - - /// - /// Resets all counters for the specified template - /// - Task ResetAll(string alias); - - /// - /// Renders the final output from the template and the value - /// - string Compile(string alias, long value, DateTimeOffset? date = null); -} \ No newline at end of file diff --git a/zero/Numbers/Numbers.cs b/zero/Numbers/Numbers.cs new file mode 100644 index 00000000..17901643 --- /dev/null +++ b/zero/Numbers/Numbers.cs @@ -0,0 +1,245 @@ +using System.Text.RegularExpressions; + +namespace zero.Numbers; + +public class Numbers : INumbers +{ + protected IZeroNumberStoreDbProvider Db { get; set; } + + protected IZeroOptions Options { 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 Numbers(IZeroNumberStoreDbProvider db, IZeroOptions options) + { + Db = db; + Options = options; + } + + + /// + public async Task Next(string alias, DateTimeOffset? date = null, bool store = true) + { + Number number = await Get(alias); + string key = GetCounterKey(number); + + // get a matching counter + NumberCounter counter = number.Counters.FirstOrDefault(x => x.Key == key) ?? new() { Key = key }; + + // calculate new value + bool hasValue = counter.Count != 0; + long oldValue = hasValue ? counter.Count : number.StartNumber; + long newValue = oldValue + (hasValue ? 1 : 0); + + // increment the value and store it in database + if (store) + { + counter.Count = newValue; + if (!hasValue) + { + number.Counters.Add(counter); + } + await Db.Update(number); + } + + // compiles the template and returns the rendered result + return Compile(number.Template, newValue, number.MinLength, date); + } + + + /// + public async Task Reset(string alias) + { + string id = Id(alias); + Number number = await Db.Load(id); + + if (number == null) + { + return; + } + + string key = GetCounterKey(number); + + NumberCounter counter = number.Counters.FirstOrDefault(x => x.Key == key); + + if (counter != null) + { + number.Counters.Remove(counter); + await Db.Update(number); + } + } + + + /// + public async Task ResetAll(string alias) + { + string id = Id(alias); + Number number = await Db.Load(id); + + if (number == null) + { + return; + } + + number.Counters = new(); + await Db.Update(number); + } + + + /// + public string Compile(string template, long value, int minLength, DateTimeOffset? date = null) + { + string output = 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") + { + result = value.ToString("D" + (minLength > 0 && minLength <= 16 ? minLength : 1)); + } + 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; + } + + + /// + /// Create a new number implementation + /// + protected async Task Get(string alias) + { + NumberOptionsItem options = Options.For().GetValueOrDefault(alias); + + if (options == null) + { + throw new KeyNotFoundException($"Could not find number options (zero:numbers:) for alias {alias}"); + } + + string id = Id(alias); + + Number number = await Db.Load(id); + bool exists = number != null; + + if (!exists) + { + number = new() + { + Id = id, + Template = options.Template, + StartNumber = options.StartNumber, + MinLength = options.MinLength + }; + + await Db.Create(number); + } + else + { + bool changed = false; + + if (!number.Template.Equals(options.Template)) + { + number.Template = options.Template; + changed = true; + } + if (!number.StartNumber.Equals(options.StartNumber)) + { + number.StartNumber = options.StartNumber; + changed = true; + } + if (!number.MinLength.Equals(options.MinLength)) + { + number.MinLength = options.MinLength; + changed = true; + } + + if (changed) + { + await Db.Update(number); + } + } + + return number; + } + + + /// + /// Generate number ID + /// + protected string Id(string alias) + { + return "numbers." + alias; + } + + + /// + /// Get the key for the current counter + /// + protected virtual string GetCounterKey(Number number) + { + if (!containsYearRegex.IsMatch(number.Template)) + { + return DEFAULT_COUNTER; + } + + return DateTimeOffset.Now.Year.ToString(); + } +} + + +public interface INumbers +{ + /// + /// Get the next available number for the specified template + /// + Task Next(string alias, DateTimeOffset? date = null, bool store = true); + + /// + /// Resets the current counter for the specified template + /// + Task Reset(string alias); + + /// + /// Resets all counters for the specified template + /// + Task ResetAll(string alias); + + /// + /// Renders the final output from the template and the value + /// + string Compile(string template, long value, int minLength, DateTimeOffset? date = null); +} \ No newline at end of file diff --git a/zero/Numbers/ZeroCommerceNumberModule.cs b/zero/Numbers/ZeroCommerceNumberModule.cs deleted file mode 100644 index 9515fcb9..00000000 --- a/zero/Numbers/ZeroCommerceNumberModule.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace zero.Commerce.Numbers; - -internal class ZeroCommerceNumberModule : ZeroModule -{ - public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) - { - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - services.AddScoped(); - services.AddScoped, NumberValidator>(); - - services.Configure(opts => - { - opts.Configure(cfg => - { - cfg.CanUseWithoutFlavors = false; - }); - }); - } -} \ No newline at end of file diff --git a/zero/Numbers/ZeroNumberModule.cs b/zero/Numbers/ZeroNumberModule.cs index 54f9beb6..cce42dd8 100644 --- a/zero/Numbers/ZeroNumberModule.cs +++ b/zero/Numbers/ZeroNumberModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Configuration; +using FluentValidation; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace zero.Numbers; @@ -8,5 +9,8 @@ internal class ZeroNumberModule : ZeroModule public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddOptions().Bind(configuration.GetSection("Zero:Numbers")); + + services.AddScoped(); + services.AddScoped, NumberValidator>(); } } \ No newline at end of file diff --git a/zero/Numbers/_NumberService.cs b/zero/Numbers/_NumberService.cs deleted file mode 100644 index 5520d4ed..00000000 --- a/zero/Numbers/_NumberService.cs +++ /dev/null @@ -1,379 +0,0 @@ -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; -} \ No newline at end of file diff --git a/zero/ZeroBuilder.cs b/zero/ZeroBuilder.cs index 805e67ca..987213e3 100644 --- a/zero/ZeroBuilder.cs +++ b/zero/ZeroBuilder.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using zero.Mvc; +using zero.Numbers; namespace zero; @@ -55,6 +56,7 @@ public class ZeroBuilder Modules.Add(); //Modules.Add(); Modules.Add(); + Modules.Add(); //Modules.Add(); Modules.ConfigureServices(services, configuration);