add provider sync enhancements for product lifecycle

- add discontinued status to products (soft-delete when removed from provider)
- add availability helpers to variants (available/out_of_stock/discontinued)
- add detailed sync audit logging (product created/updated/discontinued)
- add cost change detection with threshold alerts (5% warning, 20% critical)
- update cart to show unavailable items with appropriate messaging
- block checkout when cart contains unavailable items
- show discontinued badge on product pages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-29 18:49:55 +01:00
parent dd7146cb41
commit d3fe6f4b56
10 changed files with 483 additions and 11 deletions

View File

@@ -45,10 +45,18 @@ defmodule BerrypodWeb.ShopComponents.Cart do
attr :stripe_connected, :boolean, default: true
def cart_drawer(assigns) do
# Check if any cart items are unavailable (out of stock or discontinued)
has_unavailable_items =
Enum.any?(assigns.cart_items, fn item ->
Map.get(item, :product_discontinued) == true or Map.get(item, :is_available) == false
end)
assigns =
assign_new(assigns, :display_total, fn ->
assigns
|> assign_new(:display_total, fn ->
assigns.total || assigns.subtotal || "£0.00"
end)
|> assign(:has_unavailable_items, has_unavailable_items)
~H"""
<%!-- Screen reader announcements for cart changes --%>
@@ -142,6 +150,13 @@ defmodule BerrypodWeb.ShopComponents.Cart do
Checkout
</button>
<p class="cart-drawer-notice">Checkout isn't available yet.</p>
<% @has_unavailable_items -> %>
<button type="button" disabled class="cart-drawer-checkout">
Checkout
</button>
<p class="cart-drawer-notice">
Remove unavailable items to checkout.
</p>
<% true -> %>
<form action="/checkout" method="post">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
@@ -212,8 +227,14 @@ defmodule BerrypodWeb.ShopComponents.Cart do
</p>
<% end %>
<p :if={Map.get(@item, :is_available) == false} class="cart-item-unavailable">
This item is currently unavailable
<p :if={Map.get(@item, :product_discontinued)} class="cart-item-unavailable">
This product is no longer available
</p>
<p
:if={!Map.get(@item, :product_discontinued) && Map.get(@item, :is_available) == false}
class="cart-item-unavailable"
>
This option is currently out of stock
</p>
<div class="cart-item-actions">
@@ -457,6 +478,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
attr :available_countries, :list, default: []
attr :mode, :atom, default: :live
attr :stripe_connected, :boolean, default: true
attr :has_unavailable_items, :boolean, default: false
def order_summary(assigns) do
assigns =
@@ -516,6 +538,17 @@ defmodule BerrypodWeb.ShopComponents.Cart do
>
Continue shopping
</.shop_link_outline>
<% @has_unavailable_items -> %>
<.shop_button disabled class="order-summary-checkout">
Checkout
</.shop_button>
<p class="order-summary-notice">Remove unavailable items to checkout.</p>
<.shop_link_outline
href="/collections/all"
class="order-summary-continue"
>
Continue shopping
</.shop_link_outline>
<% true -> %>
<form action="/checkout" method="post" class="order-summary-checkout-form">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />

View File

@@ -300,6 +300,8 @@ defmodule BerrypodWeb.ShopComponents.Product do
defp product_badge(assigns) do
~H"""
<%= cond do %>
<% Map.get(@product, :status) == "discontinued" -> %>
<span class="product-badge badge-discontinued">Discontinued</span>
<% Map.get(@product, :in_stock, true) == false -> %>
<span class="product-badge badge-sold-out">Sold out</span>
<% Map.get(@product, :is_new) -> %>