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;
}