|
39 | 39 | EbayMediaThroughProduct, |
40 | 40 | EbayPrice, |
41 | 41 | EbayProductContent, |
42 | | - EbayEanCode, EbayProductProperty, EbayProductCategory, |
| 42 | + EbayEanCode, EbayProductProperty, EbayProductCategory, EbayStoreCategory, EbayProductStoreCategory, |
43 | 43 | ) |
44 | 44 | from sales_channels.models import SalesChannelIntegrationPricelist, SalesChannelViewAssign |
45 | 45 | from sales_prices.models import SalesPrice |
@@ -1416,6 +1416,10 @@ def _extract_listing_details(*, payload: Mapping[str, Any] | None) -> tuple[str |
1416 | 1416 | view=resolved_view, |
1417 | 1417 | offer_payloads=offer_payloads, |
1418 | 1418 | ) |
| 1419 | + self._sync_product_store_categories( |
| 1420 | + product=local_product, |
| 1421 | + offer_payloads=offer_payloads, |
| 1422 | + ) |
1419 | 1423 |
|
1420 | 1424 | def _ensure_product_offer( |
1421 | 1425 | self, |
@@ -1551,6 +1555,145 @@ def _sync_product_category( |
1551 | 1555 | exc=exc, |
1552 | 1556 | ) |
1553 | 1557 |
|
| 1558 | + def _extract_offer_store_category_names( |
| 1559 | + self, |
| 1560 | + *offer_payloads: Mapping[str, Any] | None, |
| 1561 | + ) -> list[str] | None: |
| 1562 | + """Return normalized store category paths when explicitly declared by the offer payload.""" |
| 1563 | + |
| 1564 | + for payload in offer_payloads: |
| 1565 | + if not isinstance(payload, Mapping): |
| 1566 | + continue |
| 1567 | + |
| 1568 | + for key in ("store_category_names", "storeCategoryNames"): |
| 1569 | + if key not in payload: |
| 1570 | + continue |
| 1571 | + |
| 1572 | + raw_values = payload.get(key) |
| 1573 | + if isinstance(raw_values, str): |
| 1574 | + source_values = [raw_values] |
| 1575 | + elif isinstance(raw_values, list): |
| 1576 | + source_values = raw_values |
| 1577 | + else: |
| 1578 | + return [] |
| 1579 | + |
| 1580 | + normalized_values: list[str] = [] |
| 1581 | + for raw_value in source_values: |
| 1582 | + if raw_value is None: |
| 1583 | + continue |
| 1584 | + normalized = str(raw_value).strip() |
| 1585 | + if normalized: |
| 1586 | + normalized_values.append(normalized) |
| 1587 | + |
| 1588 | + return normalized_values |
| 1589 | + |
| 1590 | + return None |
| 1591 | + |
| 1592 | + def _resolve_store_category_leaf_by_full_path( |
| 1593 | + self, |
| 1594 | + *, |
| 1595 | + full_path: str, |
| 1596 | + ) -> EbayStoreCategory | None: |
| 1597 | + """Resolve a store-category full path and return the leaf category when it exists.""" |
| 1598 | + |
| 1599 | + segments = [segment.strip() for segment in str(full_path).split("/") if segment and segment.strip()] |
| 1600 | + if not segments: |
| 1601 | + return None |
| 1602 | + |
| 1603 | + parent_id: int | None = None |
| 1604 | + current: EbayStoreCategory | None = None |
| 1605 | + last_index = len(segments) - 1 |
| 1606 | + |
| 1607 | + for index, segment in enumerate(segments): |
| 1608 | + queryset = EbayStoreCategory.objects.filter( |
| 1609 | + sales_channel=self.sales_channel, |
| 1610 | + parent_id=parent_id, |
| 1611 | + name__iexact=segment, |
| 1612 | + ) |
| 1613 | + |
| 1614 | + # The requested path must end on a leaf category. |
| 1615 | + if index == last_index: |
| 1616 | + queryset = queryset.filter(is_leaf=True) |
| 1617 | + |
| 1618 | + current = queryset.order_by("id").first() |
| 1619 | + if current is None: |
| 1620 | + return None |
| 1621 | + |
| 1622 | + parent_id = current.id |
| 1623 | + |
| 1624 | + return current |
| 1625 | + |
| 1626 | + def _sync_product_store_categories( |
| 1627 | + self, |
| 1628 | + *, |
| 1629 | + product: Any | None, |
| 1630 | + offer_payloads: tuple[Mapping[str, Any] | None, ...], |
| 1631 | + ) -> None: |
| 1632 | + """Create/update product store-category mapping from offer storeCategoryNames.""" |
| 1633 | + |
| 1634 | + if product is None: |
| 1635 | + return |
| 1636 | + |
| 1637 | + store_category_names = self._extract_offer_store_category_names(*offer_payloads) |
| 1638 | + if store_category_names is None: |
| 1639 | + return |
| 1640 | + |
| 1641 | + resolved_categories: list[EbayStoreCategory] = [] |
| 1642 | + seen_ids: set[int] = set() |
| 1643 | + for full_path in store_category_names: |
| 1644 | + category = self._resolve_store_category_leaf_by_full_path(full_path=full_path) |
| 1645 | + if category is None or category.id in seen_ids: |
| 1646 | + continue |
| 1647 | + resolved_categories.append(category) |
| 1648 | + seen_ids.add(category.id) |
| 1649 | + if len(resolved_categories) >= 2: |
| 1650 | + break |
| 1651 | + |
| 1652 | + if not resolved_categories: |
| 1653 | + return |
| 1654 | + |
| 1655 | + primary_category = resolved_categories[0] |
| 1656 | + secondary_category = resolved_categories[1] if len(resolved_categories) > 1 else None |
| 1657 | + mapping = ( |
| 1658 | + EbayProductStoreCategory.objects.filter( |
| 1659 | + product=product, |
| 1660 | + sales_channel=self.sales_channel, |
| 1661 | + ) |
| 1662 | + .order_by("id") |
| 1663 | + .first() |
| 1664 | + ) |
| 1665 | + |
| 1666 | + try: |
| 1667 | + if mapping is None: |
| 1668 | + mapping = EbayProductStoreCategory.objects.create( |
| 1669 | + multi_tenant_company=self.multi_tenant_company, |
| 1670 | + product=product, |
| 1671 | + sales_channel=self.sales_channel, |
| 1672 | + primary_store_category=primary_category, |
| 1673 | + secondary_store_category=secondary_category, |
| 1674 | + ) |
| 1675 | + else: |
| 1676 | + update_fields: list[str] = [] |
| 1677 | + if mapping.primary_store_category_id != primary_category.id: |
| 1678 | + mapping.primary_store_category = primary_category |
| 1679 | + update_fields.append("primary_store_category") |
| 1680 | + desired_secondary_id = secondary_category.id if secondary_category else None |
| 1681 | + if mapping.secondary_store_category_id != desired_secondary_id: |
| 1682 | + mapping.secondary_store_category = secondary_category |
| 1683 | + update_fields.append("secondary_store_category") |
| 1684 | + if mapping.sales_channel_id != self.sales_channel.id: |
| 1685 | + mapping.sales_channel = self.sales_channel |
| 1686 | + update_fields.append("sales_channel") |
| 1687 | + if update_fields: |
| 1688 | + mapping.save(update_fields=update_fields) |
| 1689 | + except ValidationError: |
| 1690 | + return |
| 1691 | + |
| 1692 | + EbayProductStoreCategory.objects.filter( |
| 1693 | + product=product, |
| 1694 | + sales_channel=self.sales_channel, |
| 1695 | + ).exclude(id=mapping.id).delete() |
| 1696 | + |
1554 | 1697 | def update_remote_product( |
1555 | 1698 | self, |
1556 | 1699 | import_instance: Any, |
|
0 commit comments