diff --git a/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs b/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs index f54e89b57..19d9486b5 100644 --- a/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs +++ b/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs @@ -1,6 +1,7 @@ using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement; +using System; using YesSql.Indexes; namespace OrchardCore.Commerce.Indexes; @@ -9,6 +10,8 @@ public class ProductPartIndex : MapIndex, ISkuHolder { public string ContentItemId { get; set; } public string Sku { get; set; } + public DateTime? StartTimeUtc { get; set; } + public DateTime? EndTimeUtc { get; set; } } /// @@ -36,6 +39,8 @@ public override void Describe(DescribeContext context) => { Sku = productPart.Sku.ToUpperInvariant(), ContentItemId = contentItem.ContentItemId, + StartTimeUtc = productPart.StartTimeUtc?.Value, + EndTimeUtc = productPart.EndTimeUtc?.Value, }; }); } diff --git a/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs index 247f55552..351bd25c3 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs @@ -2,6 +2,7 @@ using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement.Metadata; using OrchardCore.ContentManagement.Metadata.Settings; +using OrchardCore.ContentFields.Settings; using OrchardCore.Data.Migration; using OrchardCore.Media.Fields; using OrchardCore.Media.Settings; @@ -54,4 +55,46 @@ await _contentDefinitionManager .WithSettings(new MediaFieldSettings { Multiple = false }))); return 2; } + + public async Task UpdateFrom2Async() + { + await SchemaBuilder + .AlterTableAsync(nameof(ProductPartIndex), table => { + table.AddColumn(nameof(ProductPartIndex.StartTimeUtc)); + table.AddColumn(nameof(ProductPartIndex.EndTimeUtc)); + }); + + await SchemaBuilder + .AlterTableAsync(nameof(ProductPartIndex), table => table + .CreateIndex( + $"IDX_{nameof(ProductPartIndex)}_TimeBased", + new[] + { + nameof(ProductPartIndex.StartTimeUtc), + nameof(ProductPartIndex.EndTimeUtc) + })); + + return 3; + } + + public async Task UpdateFrom3Async() + { + // Add DateTimeField fields to ProductPart for UI editing + await _contentDefinitionManager + .AlterPartDefinitionAsync(builder => builder + .WithField(part => part.StartTimeUtc, field => field + .WithDisplayName("Product Start Time") + .WithSettings(new DateTimeFieldSettings + { + Hint = "The date and time (UTC) from which the product becomes visible and available for purchase. Leave empty for immediate availability.", + })) + .WithField(part => part.EndTimeUtc, field => field + .WithDisplayName("Product End Time") + .WithSettings(new DateTimeFieldSettings + { + Hint = "The date and time (UTC) until which the product remains visible and available for purchase. Leave empty for indefinite availability.", + }))); + + return 4; + } } diff --git a/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs b/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs index 4275af296..d1a0ac7de 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs +++ b/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs @@ -1,6 +1,7 @@ using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Inventory.Models; using OrchardCore.ContentManagement; +using OrchardCore.ContentFields.Fields; using OrchardCore.Media.Fields; using System.Collections.Generic; @@ -26,4 +27,16 @@ public class ProductPart : ContentPart, ISkuHolderContent /// Gets or sets the image associated with this product, which will be displayed on the product's page. /// public MediaField ProductImage { get; set; } + + /// + /// Gets or sets the date and time (UTC) from which the product becomes visible and available for purchase. + /// If null, the product is visible immediately. + /// + public DateTimeField StartTimeUtc { get; set; } = new(); + + /// + /// Gets or sets the date and time (UTC) until which the product remains visible and available for purchase. + /// If null, the product remains visible indefinitely. + /// + public DateTimeField EndTimeUtc { get; set; } = new(); } diff --git a/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs b/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs index 7c3ba3e6b..a72bc6a43 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs @@ -1,6 +1,8 @@ +using OrchardCore.Commerce.Indexes; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Metadata; +using OrchardCore.ContentManagement.Metadata.Records; using OrchardCore.ContentManagement.Records; using System; using System.Collections.Generic; @@ -41,6 +43,12 @@ public async Task GetProductsAsync(ProductListPart productList, Pro var query = _session.Query(); query = query.With(index => index.ContentType.IsIn(productTypes) && index.Published); + + // Filter products by start and end time + var utcNow = DateTime.UtcNow; + query = query.With(index => + (index.StartTimeUtc == null || index.StartTimeUtc <= utcNow) && + (index.EndTimeUtc == null || index.EndTimeUtc >= utcNow)); var context = new ProductListFilterContext { diff --git a/src/Modules/OrchardCore.Commerce/Services/ProductService.cs b/src/Modules/OrchardCore.Commerce/Services/ProductService.cs index 2006fc5d4..ba39989f7 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ProductService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ProductService.cs @@ -40,8 +40,12 @@ public virtual async Task> GetProductsAsync(IEnumerable { var trimmedSkus = skus.Select(sku => sku.Split('-')[0]); + var utcNow = DateTime.UtcNow; var contentItemIds = (await _session - .QueryIndex(index => index.Sku.IsIn(trimmedSkus)) + .QueryIndex(index => + index.Sku.IsIn(trimmedSkus) && + (index.StartTimeUtc == null || index.StartTimeUtc <= utcNow) && + (index.EndTimeUtc == null || index.EndTimeUtc >= utcNow)) .ListAsync()) .Select(idx => idx.ContentItemId) .Distinct();