From 3dbe1c027bcc264db28c646d6a830ebbd8fad25c Mon Sep 17 00:00:00 2001 From: andy zheng Date: Thu, 22 Jan 2026 23:11:37 -0500 Subject: [PATCH 1/3] Logic for StartTimeUtc --- .../Indexes/ProductPartIndex.cs | 5 +++++ .../Migrations/ProductMigrations.cs | 17 +++++++++++++++++ .../OrchardCore.Commerce/Models/ProductPart.cs | 13 +++++++++++++ .../Services/ProductListService.cs | 9 ++++++++- .../Services/ProductService.cs | 6 +++++- 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs b/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs index f54e89b57..c8ca98714 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, + EndTimeUtc = productPart.EndTimeUtc, }; }); } diff --git a/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs index 247f55552..12ac1d008 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs @@ -54,4 +54,21 @@ await _contentDefinitionManager .WithSettings(new MediaFieldSettings { Multiple = false }))); return 2; } + + public async Task UpdateFrom2Async() + { + await SchemaBuilder + .AlterTableAsync(nameof(ProductPartIndex), table => table + .AddColumn(nameof(ProductPartIndex.StartTimeUtc)) + .AddColumn(nameof(ProductPartIndex.EndTimeUtc))); + + await SchemaBuilder + .AlterTableAsync(nameof(ProductPartIndex), table => table + .CreateIndex( + $"IDX_{nameof(ProductPartIndex)}_TimeBased", + nameof(ProductPartIndex.StartTimeUtc), + nameof(ProductPartIndex.EndTimeUtc))); + + return 3; + } } diff --git a/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs b/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs index 4275af296..65eef7aa7 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs +++ b/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs @@ -2,6 +2,7 @@ using OrchardCore.Commerce.Inventory.Models; using OrchardCore.ContentManagement; using OrchardCore.Media.Fields; +using System; using System.Collections.Generic; namespace OrchardCore.Commerce.Models; @@ -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 DateTime? StartTimeUtc { get; set; } + + /// + /// 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 DateTime? EndTimeUtc { get; set; } } diff --git a/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs b/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs index 7c3ba3e6b..ffe741bb8 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs @@ -1,7 +1,8 @@ +using OrchardCore.Commerce.Indexes; using OrchardCore.Commerce.Models; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Metadata; -using OrchardCore.ContentManagement.Records; +using OrchardCore.ContentManagement.Metadata.Records; using System; using System.Collections.Generic; using System.Linq; @@ -41,6 +42,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(); From b92ed2708f035f5644527d9921f2b3d8b783acbd Mon Sep 17 00:00:00 2001 From: andy zheng Date: Thu, 22 Jan 2026 23:16:59 -0500 Subject: [PATCH 2/3] Pass build --- .../Migrations/ProductMigrations.cs | 14 +++++++++----- .../Services/ProductListService.cs | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs b/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs index 12ac1d008..daba49c0d 100644 --- a/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs +++ b/src/Modules/OrchardCore.Commerce/Migrations/ProductMigrations.cs @@ -58,16 +58,20 @@ await _contentDefinitionManager public async Task UpdateFrom2Async() { await SchemaBuilder - .AlterTableAsync(nameof(ProductPartIndex), table => table - .AddColumn(nameof(ProductPartIndex.StartTimeUtc)) - .AddColumn(nameof(ProductPartIndex.EndTimeUtc))); + .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", - nameof(ProductPartIndex.StartTimeUtc), - nameof(ProductPartIndex.EndTimeUtc))); + new[] + { + nameof(ProductPartIndex.StartTimeUtc), + nameof(ProductPartIndex.EndTimeUtc) + })); return 3; } diff --git a/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs b/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs index ffe741bb8..a72bc6a43 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ProductListService.cs @@ -3,6 +3,7 @@ using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Metadata; using OrchardCore.ContentManagement.Metadata.Records; +using OrchardCore.ContentManagement.Records; using System; using System.Collections.Generic; using System.Linq; From 1d2d21aad1b7054e3257539a9b9ae134f9f221ef Mon Sep 17 00:00:00 2001 From: andy zheng Date: Fri, 23 Jan 2026 02:14:29 -0500 Subject: [PATCH 3/3] use DateTimeField --- .../Indexes/ProductPartIndex.cs | 4 ++-- .../Migrations/ProductMigrations.cs | 22 +++++++++++++++++++ .../Models/ProductPart.cs | 6 ++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs b/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs index c8ca98714..19d9486b5 100644 --- a/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs +++ b/src/Modules/OrchardCore.Commerce/Indexes/ProductPartIndex.cs @@ -39,8 +39,8 @@ public override void Describe(DescribeContext context) => { Sku = productPart.Sku.ToUpperInvariant(), ContentItemId = contentItem.ContentItemId, - StartTimeUtc = productPart.StartTimeUtc, - EndTimeUtc = productPart.EndTimeUtc, + 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 daba49c0d..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; @@ -75,4 +76,25 @@ await SchemaBuilder 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 65eef7aa7..d1a0ac7de 100644 --- a/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs +++ b/src/Modules/OrchardCore.Commerce/Models/ProductPart.cs @@ -1,8 +1,8 @@ using OrchardCore.Commerce.Abstractions.Abstractions; using OrchardCore.Commerce.Inventory.Models; using OrchardCore.ContentManagement; +using OrchardCore.ContentFields.Fields; using OrchardCore.Media.Fields; -using System; using System.Collections.Generic; namespace OrchardCore.Commerce.Models; @@ -32,11 +32,11 @@ public class ProductPart : ContentPart, ISkuHolderContent /// 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 DateTime? StartTimeUtc { get; set; } + 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 DateTime? EndTimeUtc { get; set; } + public DateTimeField EndTimeUtc { get; set; } = new(); }