diff --git a/backend/app/domains/products/serialize.py b/backend/app/domains/products/serialize.py index 47b2eaf..e3ca683 100644 --- a/backend/app/domains/products/serialize.py +++ b/backend/app/domains/products/serialize.py @@ -71,7 +71,9 @@ def _drain(buf: io.StringIO) -> str: def _product_rows(product: CatalogProduct) -> list[dict[str, str]]: - """The product's CSV rows (§6.5.1 grammar). Task 1: first variant only.""" + """The product's CSV rows (§6.5.1 grammar): product fields + option names on + the first row; one variant per row; images interleaved; image-only rows when + a product has more images than variants.""" base = {"Handle": product.handle, "Title": product.title} for field, col in _PRODUCT_FIELD_TO_COL.items(): if field in product.fields: @@ -79,10 +81,25 @@ def _product_rows(product: CatalogProduct) -> list[dict[str, str]]: for slot, name in enumerate(product.option_names, start=1): if name: base[f"Option{slot} Name"] = name - variant = product.variants[0] - row = dict(base) - _write_variant(row, product, variant) - return [row] + + count = max(len(product.variants), len(product.images)) + rows: list[dict[str, str]] = [] + for i in range(count): + # Row 0 carries the product-level fields; later rows carry only Handle. + row = dict(base) if i == 0 else {"Handle": product.handle} + if i < len(product.variants): + _write_variant(row, product, product.variants[i]) + if i < len(product.images): + _write_image(row, product.images[i]) + rows.append(row) + return rows + + +def _write_image(row: dict[str, str], image) -> None: + row["Image Src"] = image.source_url + row["Image Position"] = str(image.position) + if image.alt_text is not None: + row["Image Alt Text"] = image.alt_text def _write_variant(row: dict[str, str], product: CatalogProduct, variant) -> None: diff --git a/backend/tests/test_products_serialize.py b/backend/tests/test_products_serialize.py index 0c5fb47..6c715e8 100644 --- a/backend/tests/test_products_serialize.py +++ b/backend/tests/test_products_serialize.py @@ -51,3 +51,54 @@ def test_single_no_option_product_one_row(): assert rows[0]["Variant SKU"] == "WG-MUG" # A no-option product emits no option values. assert rows[0]["Option1 Value"] == "" + + +def _star_tee() -> CatalogProduct: + """A 3-variant, 2-image product (the sample.csv shape).""" + return CatalogProduct( + id=2, handle="star-tee", title="Star Tee", + option_names=("Size", "Color", None), + fields={"title": "Star Tee", "description_html": None, "vendor": "Acme", + "product_type": "standalone", "google_product_category": None, + "tags": ["apparel", "tees"], "status": "active", "published": True}, + variants=[ + CatalogVariant(id=10, options=("S", "Indigo", None), position=1, + fields={"sku": "WG-TEE-S", "variant_image": None}), + CatalogVariant(id=11, options=("M", "Indigo", None), position=2, + fields={"sku": "WG-TEE-M", "variant_image": None}), + CatalogVariant(id=12, options=("L", "Indigo", None), position=3, + fields={"sku": "WG-TEE-L", "variant_image": None}), + ], + images=[ + CatalogImage(id=1, source_url="https://x/a.jpg", position=1, alt_text="front"), + CatalogImage(id=2, source_url="https://x/b.jpg", position=2, alt_text="back"), + ], + ) + + +def test_multivariant_with_images_interleaves(): + rows = _rows([_star_tee()]) + assert len(rows) == 3 # max(3 variants, 2 images) + # Product-level fields only on the first row. + assert rows[0]["Title"] == "Star Tee" and rows[1]["Title"] == "" + assert rows[0]["Tags"] == "apparel, tees" and rows[1]["Tags"] == "" + # Option names on row 0; option values on every variant row. + assert rows[0]["Option1 Name"] == "Size" and rows[1]["Option1 Name"] == "" + assert [r["Option1 Value"] for r in rows] == ["S", "M", "L"] + assert [r["Variant SKU"] for r in rows] == ["WG-TEE-S", "WG-TEE-M", "WG-TEE-L"] + # Two images on the first two rows; third row has no image. + assert [r["Image Src"] for r in rows] == ["https://x/a.jpg", "https://x/b.jpg", ""] + assert [r["Image Position"] for r in rows] == ["1", "2", ""] + + +def test_more_images_than_variants_emits_image_only_rows(): + p = _product(images=[ + CatalogImage(id=1, source_url="https://x/a.jpg", position=1, alt_text=None), + CatalogImage(id=2, source_url="https://x/b.jpg", position=2, alt_text=None), + CatalogImage(id=3, source_url="https://x/c.jpg", position=3, alt_text=None), + ]) + rows = _rows([p]) + assert len(rows) == 3 # 1 variant, 3 images + assert rows[0]["Variant SKU"] == "WG-MUG" + assert rows[1]["Variant SKU"] == "" and rows[1]["Handle"] == "moon-mug" + assert [r["Image Src"] for r in rows] == ["https://x/a.jpg", "https://x/b.jpg", "https://x/c.jpg"]