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:
@@ -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()} />
|
||||
|
||||
@@ -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) -> %>
|
||||
|
||||
@@ -12,10 +12,15 @@ defmodule BerrypodWeb.Shop.Pages.Product do
|
||||
alias Berrypod.Products.{Product, ProductImage}
|
||||
|
||||
def init(socket, %{"id" => slug}, _uri) do
|
||||
case Products.get_visible_product(slug) do
|
||||
# Try to get product by slug, including discontinued products
|
||||
case Products.get_product_by_slug(slug, preload: [:variants, :images]) do
|
||||
nil ->
|
||||
{:noreply, push_navigate(socket, to: "/collections/all")}
|
||||
|
||||
%{visible: false, status: status} when status != "discontinued" ->
|
||||
# Hidden but not discontinued - redirect away
|
||||
{:noreply, push_navigate(socket, to: "/collections/all")}
|
||||
|
||||
product ->
|
||||
all_images =
|
||||
(product.images || [])
|
||||
@@ -46,6 +51,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
|
||||
og_image = og_image_url(all_images)
|
||||
|
||||
page = Pages.get_page("pdp")
|
||||
is_discontinued = product.status == "discontinued"
|
||||
|
||||
socket =
|
||||
socket
|
||||
@@ -61,6 +67,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
|
||||
|> assign(:option_types, option_types)
|
||||
|> assign(:variants, variants)
|
||||
|> assign(:page, page)
|
||||
|> assign(:product_discontinued, is_discontinued)
|
||||
|
||||
# Block data loaders (related_products, reviews) run after product is assigned
|
||||
extra = Pages.load_block_data(page.blocks, socket.assigns)
|
||||
@@ -89,8 +96,9 @@ defmodule BerrypodWeb.Shop.Pages.Product do
|
||||
|
||||
def handle_event("add_to_cart", _params, socket) do
|
||||
variant = socket.assigns.selected_variant
|
||||
is_discontinued = socket.assigns[:product_discontinued] || false
|
||||
|
||||
if variant && variant.is_available do
|
||||
if variant && variant.is_available && not is_discontinued do
|
||||
cart = Cart.add_item(socket.assigns.raw_cart, variant.id, socket.assigns.quantity)
|
||||
|
||||
if socket.assigns[:analytics_visitor_hash] do
|
||||
|
||||
Reference in New Issue
Block a user