fix(products): cleared Variant Position resolves to file order, not NULL
A blank Variant Position cell previewed position->null and aborted the confirm transaction (variant.position is NOT NULL). Resolve the clear to the variant's file order at diff time so preview and apply stay in lockstep; carry file_order on VariantPlan instead of an identity-keyed map; release the read snapshot before PreviewStale/NothingToApply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,9 @@ fingerprint over the records detect catalog drift between preview and confirm.
|
||||
Deliberately DB-free: the catalog snapshot dataclasses are defined here and the
|
||||
repo layer builds them. Only fields present in the file's canonical fields{}
|
||||
participate in a comparison — an absent column is untouched, never a change
|
||||
(§6.5.1); a present-but-empty cell resolves to the field's CLEAR_DEFAULTS entry.
|
||||
(§6.5.1); a present-but-empty cell resolves to the field's CLEAR_DEFAULTS entry
|
||||
— except a variant's position, whose default is the variant's 1-based file
|
||||
order within its product ("defaults to file order"), resolved here at diff time.
|
||||
Catalog variants/images absent from the file are likewise untouched (INV-10);
|
||||
file variants match catalog variants by their option-value combination (INV-13).
|
||||
"""
|
||||
@@ -69,6 +71,8 @@ class VariantPlan:
|
||||
kind: str # "add" | "update"
|
||||
canonical: CanonicalVariant
|
||||
catalog_id: int | None # None for add
|
||||
# 1-based index within the product's file variants — the position default.
|
||||
file_order: int = 0
|
||||
# field -> resolved after-value (update only); adds resolve from canonical.fields
|
||||
changes: dict[str, object] = field(default_factory=dict)
|
||||
|
||||
@@ -145,7 +149,10 @@ def _add_plan(product: CanonicalProduct) -> ProductPlan:
|
||||
kind="add",
|
||||
canonical=product,
|
||||
catalog=None,
|
||||
variant_plans=[VariantPlan(kind="add", canonical=v, catalog_id=None) for v in product.variants],
|
||||
variant_plans=[
|
||||
VariantPlan(kind="add", canonical=v, catalog_id=None, file_order=order)
|
||||
for order, v in enumerate(product.variants, start=1)
|
||||
],
|
||||
image_plans=[
|
||||
ImagePlan(kind="add", source_url=i.source_url, position=i.position,
|
||||
alt_text=i.alt_text, image_id=None)
|
||||
@@ -164,7 +171,8 @@ def _add_detail(plan: ProductPlan) -> dict:
|
||||
"set": {f: _json_safe(v) for f, v in set_fields.items()},
|
||||
"option_names": list(plan.canonical.option_names),
|
||||
"variants": [
|
||||
{"options": list(vp.canonical.options), "set": _resolved_variant_fields(vp.canonical)}
|
||||
{"options": list(vp.canonical.options),
|
||||
"set": _resolved_variant_fields(vp.canonical, vp.file_order)}
|
||||
for vp in plan.variant_plans
|
||||
],
|
||||
"images": [
|
||||
@@ -200,17 +208,20 @@ def _update_plan(product: CanonicalProduct, current: CatalogProduct) -> tuple[Pr
|
||||
variant_plans: list[VariantPlan] = []
|
||||
variant_entries: list[dict] = []
|
||||
by_options = {v.options: v for v in current.variants}
|
||||
for variant in product.variants:
|
||||
for file_order, variant in enumerate(product.variants, start=1):
|
||||
match = by_options.get(variant.options)
|
||||
if match is None:
|
||||
variant_plans.append(VariantPlan(kind="add", canonical=variant, catalog_id=None))
|
||||
variant_plans.append(
|
||||
VariantPlan(kind="add", canonical=variant, catalog_id=None, file_order=file_order)
|
||||
)
|
||||
variant_entries.append(
|
||||
{"options": list(variant.options), "kind": "add", "set": _resolved_variant_fields(variant)}
|
||||
{"options": list(variant.options), "kind": "add",
|
||||
"set": _resolved_variant_fields(variant, file_order)}
|
||||
)
|
||||
continue
|
||||
variant_changes: dict[str, object] = {}
|
||||
variant_change_entries: list[dict] = []
|
||||
for f, resolved in resolved_fields(variant.fields).items():
|
||||
for f, resolved in resolved_variant_fields(variant, file_order).items():
|
||||
# position lives on the catalog variant as an attribute, not in
|
||||
# fields{} — compare it explicitly (as images do for theirs).
|
||||
before = match.position if f == "position" else match.fields.get(f)
|
||||
@@ -219,7 +230,8 @@ def _update_plan(product: CanonicalProduct, current: CatalogProduct) -> tuple[Pr
|
||||
variant_change_entries.append(_change(f, before, resolved))
|
||||
if variant_changes:
|
||||
variant_plans.append(
|
||||
VariantPlan(kind="update", canonical=variant, catalog_id=match.id, changes=variant_changes)
|
||||
VariantPlan(kind="update", canonical=variant, catalog_id=match.id,
|
||||
file_order=file_order, changes=variant_changes)
|
||||
)
|
||||
variant_entries.append(
|
||||
{"options": list(variant.options), "kind": "update", "changes": variant_change_entries}
|
||||
@@ -284,8 +296,18 @@ def resolved_product_fields(product: CanonicalProduct) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def _resolved_variant_fields(variant: CanonicalVariant) -> dict[str, object]:
|
||||
return {f: _json_safe(v) for f, v in resolved_fields(variant.fields).items()}
|
||||
def resolved_variant_fields(variant: CanonicalVariant, file_order: int) -> dict[str, object]:
|
||||
"""File-present variant fields with clears resolved. A cleared position has
|
||||
no CLEAR_DEFAULTS entry — it resets to the variant's 1-based file order
|
||||
within its product ("defaults to file order"), never to NULL."""
|
||||
resolved = resolved_fields(variant.fields)
|
||||
if "position" in resolved and resolved["position"] is None:
|
||||
resolved["position"] = file_order
|
||||
return resolved
|
||||
|
||||
|
||||
def _resolved_variant_fields(variant: CanonicalVariant, file_order: int) -> dict[str, object]:
|
||||
return {f: _json_safe(v) for f, v in resolved_variant_fields(variant, file_order).items()}
|
||||
|
||||
|
||||
def _change(field_name: str, before: object, after: object) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user