Skip to content

Commit d1235e6

Browse files
Use relations to track Store root to a checkout page
1 parent 1d86571 commit d1235e6

7 files changed

+252
-111
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Umbraco.Cms.Core.Cache;
6+
using Umbraco.Cms.Core.Events;
7+
using Umbraco.Cms.Core.Models;
8+
using Umbraco.Cms.Core.Notifications;
9+
using Umbraco.Cms.Core.Services;
10+
using Umbraco.Cms.Core.Services.Changes;
11+
using Umbraco.Cms.Core.Services.Navigation;
12+
using Umbraco.Cms.Core.Sync;
13+
using Umbraco.Extensions;
14+
15+
namespace Umbraco.Commerce.Checkout.Events;
16+
17+
public abstract class ContentOfTypeCacheRefresherNotificationHandlerBase(
18+
IDocumentNavigationQueryService documentNavigationQueryService,
19+
IContentService contentService,
20+
IIdKeyMap keyMap,
21+
IServerRoleAccessor serverRoleAccessor) : INotificationAsyncHandler<ContentCacheRefresherNotification>
22+
{
23+
protected abstract string ContentTypeAlias { get; }
24+
protected abstract Task HandleContentOfTypeAsync(IContent content);
25+
26+
public async Task HandleAsync(ContentCacheRefresherNotification notification, CancellationToken cancellationToken)
27+
{
28+
ArgumentNullException.ThrowIfNull(notification, nameof(notification));
29+
30+
if (serverRoleAccessor.CurrentServerRole is ServerRole.Subscriber or ServerRole.Unknown)
31+
{
32+
return;
33+
}
34+
35+
if (notification.MessageType != MessageType.RefreshByPayload)
36+
{
37+
throw new NotSupportedException();
38+
}
39+
40+
if (notification.MessageObject is not ContentCacheRefresher.JsonPayload[] payloads)
41+
{
42+
return;
43+
}
44+
45+
foreach (ContentCacheRefresher.JsonPayload payload in payloads)
46+
{
47+
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
48+
{
49+
// Single node refresh
50+
IContent? content = contentService.GetById(payload.Id);
51+
if (content != null && content.ContentType.Alias == ContentTypeAlias)
52+
{
53+
await HandleContentOfTypeAsync(content);
54+
}
55+
}
56+
else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
57+
{
58+
// Branch refresh
59+
Guid rootNodeKey = payload.Key ?? keyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).ResultOr(Guid.Empty);
60+
if (rootNodeKey != Guid.Empty && documentNavigationQueryService.TryGetDescendantsKeysOfType(rootNodeKey, ContentTypeAlias, out IEnumerable<Guid> keys))
61+
{
62+
foreach (Guid key in keys)
63+
{
64+
IContent? content = contentService.GetById(key);
65+
if (content != null)
66+
{
67+
await HandleContentOfTypeAsync(content);
68+
}
69+
}
70+
}
71+
}
72+
else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
73+
{
74+
if (documentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys))
75+
{
76+
foreach (Guid rootKey in rootKeys)
77+
{
78+
if (documentNavigationQueryService.TryGetDescendantsKeysOfType(rootKey, ContentTypeAlias, out IEnumerable<Guid> keys))
79+
{
80+
foreach (Guid key in keys)
81+
{
82+
IContent? content = contentService.GetById(key);
83+
if (content != null)
84+
{
85+
await HandleContentOfTypeAsync(content);
86+
}
87+
}
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using Umbraco.Cms.Core.Models;
5+
using Umbraco.Cms.Core.Services;
6+
using Umbraco.Cms.Core.Services.Navigation;
7+
using Umbraco.Cms.Core.Sync;
8+
using UmbracoCommerceConstants = Umbraco.Commerce.Cms.Constants;
9+
10+
namespace Umbraco.Commerce.Checkout.Events;
11+
12+
public class SetCheckoutStoreRootRelation(
13+
IDocumentNavigationQueryService documentNavigationQueryService,
14+
IContentService contentService,
15+
IIdKeyMap keyMap,
16+
IServerRoleAccessor serverRoleAccessor,
17+
IRelationService relationService)
18+
: ContentOfTypeCacheRefresherNotificationHandlerBase(documentNavigationQueryService, contentService, keyMap, serverRoleAccessor)
19+
{
20+
private readonly IDocumentNavigationQueryService _documentNavigationQueryService = documentNavigationQueryService;
21+
private readonly IContentService _contentService = contentService;
22+
23+
protected override string ContentTypeAlias => UmbracoCommerceCheckoutConstants.ContentTypes.Aliases.CheckoutPage;
24+
25+
protected override Task HandleContentOfTypeAsync(IContent content)
26+
{
27+
if (content.HasProperty(UmbracoCommerceConstants.Properties.StorePropertyAlias))
28+
{
29+
EnsureStoreCheckoutStoreRelation(content, content);
30+
}
31+
else
32+
{
33+
if (_documentNavigationQueryService.TryGetAncestorsKeys(content.Key, out IEnumerable<Guid> ancestorsKeys))
34+
{
35+
foreach (Guid ancestorKey in ancestorsKeys)
36+
{
37+
IContent? content2 = _contentService.GetById(ancestorKey);
38+
if (content2 != null)
39+
{
40+
if (content2.HasProperty(UmbracoCommerceConstants.Properties.StorePropertyAlias))
41+
{
42+
EnsureStoreCheckoutStoreRelation(content2, content);
43+
break;
44+
}
45+
}
46+
}
47+
}
48+
}
49+
50+
return Task.CompletedTask;
51+
}
52+
53+
private void EnsureStoreCheckoutStoreRelation(IContent storeRootPage, IContent checkoutPage)
54+
{
55+
if (!relationService.AreRelated(checkoutPage.Id, storeRootPage.Id, UmbracoCommerceCheckoutConstants.RelationTypes.Aliases.StoreCheckout))
56+
{
57+
IRelationType? relationType = relationService.GetRelationTypeByAlias(UmbracoCommerceCheckoutConstants.RelationTypes.Aliases.StoreCheckout);
58+
59+
if (relationType == null)
60+
{
61+
relationType = new RelationType(UmbracoCommerceCheckoutConstants.RelationTypes.Aliases.StoreCheckout, "[Umbraco Commerce Checkout] Store Checkout");
62+
relationService.Save(relationType);
63+
}
64+
65+
relationService.Save(new Relation(storeRootPage.Id, checkoutPage.Id, relationType));
66+
}
67+
}
68+
}

src/Umbraco.Commerce.Checkout/Events/SyncZeroValuePaymentProviderContinueUrl.cs

Lines changed: 52 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,100 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Threading;
43
using System.Threading.Tasks;
5-
using Umbraco.Cms.Core.Cache;
6-
using Umbraco.Cms.Core.Events;
7-
using Umbraco.Cms.Core.Models.PublishedContent;
8-
using Umbraco.Cms.Core.Notifications;
9-
using Umbraco.Cms.Core.PublishedCache;
10-
using Umbraco.Cms.Core.Services.Changes;
4+
using Umbraco.Cms.Core.Models;
5+
using Umbraco.Cms.Core.Routing;
6+
using Umbraco.Cms.Core.Services;
117
using Umbraco.Cms.Core.Services.Navigation;
128
using Umbraco.Cms.Core.Sync;
13-
using Umbraco.Commerce.Checkout.Web;
149
using Umbraco.Commerce.Core.Api;
1510
using Umbraco.Commerce.Core.Models;
1611
using Umbraco.Commerce.Extensions;
17-
using Umbraco.Extensions;
12+
using UmbracoCommerceConstants = Umbraco.Commerce.Cms.Constants;
1813

1914
namespace Umbraco.Commerce.Checkout.Events
2015
{
21-
public class SyncZeroValuePaymentProviderContinueUrl : INotificationAsyncHandler<ContentCacheRefresherNotification>
16+
public class SyncZeroValuePaymentProviderContinueUrl(
17+
IDocumentNavigationQueryService documentNavigationQueryService,
18+
IContentService contentService,
19+
IIdKeyMap keyMap,
20+
IServerRoleAccessor serverRoleAccessor,
21+
IUmbracoCommerceApi commerceApi,
22+
IPublishedUrlProvider publishedUrlProvider)
23+
: ContentOfTypeCacheRefresherNotificationHandlerBase(documentNavigationQueryService, contentService, keyMap, serverRoleAccessor)
2224
{
23-
private readonly IUmbracoCommerceApi _commerceApi;
24-
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
25-
private readonly IPublishedContentCache _publishedContentCache;
25+
private readonly IDocumentNavigationQueryService _documentNavigationQueryService = documentNavigationQueryService;
26+
private readonly IContentService _contentService = contentService;
2627

27-
public SyncZeroValuePaymentProviderContinueUrl(
28-
IUmbracoCommerceApi commerceApi,
29-
IDocumentNavigationQueryService documentNavigationQueryService,
30-
IPublishedContentCache publishedContentCache)
28+
protected override string ContentTypeAlias => UmbracoCommerceCheckoutConstants.ContentTypes.Aliases.CheckoutStepPage;
29+
30+
protected override async Task HandleContentOfTypeAsync(IContent content)
3131
{
32-
_commerceApi = commerceApi;
33-
_documentNavigationQueryService = documentNavigationQueryService;
34-
_publishedContentCache = publishedContentCache;
32+
if (IsConfirmationPageType(content))
33+
{
34+
await DoSyncZeroValuePaymentProviderContinueUrlAsync(content);
35+
}
3536
}
3637

37-
public async Task HandleAsync(ContentCacheRefresherNotification notification, CancellationToken cancellationToken)
38+
private static bool IsConfirmationPageType(IContent content)
3839
{
39-
ArgumentNullException.ThrowIfNull(notification, nameof(notification));
40-
41-
if (notification.MessageType != MessageType.RefreshByPayload)
40+
if (!content.HasProperty("uccStepType"))
4241
{
43-
throw new NotSupportedException();
42+
return false;
4443
}
4544

46-
if (notification.MessageObject is not ContentCacheRefresher.JsonPayload[] payloads)
45+
return content.ContentType.Alias == UmbracoCommerceCheckoutConstants.ContentTypes.Aliases.CheckoutStepPage && content.GetValue<string>("uccStepType") == "Confirmation";
46+
}
47+
48+
private async Task DoSyncZeroValuePaymentProviderContinueUrlAsync(IContent content)
49+
{
50+
if (!content.Published)
4751
{
52+
// Don't do anything if the content is not published
4853
return;
4954
}
5055

51-
foreach (ContentCacheRefresher.JsonPayload payload in payloads)
56+
// Get the store ID from the store picker in the contents ancestors
57+
Guid? storeId = null;
58+
59+
if (content.HasProperty(UmbracoCommerceConstants.Properties.StorePropertyAlias))
5260
{
53-
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
54-
{
55-
// Single node refresh
56-
IPublishedContent? node = _publishedContentCache.GetById(payload.Id);
57-
if (node != null && IsConfirmationPageType(node))
58-
{
59-
await DoSyncZeroValuePaymentProviderContinueUrlAsync(node);
60-
}
61-
}
62-
else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
63-
{
64-
// Branch refresh
65-
IPublishedContent? rootNode = _publishedContentCache.GetById(payload.Id);
66-
if (rootNode != null && _documentNavigationQueryService.TryGetDescendantsKeysOfType(rootNode.Key, UmbracoCommerceCheckoutConstants.ContentTypes.Aliases.CheckoutStepPage, out IEnumerable<Guid> keys))
67-
{
68-
foreach (Guid key in keys)
69-
{
70-
IPublishedContent? node = _publishedContentCache.GetById(key);
71-
if (node != null && IsConfirmationPageType(node))
72-
{
73-
await DoSyncZeroValuePaymentProviderContinueUrlAsync(node);
74-
}
75-
}
76-
}
77-
}
78-
else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
61+
storeId = content.GetValue<Guid>(UmbracoCommerceConstants.Properties.StorePropertyAlias);
62+
}
63+
else
64+
{
65+
if (_documentNavigationQueryService.TryGetAncestorsKeys(content.Key, out IEnumerable<Guid> ancestorsKeys))
7966
{
80-
if (_documentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys))
67+
foreach (Guid ancestorKey in ancestorsKeys)
8168
{
82-
foreach (Guid rootKey in rootKeys)
69+
IContent? content2 = _contentService.GetById(ancestorKey);
70+
if (content2 != null && content2.HasProperty(UmbracoCommerceConstants.Properties.StorePropertyAlias))
8371
{
84-
if (_documentNavigationQueryService.TryGetDescendantsKeysOfType(rootKey, UmbracoCommerceCheckoutConstants.ContentTypes.Aliases.CheckoutStepPage, out IEnumerable<Guid> keys))
85-
{
86-
foreach (Guid key in keys)
87-
{
88-
IPublishedContent? node = _publishedContentCache.GetById(key);
89-
if (node != null && IsConfirmationPageType(node))
90-
{
91-
await DoSyncZeroValuePaymentProviderContinueUrlAsync(node);
92-
}
93-
}
94-
}
72+
storeId = content2.GetValue<Guid>(UmbracoCommerceConstants.Properties.StorePropertyAlias);
73+
break;
9574
}
9675
}
9776
}
9877
}
99-
}
10078

101-
private static bool IsConfirmationPageType(IPublishedContent node)
102-
{
103-
if (!node.HasProperty("uccStepType"))
79+
if (!storeId.HasValue)
10480
{
105-
return false;
81+
return;
10682
}
10783

108-
return node.ContentType.Alias == UmbracoCommerceCheckoutConstants.ContentTypes.Aliases.CheckoutStepPage && node.Value<string>("uccStepType") == "Confirmation";
109-
}
110-
111-
private async Task DoSyncZeroValuePaymentProviderContinueUrlAsync(IPublishedContent confirmationNode)
112-
{
113-
StoreReadOnly store = confirmationNode.GetStore();
114-
PaymentMethodReadOnly paymentMethod = await _commerceApi.GetPaymentMethodAsync(store.Id, UmbracoCommerceCheckoutConstants.PaymentMethods.Aliases.ZeroValue);
84+
// Get the payment method and set the continue URL
85+
PaymentMethodReadOnly paymentMethod = await commerceApi.GetPaymentMethodAsync(storeId.Value, UmbracoCommerceCheckoutConstants.PaymentMethods.Aliases.ZeroValue);
11586

11687
if (paymentMethod == null)
11788
{
11889
return;
11990
}
12091

121-
await _commerceApi.Uow.ExecuteAsync(async uow =>
92+
await commerceApi.Uow.ExecuteAsync(async uow =>
12293
{
12394
PaymentMethod writable = await paymentMethod.AsWritableAsync(uow)
124-
.SetSettingAsync("ContinueUrl", confirmationNode.Url());
95+
.SetSettingAsync("ContinueUrl", publishedUrlProvider.GetUrl(content.Key));
12596

126-
await _commerceApi.SavePaymentMethodAsync(writable);
97+
await commerceApi.SavePaymentMethodAsync(writable);
12798

12899
uow.Complete();
129100
});

src/Umbraco.Commerce.Checkout/UmbracoCommerceCheckoutConstants.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ public static class System
2727
};
2828
#pragma warning restore IDE1006 // Naming Styles
2929

30+
public static class RelationTypes
31+
{
32+
public static class Aliases
33+
{
34+
public const string StoreCheckout = "uccStoreCheckout";
35+
}
36+
}
37+
3038
public static class DataTypes
3139
{
3240
public static class Guids

src/Umbraco.Commerce.Checkout/Views/UmbracoCommerceCheckout/Templates/Email/UmbracoCommerceCheckoutGiftCardEmail.cshtml

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
1-
@using Examine
2-
@using Umbraco.Cms.Core.Services.Navigation
1+
@using Umbraco.Cms.Core.Services
32
@inherits UmbracoCommerceRazorTemplateView<GiftCardReadOnly>
43
@{
54
// Get order entities
65
var store = await UmbracoCommerceApi.GetStoreAsync(Model.StoreId);
76

87
// Lookup the umbraco commerce checkout page for the associated orders store
98
// so that we can extract relevant settings to configure the email template
10-
// var contentCache = this.GetRequiredService<IPublishedContentCache>();
11-
// var nav = this.GetRequiredService<IDocumentNavigationQueryService>();
12-
// var rootKeys = nav.TryGetRootKeys(out var keys1) ? keys1 : [];
13-
// var nodeKeys = rootKeys.SelectMany(key => nav.TryGetDescendantsKeysOfType(key, UmbracoCommerceCheckoutConstants.ContentTypes.Aliases.CheckoutPage, out var keys2) ? keys2 : []);
14-
// var checkoutPage = nodeKeys.Select(x => contentCache.GetById(x)).FirstOrDefault(x => x.GetStore()?.Id == Model.StoreId);
15-
169
var contentCache = this.GetRequiredService<IPublishedContentCache>();
17-
var examineManager = this.GetRequiredService<IExamineManager>();
18-
var searcher = examineManager.GetIndex("InternalIndex").Searcher;
19-
var storeNodeRecord = searcher.CreateQuery("content").Field("store", store.Id.ToString()).Execute().First();
20-
var checkoutPageRecords = searcher.CreateQuery("content").NodeTypeAlias(UmbracoCommerceCheckoutConstants.ContentTypes.Aliases.CheckoutPage).Execute();
21-
var checkoutPageRecord = checkoutPageRecords.First(x => x.Values["path"].StartsWith(storeNodeRecord.Values["path"]));
22-
var checkoutPage = contentCache.GetById(Guid.Parse(checkoutPageRecord.Values["__Key"]));
10+
var relationService = this.GetRequiredService<IRelationService>();
11+
var storeCheckoutRelations = relationService.GetByRelationTypeAlias(UmbracoCommerceCheckoutConstants.RelationTypes.Aliases.StoreCheckout);
12+
var storeCheckoutRelation = storeCheckoutRelations.FirstOrDefault(x => contentCache.GetById(x.ParentId)?.GetStore().Id == store.Id);
13+
var checkoutPage = storeCheckoutRelation != null ? contentCache.GetById(storeCheckoutRelation.ChildId) : null;
14+
15+
if (checkoutPage == null)
16+
{
17+
throw new InvalidOperationException($"No checkout page found for store {store.Name} ({store.Id})");
18+
}
2319

2420
var uccStoreLogoUrl = checkoutPage.Value<IPublishedContent>("uccStoreLogo")?.Url();
2521
var uccStoreTsAndCsUrl = checkoutPage.Value<IPublishedContent>("uccTermsAndConditionsPage")?.Url();

0 commit comments

Comments
 (0)