Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
git fetch --tags --force

# If HEAD already has a stable vX.Y.Z tag, reuse it (idempotent re-runs).
existing_tag="$(git tag --points-at HEAD | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1 || true)"
existing_tag="$(git tag --points-at HEAD | grep -E '^v[1-9][0-9]*\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1 || true)"
if [[ -n "${existing_tag}" ]]; then
echo "tag_already_exists=true" >> "$GITHUB_OUTPUT"
echo "tag=${existing_tag}" >> "$GITHUB_OUTPUT"
Expand All @@ -98,7 +98,7 @@ jobs:
fi

# Find the latest stable (non-prerelease) tag anywhere in history.
latest="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1 || true)"
latest="$(git tag -l 'v[1-9]*' | grep -E '^v[1-9][0-9]*\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1 || true)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve v0 tags when deriving next release version

This filter now excludes all v0.x.y tags, so repositories that are still on 0-major releases will be treated as if no stable tag exists and the workflow will jump to v1.0.0 on the next publish run. In that scenario, reruns also lose the intended idempotent behavior for existing 0-major tags. If 0-major history must remain part of version progression, keep v0 in the stable-tag match (or add an explicit migration step before switching to the stricter pattern).

Useful? React with 👍 / 👎.

if [[ -z "${latest}" ]]; then
# No stable tag yet — this push becomes v1.0.0.
version="1.0.0"
Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/caching/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ This means you do not need to manually handle distributed invalidation -- just u

## Common Cache Key Prefixes

Merchello's canonical cache key/tag constants live in `Merchello.Core/Constants.cs` under `CacheKeys` and `CacheTags` (see [Constants.cs](../../../src/Merchello.Core/Constants.cs)). Services that own their own cache also define local constants. Current prefixes in use:
Merchello's canonical cache key/tag constants live in `Merchello.Core/Constants.cs` under `CacheKeys` and `CacheTags` (see [Constants.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Constants.cs)). Services that own their own cache also define local constants. Current prefixes in use:

| Prefix / Key | Used For | Source |
|---|---|---|
Expand Down
4 changes: 2 additions & 2 deletions docs/documentation/checkout/abandoned-cart.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Industry data shows 60-80% of shopping carts are abandoned. Merchello's abandone

**Why you use it:** You do not have to write any tracking code — the existing checkout services update abandoned-checkout records automatically. You enable it by registering `IAbandonedCheckoutService` (configured in `Startup.cs`) and configuring the recovery email topics.

Source: [AbandonedCheckoutDetectionJob.cs](../../../src/Merchello.Core/Checkout/Services/AbandonedCheckoutDetectionJob.cs), [AbandonedCheckoutSettings.cs](../../../src/Merchello.Core/Checkout/AbandonedCheckoutSettings.cs), [CheckoutAbandonedNotificationBase.cs](../../../src/Merchello.Core/Notifications/CheckoutNotifications/CheckoutAbandonedNotificationBase.cs).
Source: [AbandonedCheckoutDetectionJob.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/AbandonedCheckoutDetectionJob.cs), [AbandonedCheckoutSettings.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/AbandonedCheckoutSettings.cs), [CheckoutAbandonedNotificationBase.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Notifications/CheckoutNotifications/CheckoutAbandonedNotificationBase.cs).

## How It Works

Expand Down Expand Up @@ -162,7 +162,7 @@ Subscribe to webhook topics to trigger external platforms (Klaviyo, Mailchimp, e

## Notification Properties

All recovery email notifications inherit from [`CheckoutAbandonedNotificationBase`](../../../src/Merchello.Core/Notifications/CheckoutNotifications/CheckoutAbandonedNotificationBase.cs):
All recovery email notifications inherit from [`CheckoutAbandonedNotificationBase`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Notifications/CheckoutNotifications/CheckoutAbandonedNotificationBase.cs):

| Property | Type | Description |
|----------|------|-------------|
Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/checkout/checkout-addresses.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The checkout address step captures the customer's billing and shipping addresses

**Why it matters:** Saving the address also triggers a basket recalculation because tax rates, shipping options, and some automatic discounts depend on the shipping destination. This is not just a data save — it is the entry point for location-dependent checkout state.

Source: [CheckoutApiController.SaveAddresses](../../../src/Merchello/Controllers/CheckoutApiController.cs), [ICheckoutService.SaveAddressesAsync](../../../src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutService.cs), [ICheckoutValidator.cs](../../../src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutValidator.cs).
Source: [CheckoutApiController.SaveAddresses](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/CheckoutApiController.cs), [ICheckoutService.SaveAddressesAsync](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutService.cs), [ICheckoutValidator.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutValidator.cs).

## Saving Addresses

Expand Down
6 changes: 3 additions & 3 deletions docs/documentation/checkout/checkout-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Complete REST API reference for all checkout endpoints. These are public-facing

All checkout endpoints are prefixed with `/api/merchello/checkout` and accept JSON request bodies. The surface is split across two controllers that share the route prefix:

- [`CheckoutApiController`](../../../src/Merchello/Controllers/CheckoutApiController.cs) — basket, countries, addresses, shipping, discounts, auth, address lookup, recovery.
- [`CheckoutPaymentsApiController`](../../../src/Merchello/Controllers/CheckoutPaymentsApiController.cs) — payment methods, payment session creation, express checkout, widget orders, provider returns.
- [`CheckoutApiController`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/CheckoutApiController.cs) — basket, countries, addresses, shipping, discounts, auth, address lookup, recovery.
- [`CheckoutPaymentsApiController`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/CheckoutPaymentsApiController.cs) — payment methods, payment session creation, express checkout, widget orders, provider returns.

> **Invariant:** Controllers do no business logic. They validate input, delegate to `ICheckoutService` / `ICheckoutPaymentsOrchestrationService`, and map the result. Basket totals are always produced by `CheckoutService.CalculateBasketAsync()`.

Expand All @@ -21,7 +21,7 @@ Retrieves the current basket with formatted totals in the customer's display cur
GET /api/merchello/checkout/basket
```

**Response** `200 OK` -- [`CheckoutBasketDto`](../../../src/Merchello.Core/Checkout/Dtos/CheckoutBasketDto.cs)
**Response** `200 OK` -- [`CheckoutBasketDto`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Dtos/CheckoutBasketDto.cs)

The DTO carries **both** store-currency amounts (for calculation reconciliation) and display-currency amounts (for rendering). Pre-formatted strings are included so views never need to format money themselves.

Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/checkout/checkout-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Customer authentication during checkout -- how Merchello handles email checks, s

**Why it exists:** Standard ecommerce flows need all three of guest / sign-in / sign-up from one screen, plus a password-reset path that doesn't dump users back to a generic Umbraco login page. Merchello keeps these inside the checkout UI so conversion isn't broken by authentication detours.

Source: [ICheckoutMemberService.cs](../../../src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutMemberService.cs), [CheckoutApiController.cs](../../../src/Merchello/Controllers/CheckoutApiController.cs), [CheckoutPasswordResetController.cs](../../../src/Merchello/Controllers/CheckoutPasswordResetController.cs).
Source: [ICheckoutMemberService.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutMemberService.cs), [CheckoutApiController.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/CheckoutApiController.cs), [CheckoutPasswordResetController.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/CheckoutPasswordResetController.cs).

## Overview

Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/checkout/checkout-discounts.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Checkout Discounts

Merchello supports three types of discounts during checkout: manual discount codes, automatic discounts, and Google auto-discounts. All discount logic is handled by [`ICheckoutDiscountService`](../../../src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutDiscountService.cs).
Merchello supports three types of discounts during checkout: manual discount codes, automatic discounts, and Google auto-discounts. All discount logic is handled by [`ICheckoutDiscountService`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutDiscountService.cs).

**What it is:** The checkout-side façade over the discount engine. It knows how to apply, remove, and refresh discounts against a basket while keeping totals in sync.

Expand Down
12 changes: 6 additions & 6 deletions docs/documentation/checkout/checkout-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ Merchello provides a built-in, Shopify-style checkout experience. It is a consis

| Layer | Responsibility | Source |
|-------|----------------|--------|
| `CheckoutContentFinder` | Resolves `/checkout/*` URLs into a virtual page | [CheckoutContentFinder.cs](../../../src/Merchello/Routing/CheckoutContentFinder.cs) |
| `MerchelloCheckoutController` | Razor-hijacked controller that renders each step | [MerchelloCheckoutController.cs](../../../src/Merchello/Controllers/MerchelloCheckoutController.cs) |
| `CheckoutApiController` | Public AJAX surface at `/api/merchello/checkout` | [CheckoutApiController.cs](../../../src/Merchello/Controllers/CheckoutApiController.cs) |
| `CheckoutPaymentsApiController` | Payment/express-checkout endpoints on the same base path | [CheckoutPaymentsApiController.cs](../../../src/Merchello/Controllers/CheckoutPaymentsApiController.cs) |
| `ICheckoutService` | Business logic: basket math, order grouping, address/shipping saves | [ICheckoutService.cs](../../../src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutService.cs) |
| `ICheckoutSessionService` | Per-basket session state (step, addresses, shipping selections, invoice id) | [ICheckoutSessionService.cs](../../../src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutSessionService.cs) |
| `CheckoutContentFinder` | Resolves `/checkout/*` URLs into a virtual page | [CheckoutContentFinder.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Routing/CheckoutContentFinder.cs) |
| `MerchelloCheckoutController` | Razor-hijacked controller that renders each step | [MerchelloCheckoutController.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/MerchelloCheckoutController.cs) |
| `CheckoutApiController` | Public AJAX surface at `/api/merchello/checkout` | [CheckoutApiController.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/CheckoutApiController.cs) |
| `CheckoutPaymentsApiController` | Payment/express-checkout endpoints on the same base path | [CheckoutPaymentsApiController.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/CheckoutPaymentsApiController.cs) |
| `ICheckoutService` | Business logic: basket math, order grouping, address/shipping saves | [ICheckoutService.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutService.cs) |
| `ICheckoutSessionService` | Per-basket session state (step, addresses, shipping selections, invoice id) | [ICheckoutSessionService.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutSessionService.cs) |

> **Invariant:** Controllers never touch `DbContext`. All persistence and calculation flows through services. Basket totals are always computed by `CheckoutService.CalculateBasketAsync()` — never in views, controllers, or JS.

Expand Down
6 changes: 3 additions & 3 deletions docs/documentation/checkout/checkout-razor.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Merchello ships with a Shopify-style checkout rendered via Razor views. This gui

## How Checkout Rendering Works

[`CheckoutContentFinder`](../../../src/Merchello/Routing/CheckoutContentFinder.cs) intercepts any `/checkout/*` URL, parses the step and optional invoice ID from the path, and sets a virtual `IPublishedContent` (`MerchelloCheckoutPage`) whose content type alias is `"MerchelloCheckout"`. Umbraco's route hijacking then resolves [`MerchelloCheckoutController`](../../../src/Merchello/Controllers/MerchelloCheckoutController.cs) by controller-name convention and its `Index(CancellationToken)` action renders the Razor view for the current step.
[`CheckoutContentFinder`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Routing/CheckoutContentFinder.cs) intercepts any `/checkout/*` URL, parses the step and optional invoice ID from the path, and sets a virtual `IPublishedContent` (`MerchelloCheckoutPage`) whose content type alias is `"MerchelloCheckout"`. Umbraco's route hijacking then resolves [`MerchelloCheckoutController`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello/Controllers/MerchelloCheckoutController.cs) by controller-name convention and its `Index(CancellationToken)` action renders the Razor view for the current step.

You do not create Umbraco content nodes for checkout pages — the virtual content is generated from the URL.

Expand Down Expand Up @@ -157,8 +157,8 @@ This lets you build the confirmation page however you want while keeping the res

Branding is split across two configuration sections:

- `Merchello:Store` binds [`StoreSettings`](../../../src/Merchello.Core/Shared/Models/StoreSettings.cs) — things that are the store's identity (name, **logo URL**, contact details).
- `Merchello:Checkout` binds [`CheckoutSettings`](../../../src/Merchello.Core/Checkout/Models/CheckoutSettings.cs) — checkout-specific colours, typography, and behaviour.
- `Merchello:Store` binds [`StoreSettings`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Shared/Models/StoreSettings.cs) — things that are the store's identity (name, **logo URL**, contact details).
- `Merchello:Checkout` binds [`CheckoutSettings`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Models/CheckoutSettings.cs) — checkout-specific colours, typography, and behaviour.

```json
{
Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/checkout/checkout-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The checkout session is per-basket state that tracks where the customer is in th

**Why it exists:** The basket stores the latest-saved addresses and totals, but the session tracks checkout-specific signals the basket doesn't know about — like which shipping options the customer picked, what rate they were quoted, and which invoice they're paying for. It also enforces security by binding the active invoice to the session cookie so someone with a stolen URL can't pay for someone else's order.

Source: [CheckoutSession.cs](../../../src/Merchello.Core/Checkout/Models/CheckoutSession.cs), [CheckoutSessionService.cs](../../../src/Merchello.Core/Checkout/Services/CheckoutSessionService.cs)
Source: [CheckoutSession.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Models/CheckoutSession.cs), [CheckoutSessionService.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/CheckoutSessionService.cs)

## What the Session Stores

Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/checkout/checkout-shipping.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ During checkout the customer selects a shipping method for each order group. Thi

**Why you need to understand it:** Every basket is at least one order group (one warehouse). Multi-warehouse or vendor baskets produce multiple groups and the customer must pick shipping for each one. Getting the order-grouping, selection-key, and quoted-cost contracts right is what makes multi-warehouse and live-rate carrier checkouts work.

Source: [ICheckoutService.cs](../../../src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutService.cs), [DefaultOrderGroupingStrategy.cs](../../../src/Merchello.Core/Checkout/Strategies/DefaultOrderGroupingStrategy.cs), [OrderGroup.cs](../../../src/Merchello.Core/Checkout/Strategies/Models/OrderGroup.cs).
Source: [ICheckoutService.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Services/Interfaces/ICheckoutService.cs), [DefaultOrderGroupingStrategy.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Strategies/DefaultOrderGroupingStrategy.cs), [OrderGroup.cs](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Checkout/Strategies/Models/OrderGroup.cs).

## Order Grouping

Expand Down
4 changes: 2 additions & 2 deletions docs/documentation/customers/customer-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Merchello customers are created automatically during checkout -- you do not need

## How It Works

At checkout, `ICustomerService.GetOrCreateByEmailAsync(...)` ([ICustomerService.cs:61](../../../src/Merchello.Core/Customers/Services/Interfaces/ICustomerService.cs#L61)) checks for an existing customer record matching the billing email address. If one exists, the invoice is linked to it. If not, a new customer record is created from the billing address details.
At checkout, `ICustomerService.GetOrCreateByEmailAsync(...)` ([ICustomerService.cs:61](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Customers/Services/Interfaces/ICustomerService.cs#L61)) checks for an existing customer record matching the billing email address. If one exists, the invoice is linked to it. If not, a new customer record is created from the billing address details.

This means guest checkout works out of the box. Returning guests are matched by email to their previous orders automatically without needing to sign in.

Expand All @@ -19,7 +19,7 @@ Merchello `Customer` and Umbraco `Member` are separate concepts:

## Accessing Customer Data in Storefront Code

Inject [`ICustomerService`](../../../src/Merchello.Core/Customers/Services/Interfaces/ICustomerService.cs) to look up customers from Razor controllers or view components:
Inject [`ICustomerService`](https://github.com/YodasMyDad/Merchello/blob/main/src/Merchello.Core/Customers/Services/Interfaces/ICustomerService.cs) to look up customers from Razor controllers or view components:

```csharp
public class AccountController(ICustomerService customerService)
Expand Down
Loading
Loading