OElite.Services 5.0.9-develop.510

OElite.Services

Core business logic services foundation for OElite applications.

NuGet Version .NET

Overview

OElite.Services provides the foundational service layer components and patterns for implementing business logic in OElite applications. It defines the core IOEliteService interface that enables automatic service discovery and registration through OElite.Common.Hosting.

This package establishes the service patterns used throughout the OElite platform for implementing business rules, data validation, cross-cutting concerns, and application workflows.

Features

🔧 Service Foundation

  • IOEliteService Interface: Marker interface for automatic service discovery
  • Automatic Registration: Services are discovered and registered as singletons automatically
  • Dependency Injection Ready: Designed for modern DI container patterns
  • Interface-based Design: Promotes testable and maintainable code architecture

🏗️ Business Logic Patterns

  • Multi-tenant Service Support: Built-in patterns for merchant isolation
  • Request Context Integration: Standard context passing for security and auditing
  • Validation Patterns: Integrated with OElite.Common validation systems
  • Error Handling Standards: Consistent exception handling across services

🔄 Integration Points

  • Data Layer Integration: Seamless integration with OElite.Data repositories
  • Configuration Support: Access to OElite.Common configuration patterns
  • Transformation Support: Model transformation for service responses
  • Caching Integration: Built-in support for OElite.Restme caching

Quick Start

1. Installation

dotnet add package OElite.Services

2. Basic Service Implementation

using OElite.Services;

// Service interface
public interface IProductService : IOEliteService
{
    Task<Product?> GetProductAsync(string productId, RequestContext context);
    Task<List<Product>> GetProductsByCategoryAsync(string categoryId, RequestContext context);
    Task<Product> CreateProductAsync(CreateProductRequest request, RequestContext context);
    Task<Product> UpdateProductAsync(string productId, UpdateProductRequest request, RequestContext context);
    Task DeleteProductAsync(string productId, RequestContext context);
}

// Service implementation
public class ProductService : IProductService
{
    private readonly IProductRepository _productRepository;
    private readonly ICategoryRepository _categoryRepository;
    private readonly ILogger<ProductService> _logger;

    public ProductService(
        IProductRepository productRepository,
        ICategoryRepository categoryRepository,
        ILogger<ProductService> logger)
    {
        _productRepository = productRepository;
        _categoryRepository = categoryRepository;
        _logger = logger;
    }

    public async Task<Product?> GetProductAsync(string productId, RequestContext context)
    {
        _logger.LogDebug("Getting product {ProductId} for merchant {MerchantId}",
            productId, context.MerchantId);

        var product = await _productRepository.GetByIdAsync(productId);

        // Ensure tenant isolation
        if (product != null && product.MerchantId != context.MerchantId)
        {
            _logger.LogWarning("Product {ProductId} access denied for merchant {MerchantId}",
                productId, context.MerchantId);
            return null;
        }

        return product;
    }

    public async Task<Product> CreateProductAsync(CreateProductRequest request, RequestContext context)
    {
        // Validate request
        if (string.IsNullOrEmpty(request.Name))
            throw new ValidationException("Product name is required");

        // Verify category exists and belongs to merchant
        var category = await _categoryRepository.GetByIdAsync(request.CategoryId);
        if (category == null || category.MerchantId != context.MerchantId)
            throw new BusinessException("Invalid category");

        // Create product
        var product = new Product
        {
            Name = request.Name,
            CategoryId = request.CategoryId,
            Price = request.Price,
            MerchantId = context.MerchantId,
            CreatedBy = context.UserId,
            CreatedOnUtc = DateTime.UtcNow
        };

        await _productRepository.CreateAsync(product);

        _logger.LogInformation("Created product {ProductId} for merchant {MerchantId}",
            product.Id, context.MerchantId);

        return product;
    }

    // Implement other interface methods...
}

// The service will be automatically discovered and registered when using OElite.Common.Hosting

3. Using Services in Controllers

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProduct(string id)
    {
        var context = HttpContext.GetOEliteRequestContext();
        var product = await _productService.GetProductAsync(id, context);

        if (product == null)
            return NotFound();

        return Ok(product);
    }

    [HttpPost]
    public async Task<ActionResult<Product>> CreateProduct([FromBody] CreateProductRequest request)
    {
        var context = HttpContext.GetOEliteRequestContext();
        var product = await _productService.CreateProductAsync(request, context);

        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }
}

Advanced Service Patterns

Multi-tenant Service with Caching

public class CustomerService : ICustomerService
{
    private readonly ICustomerRepository _customerRepository;
    private readonly IDistributedCache _distributedCache;
    private readonly ILogger<CustomerService> _logger;

    public CustomerService(
        ICustomerRepository customerRepository,
        IDistributedCache distributedCache,
        ILogger<CustomerService> logger)
    {
        _customerRepository = customerRepository;
        _distributedCache = distributedCache;
        _logger = logger;
    }

    public async Task<Customer?> GetCustomerAsync(string customerId, RequestContext context)
    {
        // Create tenant-specific cache key
        var cacheKey = $"customer:{context.MerchantId}:{customerId}";

        // Try cache first
        var cached = await _distributedCache.GetStringAsync(cacheKey);
        if (cached != null)
        {
            _logger.LogDebug("Customer {CustomerId} found in cache", customerId);
            return JsonSerializer.Deserialize<Customer>(cached);
        }

        // Load from database
        var customer = await _customerRepository.GetByIdAsync(customerId);

        // Ensure tenant isolation
        if (customer != null && customer.MerchantId != context.MerchantId)
        {
            _logger.LogWarning("Customer {CustomerId} access denied for merchant {MerchantId}",
                customerId, context.MerchantId);
            return null;
        }

        // Cache the result
        if (customer != null)
        {
            var serialized = JsonSerializer.Serialize(customer);
            var cacheOptions = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
            };
            await _distributedCache.SetStringAsync(cacheKey, serialized, cacheOptions);
        }

        return customer;
    }

    public async Task<Customer> UpdateCustomerAsync(string customerId, UpdateCustomerRequest request, RequestContext context)
    {
        var customer = await GetCustomerAsync(customerId, context);
        if (customer == null)
            throw new NotFoundException($"Customer {customerId} not found");

        // Update customer
        customer.Name = request.Name ?? customer.Name;
        customer.Email = request.Email ?? customer.Email;
        customer.UpdatedBy = context.UserId;
        customer.UpdatedOnUtc = DateTime.UtcNow;

        await _customerRepository.UpdateAsync(customer);

        // Invalidate cache
        var cacheKey = $"customer:{context.MerchantId}:{customerId}";
        await _distributedCache.RemoveAsync(cacheKey);

        _logger.LogInformation("Updated customer {CustomerId} for merchant {MerchantId}",
            customerId, context.MerchantId);

        return customer;
    }
}

Service with Model Transformation

public class OrderService : IOrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IModelTransformationService _transformationService;
    private readonly ILogger<OrderService> _logger;

    public OrderService(
        IOrderRepository orderRepository,
        IModelTransformationService transformationService,
        ILogger<OrderService> logger)
    {
        _orderRepository = orderRepository;
        _transformationService = transformationService;
        _logger = logger;
    }

    public async Task<object> GetOrderAsync(string orderId, RequestContext context, string format = "api")
    {
        var order = await _orderRepository.GetByIdAsync(orderId);

        if (order == null || order.MerchantId != context.MerchantId)
            return null;

        // Transform based on requested format
        var transformationContext = new TransformationContext
        {
            TargetFormat = format,
            Properties = new Dictionary<string, object>
            {
                ["UserId"] = context.UserId,
                ["IsAdmin"] = context.IsAdminRequest,
                ["IncludeInternalData"] = context.IsAdminRequest
            }
        };

        var transformed = await _transformationService.TransformAsync(order, transformationContext);
        return transformed ?? order;
    }

    public async Task<List<object>> GetOrderHistoryAsync(string customerId, RequestContext context,
        int page = 1, int size = 20, string format = "list")
    {
        var orders = await _orderRepository.GetOrdersByCustomerAsync(customerId, context.MerchantId, page, size);

        // Transform collection
        var transformationContext = new TransformationContext
        {
            TargetFormat = format,
            Properties = new Dictionary<string, object>
            {
                ["UserId"] = context.UserId,
                ["IncludeSensitiveData"] = false // Never include sensitive data in list views
            }
        };

        var transformedOrders = new List<object>();
        foreach (var order in orders)
        {
            var transformed = await _transformationService.TransformAsync(order, transformationContext);
            transformedOrders.Add(transformed ?? order);
        }

        return transformedOrders;
    }
}

Background Service Implementation

public class EmailProcessingService : OEliteBackgroundService, IOEliteService
{
    private readonly IEmailQueueRepository _emailQueueRepository;
    private readonly IEmailSender _emailSender;
    private readonly ILogger<EmailProcessingService> _logger;

    public EmailProcessingService(
        IEmailQueueRepository emailQueueRepository,
        IEmailSender emailSender,
        ILogger<EmailProcessingService> logger)
    {
        _emailQueueRepository = emailQueueRepository;
        _emailSender = emailSender;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Email processing service started");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await ProcessPendingEmailsAsync();
                await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing emails");
                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
    }

    private async Task ProcessPendingEmailsAsync()
    {
        var pendingEmails = await _emailQueueRepository.GetPendingEmailsAsync(50);

        foreach (var email in pendingEmails)
        {
            try
            {
                await _emailSender.SendEmailAsync(email);
                await _emailQueueRepository.MarkAsProcessedAsync(email.Id);

                _logger.LogDebug("Processed email {EmailId}", email.Id);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to process email {EmailId}", email.Id);
                await _emailQueueRepository.MarkAsFailedAsync(email.Id, ex.Message);
            }
        }
    }

    public async Task QueueEmailAsync(EmailRequest request, RequestContext context)
    {
        var email = new QueuedEmail
        {
            To = request.To,
            Subject = request.Subject,
            Body = request.Body,
            MerchantId = context.MerchantId,
            CreatedBy = context.UserId,
            CreatedOnUtc = DateTime.UtcNow,
            Status = EmailStatus.Pending
        };

        await _emailQueueRepository.CreateAsync(email);

        _logger.LogInformation("Queued email for {Recipient} from merchant {MerchantId}",
            request.To, context.MerchantId);
    }
}

// This service implements both IOEliteService (for DI registration)
// and OEliteBackgroundService (for hosted service functionality)

Service with Complex Business Logic

public class OrderProcessingService : IOrderProcessingService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IInventoryService _inventoryService;
    private readonly IPaymentService _paymentService;
    private readonly IShippingService _shippingService;
    private readonly ILogger<OrderProcessingService> _logger;

    public OrderProcessingService(
        IOrderRepository orderRepository,
        IInventoryService inventoryService,
        IPaymentService paymentService,
        IShippingService shippingService,
        ILogger<OrderProcessingService> logger)
    {
        _orderRepository = orderRepository;
        _inventoryService = inventoryService;
        _paymentService = paymentService;
        _shippingService = shippingService;
        _logger = logger;
    }

    public async Task<Order> ProcessOrderAsync(CreateOrderRequest request, RequestContext context)
    {
        _logger.LogInformation("Processing order for customer {CustomerId}", request.CustomerId);

        // 1. Validate order
        await ValidateOrderAsync(request, context);

        // 2. Create order in pending status
        var order = new Order
        {
            CustomerId = request.CustomerId,
            Items = request.Items,
            MerchantId = context.MerchantId,
            Status = OrderStatus.Pending,
            CreatedBy = context.UserId,
            CreatedOnUtc = DateTime.UtcNow
        };

        await _orderRepository.CreateAsync(order);

        try
        {
            // 3. Reserve inventory
            foreach (var item in order.Items)
            {
                await _inventoryService.ReserveItemAsync(item.ProductId, item.Quantity, context);
            }

            // 4. Process payment
            var paymentResult = await _paymentService.ProcessPaymentAsync(new ProcessPaymentRequest
            {
                OrderId = order.Id,
                Amount = order.TotalAmount,
                PaymentMethod = request.PaymentMethod
            }, context);

            if (!paymentResult.IsSuccess)
            {
                // Release inventory and mark order as failed
                await ReleaseInventoryAsync(order, context);
                order.Status = OrderStatus.Failed;
                order.ErrorMessage = paymentResult.ErrorMessage;
                await _orderRepository.UpdateAsync(order);

                throw new PaymentException($"Payment failed: {paymentResult.ErrorMessage}");
            }

            // 5. Create shipment
            var shipment = await _shippingService.CreateShipmentAsync(new CreateShipmentRequest
            {
                OrderId = order.Id,
                ShippingAddress = request.ShippingAddress,
                ShippingMethod = request.ShippingMethod
            }, context);

            // 6. Update order status
            order.Status = OrderStatus.Processing;
            order.PaymentId = paymentResult.PaymentId;
            order.ShipmentId = shipment.Id;
            order.UpdatedOnUtc = DateTime.UtcNow;

            await _orderRepository.UpdateAsync(order);

            _logger.LogInformation("Successfully processed order {OrderId} for merchant {MerchantId}",
                order.Id, context.MerchantId);

            return order;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process order {OrderId}", order.Id);

            // Cleanup on failure
            await ReleaseInventoryAsync(order, context);
            order.Status = OrderStatus.Failed;
            order.ErrorMessage = ex.Message;
            await _orderRepository.UpdateAsync(order);

            throw;
        }
    }

    private async Task ValidateOrderAsync(CreateOrderRequest request, RequestContext context)
    {
        if (!request.Items.Any())
            throw new ValidationException("Order must contain at least one item");

        if (request.Items.Any(i => i.Quantity <= 0))
            throw new ValidationException("All items must have positive quantities");

        // Verify all products exist and belong to the merchant
        foreach (var item in request.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            if (product == null || product.MerchantId != context.MerchantId)
                throw new ValidationException($"Invalid product: {item.ProductId}");
        }
    }

    private async Task ReleaseInventoryAsync(Order order, RequestContext context)
    {
        foreach (var item in order.Items)
        {
            try
            {
                await _inventoryService.ReleaseReservationAsync(item.ProductId, item.Quantity, context);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to release inventory for product {ProductId}", item.ProductId);
            }
        }
    }
}

Service Registration and Discovery

Automatic Registration

Services implementing IOEliteService are automatically discovered and registered:

public class Program
{
    public static async Task Main(string[] args)
    {
        await OeApp.RunWebAppAsync<MyApiConfig>(
            args: args,
            applicationName: "MyApi",
            dependencyInjectionOptions: options =>
            {
                // Services are automatically discovered from these assemblies
                options.IncludeAssemblyPrefixes.Add("MyApp");

                // Exclude specific services if needed
                options.ExcludeServiceTypes.Add(typeof(LegacyService));

                // Custom service filter
                options.ServiceTypeFilter = type => !type.Name.Contains("Test");
            });
    }
}

// All services implementing IOEliteService are registered as:
// - Singleton instances (for performance and state consistency)
// - Both concrete type and interface registrations
// - Hosted services if they also implement IHostedService

Manual Service Registration

For services that don't implement IOEliteService or need custom configuration:

await OeApp.RunWebAppAsync<MyApiConfig>(
    args: args,
    applicationName: "MyApi",
    configureServices: builder =>
    {
        // Manual service registration
        builder.Services.AddScoped<ICustomService, CustomService>();

        // Singleton service with specific configuration
        builder.Services.AddSingleton<IEmailService>(provider =>
            new EmailService(provider.GetRequiredService<IOptions<EmailOptions>>().Value));

        // Conditional service registration
        if (builder.Environment.IsProduction())
        {
            builder.Services.AddSingleton<IPaymentService, StripePaymentService>();
        }
        else
        {
            builder.Services.AddSingleton<IPaymentService, MockPaymentService>();
        }
    });

Testing Services

Unit Testing

[Test]
public async Task GetProductAsync_WithValidId_ReturnsProduct()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var mockLogger = new Mock<ILogger<ProductService>>();

    var product = new Product
    {
        Id = "test-id",
        Name = "Test Product",
        MerchantId = DbObjectId.Parse("507f1f77bcf86cd799439011")
    };

    var context = new RequestContext
    {
        MerchantId = DbObjectId.Parse("507f1f77bcf86cd799439011"),
        UserId = DbObjectId.Parse("507f1f77bcf86cd799439012")
    };

    mockRepository.Setup(r => r.GetByIdAsync("test-id"))
        .ReturnsAsync(product);

    var service = new ProductService(mockRepository.Object, Mock.Of<ICategoryRepository>(), mockLogger.Object);

    // Act
    var result = await service.GetProductAsync("test-id", context);

    // Assert
    Assert.NotNull(result);
    Assert.Equal("Test Product", result.Name);
    mockRepository.Verify(r => r.GetByIdAsync("test-id"), Times.Once);
}

[Test]
public async Task GetProductAsync_WithDifferentMerchant_ReturnsNull()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var mockLogger = new Mock<ILogger<ProductService>>();

    var product = new Product
    {
        Id = "test-id",
        MerchantId = DbObjectId.Parse("507f1f77bcf86cd799439011") // Different merchant
    };

    var context = new RequestContext
    {
        MerchantId = DbObjectId.Parse("507f1f77bcf86cd799439012") // Different merchant
    };

    mockRepository.Setup(r => r.GetByIdAsync("test-id"))
        .ReturnsAsync(product);

    var service = new ProductService(mockRepository.Object, Mock.Of<ICategoryRepository>(), mockLogger.Object);

    // Act
    var result = await service.GetProductAsync("test-id", context);

    // Assert
    Assert.Null(result); // Should return null due to tenant isolation
}

Integration Testing

public class ProductServiceIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public ProductServiceIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Test]
    public async Task CreateProduct_WithValidData_CreatesProduct()
    {
        // Arrange
        using var scope = _factory.Services.CreateScope();
        var productService = scope.ServiceProvider.GetRequiredService<IProductService>();
        var context = new RequestContext
        {
            MerchantId = DbObjectId.Parse("507f1f77bcf86cd799439011"),
            UserId = DbObjectId.Parse("507f1f77bcf86cd799439012")
        };

        var request = new CreateProductRequest
        {
            Name = "Integration Test Product",
            CategoryId = "test-category-id",
            Price = 99.99m
        };

        // Act
        var product = await productService.CreateProductAsync(request, context);

        // Assert
        Assert.NotNull(product);
        Assert.Equal("Integration Test Product", product.Name);
        Assert.Equal(context.MerchantId, product.MerchantId);
    }
}

Best Practices

1. Multi-tenant Security

// ✅ DO: Always validate merchant access
public async Task<Product?> GetProductAsync(string productId, RequestContext context)
{
    var product = await _repository.GetByIdAsync(productId);

    if (product != null && product.MerchantId != context.MerchantId)
    {
        _logger.LogWarning("Access denied: Product {ProductId} for merchant {MerchantId}",
            productId, context.MerchantId);
        return null;
    }

    return product;
}

// ❌ DON'T: Skip tenant validation
public async Task<Product?> GetProductAsync(string productId, RequestContext context)
{
    return await _repository.GetByIdAsync(productId); // Security risk!
}

2. Error Handling

// ✅ DO: Use specific exception types
public async Task<Customer> UpdateCustomerAsync(string customerId, UpdateCustomerRequest request, RequestContext context)
{
    var customer = await GetCustomerAsync(customerId, context);
    if (customer == null)
        throw new NotFoundException($"Customer {customerId} not found");

    if (string.IsNullOrEmpty(request.Email) || !IsValidEmail(request.Email))
        throw new ValidationException("Valid email address is required");

    // Update logic...
}

// ❌ DON'T: Use generic exceptions
public async Task<Customer> UpdateCustomerAsync(string customerId, UpdateCustomerRequest request, RequestContext context)
{
    var customer = await GetCustomerAsync(customerId, context);
    if (customer == null)
        throw new Exception("Not found"); // Too generic

    // Update logic...
}

3. Logging

// ✅ DO: Use structured logging with context
_logger.LogInformation("Created order {OrderId} for customer {CustomerId} in merchant {MerchantId}",
    order.Id, order.CustomerId, context.MerchantId);

// Include correlation ID for tracing
_logger.LogDebug("Processing payment for order {OrderId} with correlation {CorrelationId}",
    orderId, context.CorrelationId);

// ❌ DON'T: Use string concatenation
_logger.LogInformation("Created order " + order.Id + " for customer " + order.CustomerId);

4. Performance Optimization

// ✅ DO: Use efficient caching patterns
public async Task<List<Category>> GetCategoriesAsync(RequestContext context)
{
    var cacheKey = $"categories:{context.MerchantId}";

    var cached = await _cache.GetStringAsync(cacheKey);
    if (cached != null)
        return JsonSerializer.Deserialize<List<Category>>(cached);

    var categories = await _repository.GetCategoriesByMerchantAsync(context.MerchantId);

    await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(categories),
        new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
        });

    return categories;
}

// ❌ DON'T: Always hit the database
public async Task<List<Category>> GetCategoriesAsync(RequestContext context)
{
    return await _repository.GetCategoriesByMerchantAsync(context.MerchantId);
}

Version History

  • 5.0.9: Current version with .NET 10.0 support
  • Foundation for IOEliteService interface
  • Integration with OElite.Common.Hosting automatic discovery
  • Support for multi-tenant service patterns
  • Request context integration for security and auditing
  • OElite.Common: Core infrastructure and request context
  • OElite.Common.Hosting: Service discovery and automatic registration
  • OElite.Data: Repository patterns that integrate with services
  • OElite.Common.Hosting.AspNetCore: Web controller integration
  • OElite.Restme: Caching and external service integration

License

Copyright © Phanes Technology Ltd. All rights reserved.

Support

For support and documentation, visit https://www.oelite.com


Integration with OElite Coding Standards

This package implements the patterns described in:

Showing the top 20 packages that depend on OElite.Services.

Packages Downloads
OElite.Common.Hosting
Package Description
98
OElite.Common.Hosting
Package Description
34
OElite.Common.Hosting
Package Description
17
OElite.Common.Hosting
Package Description
16
OElite.Common.Hosting
Package Description
15
OElite.Common.Hosting
Package Description
13
OElite.Common.Hosting
Package Description
12
OElite.Common.Hosting
Package Description
10
OElite.Common.Hosting
Package Description
9
OElite.Common.Hosting
Package Description
8
OElite.Services.Kortex
Package Description
7
OElite.Common.AspNetCore
Package Description
7
OElite.Common.Hosting
Package Description
7
OElite.Common.Hosting
Package Description
6

.NET 10.0

Version Downloads Last updated
5.0.9-develop.511 4 11/26/2025
5.0.9-develop.510 2 11/26/2025
5.0.9-develop.509 3 11/23/2025
5.0.9-develop.480 2 11/17/2025
5.0.9-develop.478 2 11/17/2025
5.0.9-develop.477 3 11/17/2025
5.0.9-develop.476 3 11/17/2025
5.0.9-develop.472 3 11/15/2025
5.0.9-develop.471 2 11/15/2025
5.0.9-develop.470 3 11/15/2025
5.0.9-develop.462 2 11/14/2025
5.0.9-develop.448 3 11/11/2025
5.0.9-develop.446 2 11/11/2025
5.0.9-develop.443 3 11/11/2025
5.0.9-develop.441 3 11/09/2025
5.0.9-develop.415 10 10/28/2025
5.0.9-develop.412 4 10/27/2025
5.0.9-develop.411 3 10/27/2025
5.0.9-develop.410 7 10/27/2025
5.0.9-develop.409 4 10/27/2025
5.0.9-develop.402 16 10/26/2025
5.0.9-develop.399 12 10/26/2025
5.0.9-develop.394 13 10/25/2025
5.0.9-develop.391 9 10/25/2025
5.0.9-develop.389 7 10/25/2025
5.0.9-develop.376 30 10/25/2025
5.0.9-develop.374 8 10/24/2025
5.0.9-develop.373 3 10/24/2025
5.0.9-develop.372 4 10/24/2025
5.0.9-develop.259 93 10/20/2025
5.0.9-develop.258 4 10/20/2025
5.0.9-develop.244 5 10/19/2025
5.0.9-develop.240 4 10/18/2025
5.0.9-develop.239 5 10/18/2025
5.0.9-develop.238 4 10/18/2025
5.0.9-develop.236 5 10/18/2025
5.0.9-develop.235 5 10/18/2025
5.0.9-develop.234 5 10/17/2025
5.0.9-develop.227 5 10/17/2025
5.0.9-develop.226 5 10/17/2025
5.0.9-develop.225 5 10/17/2025
5.0.9-develop.224 5 10/16/2025
5.0.9-develop.222 4 10/16/2025
5.0.9-develop.216 5 10/16/2025
5.0.9-develop.215 4 10/16/2025
5.0.9-develop.212 4 10/15/2025
5.0.9-develop.211 5 10/14/2025
5.0.9-develop.210 5 10/14/2025
5.0.9-develop.209 4 10/14/2025
5.0.9-develop.206 4 10/13/2025
5.0.9-develop.203 4 10/13/2025
5.0.9-develop.61 4 09/17/2025
5.0.9-develop.60 4 09/17/2025
5.0.9-develop.47 4 09/15/2025