- 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>
7.1 KiB
Provider Sync Enhancements
Status: Planned Tier: 3.5 (Business tools)
Small improvements to provider sync for better visibility and resilience. Not versioning products (overkill), just better audit trail and handling of edge cases.
Overview
Three enhancements to the provider sync system:
- Detailed audit logging — Field-level change tracking, not just "3 updated"
- Soft-delete for discontinued products — Don't break carts/orders/links
- Variant-level availability — Handle out of stock / discontinued variants
- Price change alerts — Warn when margins might be affected
1. Detailed sync audit logging
Currently sync.completed logs "47 products, 3 updated" but doesn't capture what changed. Enhance to log field-level changes.
Implementation:
# In product sync, before update
def sync_product(product, provider_data) do
changes = diff_product(product, provider_data)
if changes != %{} do
ActivityLog.log_event("sync.product_updated",
"#{product.title}: #{summarize_changes(changes)}",
payload: %{product_id: product.id, changes: changes}
)
end
update_product(product, provider_data)
end
defp diff_product(current, new) do
fields = [:title, :description, :base_price, :status]
Enum.reduce(fields, %{}, fn field, acc ->
old_val = Map.get(current, field)
new_val = Map.get(new, field)
if old_val != new_val, do: Map.put(acc, field, {old_val, new_val}), else: acc
end)
end
defp summarize_changes(changes) do
changes
|> Map.keys()
|> Enum.map(&to_string/1)
|> Enum.join(", ")
# => "title, base_price"
end
Activity log entries:
sync.product_updated— "Awesome T-Shirt: title, base_price changed"sync.product_added— "New product: Cool Hoodie"sync.product_removed— "Product removed: Old Mug" (if provider removes it)
2. Soft-delete for discontinued products
When a product disappears from the provider catalogue (sync returns 404 or product not in list), don't hard-delete it. Set status: "discontinued" instead.
Why:
- Existing carts might have this product
- Order history references it
- Links from external sites won't 404 immediately
Implementation:
# products schema
field :status, :string, default: "active" # "active", "discontinued", "draft"
# In sync
def handle_missing_product(product) do
Products.update_product(product, %{status: "discontinued"})
ActivityLog.log_event("sync.product_discontinued",
"#{product.title} no longer available from provider",
level: "warning",
payload: %{product_id: product.id}
)
end
UI behaviour:
- Discontinued products hidden from shop browse/search
- Product page shows "This product is no longer available"
- Cart shows warning if item is discontinued
- Admin can see discontinued products in a filtered view
2b. Variant-level availability
Variants can go out of stock or be discontinued independently of the product. This matters because:
- A customer may have "Blue / Large" in their cart when that specific variant becomes unavailable
- The product is still active, just that combination isn't
Variant statuses:
# variants schema
field :availability, :string, default: "available"
# "available", "out_of_stock", "discontinued"
Sync handling:
def sync_variant(variant, provider_data) do
new_availability = case provider_data do
%{is_available: false, is_enabled: false} -> "discontinued"
%{is_available: false} -> "out_of_stock"
%{is_available: true} -> "available"
end
if variant.availability != new_availability do
Products.update_variant(variant, %{availability: new_availability})
if new_availability in ["out_of_stock", "discontinued"] do
ActivityLog.log_event("sync.variant_unavailable",
"#{product.title} (#{variant.title}): now #{new_availability}",
level: "warning",
payload: %{variant_id: variant.id, product_id: variant.product_id}
)
end
end
end
Cart impact:
- On cart load: check all variants still available
- If out of stock: show warning "This size/colour is currently out of stock"
- If discontinued: show warning "This option is no longer available"
- Prevent checkout if cart contains unavailable variants (or offer to remove them)
Product page:
- Out of stock variants: show crossed out or greyed, not selectable
- Discontinued variants: hide entirely from selector
- If all variants unavailable: show "Currently unavailable" on product
3. Price change alerts
Log a warning when a product's cost basis changes significantly (>5% or configurable threshold). Helps shop owner know when their margins might be affected.
Implementation:
def check_price_change(product, new_cost) do
old_cost = product.base_cost
return if old_cost == nil or old_cost == 0
change_pct = abs((new_cost - old_cost) / old_cost * 100)
if change_pct > 5 do
ActivityLog.log_event("sync.price_change",
"#{product.title}: cost changed from £#{old_cost} to £#{new_cost} (#{round(change_pct)}%)",
level: if(change_pct > 20, do: "warning", else: "info"),
payload: %{product_id: product.id, old_cost: old_cost, new_cost: new_cost, change_pct: change_pct}
)
end
end
Files to Modify
| File | Changes |
|---|---|
lib/berrypod/products/product.ex |
Add status field if not present |
lib/berrypod/products/variant.ex |
Add availability field |
lib/berrypod/providers/printify.ex |
Add diff logic, price change check, soft-delete handling, variant availability sync |
lib/berrypod/providers/printful.ex |
Same as above |
lib/berrypod/products.ex |
Query helpers for discontinued products/variants |
lib/berrypod_web/live/shop/pages/product_page.ex |
Handle discontinued status, variant availability display |
lib/berrypod_web/components/shop_components/cart.ex |
Warn on discontinued/unavailable items |
lib/berrypod_web/components/shop_components/product.ex |
Grey out/hide unavailable variants in selector |
Tasks
| # | Task | Est |
|---|---|---|
| 1 | Add status field to products, availability field to variants |
30m |
| 2 | Product discontinued handling in sync (soft-delete) | 45m |
| 3 | Variant availability sync (out of stock / discontinued) | 45m |
| 4 | Detailed sync audit logging (field-level diff) | 1h |
| 5 | Price change alerts with threshold | 30m |
| 6 | Product page UI: discontinued products, variant availability display | 1h |
| 7 | Cart UI: warn on discontinued/unavailable items, prevent checkout | 1h |
| Total | 5.5h |
Verification
- Manually trigger a sync
- Check activity log shows field-level changes
- Remove a product from provider (or mock it) — verify soft-delete
- Change a product's cost — verify price change alert appears
- Try to add discontinued product to cart — verify warning
- Mark a variant as out of stock — verify greyed out on product page
- Mark a variant as discontinued — verify hidden from selector
- Add item to cart, then mark its variant unavailable — verify cart warning
- Try to checkout with unavailable variant — verify blocked or prompted to remove