@miravia_test/miravia-publish-skill 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/SKILL.md CHANGED
@@ -117,6 +117,7 @@ description: Miravia 商品发布 AI Skill。仅负责商品创建(手动一
117
117
  | `need_compliance` | 制造商/EU 负责人匹配失败 | 按返回候选选择或补充信息后重跑 |
118
118
  | `compliance_missing_suggest_draft` | 合规字段缺失(制造商/欧代/Safety Warning内容/合规文档) | 补充合规字段或加 `--draft` 发布为草稿 |
119
119
  | `gpsr_missing_suggest_draft` | GPSR 字段缺失(制造商和/或欧盟负责人为空) | 补充 GPSR 字段或加 `--draft` 发布为草稿 |
120
+ | `need_safety_warning_confirm` | AI/Excel 源检测到安全相关内容,但卖家未显式设置「该产品是否有安全警告」;系统**不会**静默默认为 No | 展示 `detected_safety_content` 证据与当前值,让卖家确认:确有则加 `--safety-warning yes`(必要时 `--safety-warning-content`)重跑;确无则加 `--safety-warning no` 确认;或在 Excel 补齐该列后重跑 |
120
121
  | `post_to_inactive_missing_suggest_draft` | 类目必填字段缺失(EAN) | 补充字段或加 `--draft` 发布为草稿 |
121
122
  | `special_category_block` | 特殊类目规则阻断(PPU 值无效 / 图书类目 ISBN 缺失) | 修正错误后重跑,不可转 Draft |
122
123
  | `special_category_suggest_draft` | 特殊类目字段缺失(PPU 缺失 / 食品属性缺失) | 补充字段或加 `--draft` 发布为草稿 |
package/dist/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.8
1
+ 0.1.9
@@ -220,6 +220,31 @@ def require_business_credentials() -> None:
220
220
  pass
221
221
 
222
222
 
223
+ def has_valid_business_credentials() -> bool:
224
+ """Return True when credentials are present and not expired.
225
+
226
+ Non-raising companion to ``require_business_credentials``. The publish
227
+ flow uses this to decide whether the seller is already authorized: when
228
+ valid credentials exist (they can only exist after a successful
229
+ ``auth_cli.py login``, which grants the read-profile / access-products /
230
+ update-products scopes), the skill must not re-prompt the three-item
231
+ authorization consent on every run. Returns False for missing or expired
232
+ credentials so the caller falls back to asking the seller.
233
+ """
234
+ if not (APP_KEY and APP_SECRET and ACCESS_TOKEN):
235
+ return False
236
+
237
+ if TOKEN_EXPIRES_AT:
238
+ try:
239
+ expires_at_ms = int(TOKEN_EXPIRES_AT)
240
+ if expires_at_ms > 0 and int(time.time() * 1000) > expires_at_ms - 60_000:
241
+ return False
242
+ except ValueError:
243
+ pass
244
+
245
+ return True
246
+
247
+
223
248
  def mask_token(token: str) -> str:
224
249
  if not token:
225
250
  return "<empty>"
@@ -218,6 +218,11 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
218
218
  "pub_compliance_manufacturer_eurp_both_empty": "Manufacturer and EU Responsible Person are both empty. Non-EU manufacturers require an EU Responsible Person.",
219
219
  "pub_compliance_eurp_missing_with_manufacturer": "Manufacturer is filled but EU Responsible Person is empty. Non-EU manufacturers require an EU Responsible Person.",
220
220
  "pub_safety_warning_content_missing": "Safety Warning is set to Yes but the warning content is empty. Please provide the safety warning content.",
221
+ "pub_safety_confirm_title": "Safety warning needs your confirmation",
222
+ "pub_safety_confirm_message": "We detected safety-related content in your source ({evidence}), but the 'Does this product have a safety warning' flag was not set explicitly. We will NOT silently default it to 'No'. Please confirm the correct value, or complete the field in your Excel and rerun.",
223
+ "pub_safety_confirm_next_yes": "If the product does have a safety warning, rerun with --safety-warning yes (add --safety-warning-content if needed).",
224
+ "pub_safety_confirm_next_no": "If it genuinely has no safety warning, rerun with --safety-warning no to confirm.",
225
+ "pub_safety_confirm_next_excel": "Your Excel fields may be incomplete; you can fill the safety column there and rerun.",
221
226
  "pub_compliance_docs_missing": "Compliance documents are required for this category but not provided.",
222
227
  "pub_compliance_missing_message": "Some compliance fields are missing. You can fill them or publish to Draft.",
223
228
  "pub_compliance_fill_or_draft": "Fill the missing compliance fields, or publish as Draft to complete them later.",
@@ -887,6 +892,11 @@ TRANSLATIONS: Dict[str, Dict[str, str]] = {
887
892
  "pub_compliance_manufacturer_eurp_both_empty": "制造商和欧盟负责人均为空。非欧盟制造商必须填写欧盟负责人。",
888
893
  "pub_compliance_eurp_missing_with_manufacturer": "已填写制造商但欧盟负责人为空。非欧盟制造商必须填写欧盟负责人。",
889
894
  "pub_safety_warning_content_missing": "安全警告已设为 Yes 但未填写警告内容。请补充安全警告内容。",
895
+ "pub_safety_confirm_title": "安全警告需要你确认",
896
+ "pub_safety_confirm_message": "我们从你的源文件中检测到安全相关内容({evidence}),但“该产品是否有安全警告”这一项没有被显式设置。我们不会把它静默默认为 No。请确认应取的值,或先在 Excel 中补全该项后重新运行。",
897
+ "pub_safety_confirm_next_yes": "如果该商品确实有安全警告,请加 --safety-warning yes 重新运行(必要时再加 --safety-warning-content)。",
898
+ "pub_safety_confirm_next_no": "如果确实没有安全警告,请加 --safety-warning no 确认。",
899
+ "pub_safety_confirm_next_excel": "Excel 字段可能不完整,你可以在其中补上安全警告列后重新运行。",
890
900
  "pub_compliance_docs_missing": "该类目要求上传合规文档但未提供。",
891
901
  "pub_compliance_missing_message": "部分合规字段缺失。您可以补充完整或发布为草稿。",
892
902
  "pub_compliance_fill_or_draft": "补充缺失的合规字段,或发布为草稿(Draft)稍后在 Seller Center 中完善。",
@@ -360,15 +360,14 @@ class ProductClient:
360
360
 
361
361
  # GPSR compliance fields are injected at SKU level (see below).
362
362
 
363
- # Delivery method -> Miravia create field ``Attributes.deliverySof``
364
- # ("DBM" or "DBS"). Only written when the seller explicitly chose one;
363
+ # Delivery method -> Miravia create field ``Attributes.delivery_option_sof``,
364
+ # which is a YES/NO flag: "Yes" = DBS (seller-fulfilled), "No" = DBM
365
+ # (platform-fulfilled). Only written when the seller explicitly chose one;
365
366
  # when absent, publish_module._check_dbm_limits applies the default
366
- # (DBM generally, DBS if the seller has DBS permission).
367
+ # (No/DBM generally, Yes/DBS if the seller has DBS permission).
367
368
  _dm = str(data.get("delivery_method") or "").strip().upper()
368
- if "DBS" in _dm:
369
- attributes["deliverySof"] = "DBS"
370
- elif "DBM" in _dm:
371
- attributes["deliverySof"] = "DBM"
369
+ if _dm in ("DBM", "DBS"):
370
+ attributes["delivery_option_sof"] = "Yes" if _dm == "DBS" else "No"
372
371
 
373
372
  product_payload: Dict[str, Any] = {
374
373
  "PrimaryCategory": category_id,
@@ -407,13 +406,14 @@ class ProductClient:
407
406
  if draft:
408
407
  product_payload["Status"] = "inactive"
409
408
 
410
- # Country-specific prices (best-effort mapping)
411
- country_prices = data.get("country_general_prices", {})
412
- if country_prices:
413
- product_payload["CountryPrice"] = [
414
- {"country": c, "price": str(p)}
415
- for c, p in country_prices.items()
416
- ]
409
+ # Country-specific prices are NOT injected here at the product level.
410
+ # Verified against the authoritative /product/create contract: each
411
+ # country override lives INSIDE the SKU as
412
+ # ``CountryPrice: [{"CountryCode":"ES","Price":"1.9","SalePrice":"1.8"}]``
413
+ # (capitalised keys). A product-level ``CountryPrice`` list with the
414
+ # old ``{"country","price"}`` shape is silently ignored by the API.
415
+ # This is built per SKU in
416
+ # ``PublishModule._build_sku_list_from_schema``.
417
417
 
418
418
  # Standard unit type / count
419
419
  if data.get("standard_unit_type"):
@@ -815,7 +815,8 @@ def main() -> None:
815
815
  print(json.dumps(auth_payload, indent=2, ensure_ascii=False))
816
816
  return
817
817
 
818
- if not skip_credential_check and args.scenario and not getattr(args, "authorized", False):
818
+ already_authorized = getattr(args, "authorized", False) or config.has_valid_business_credentials()
819
+ if not skip_credential_check and args.scenario and not already_authorized:
819
820
  auth_payload = {
820
821
  "status": "need_authorization",
821
822
  "title": i18n._("pub_authorization_title"),
@@ -801,32 +801,55 @@ class PublishModule:
801
801
  *,
802
802
  has_dbs_permission: bool = False,
803
803
  ) -> Optional[Dict[str, Any]]:
804
- """Resolve ``Attributes.deliverySof`` and enforce DBM package limits.
805
-
806
- Miravia ``/product/create`` expects ``Attributes.deliverySof`` to be
807
- ``"DBM"`` or ``"DBS"``. When the seller did not choose one it is
808
- defaulted here: generally DBM, but DBS when the seller has DBS
809
- permission. If the effective method is DBM but the package exceeds the
810
- DBM limits (weight <= 40KG, L+W+H <= 300cm, volumetric weight
811
- L*W*H/6000 <= 40), it is switched to DBS when permitted, otherwise the
812
- caller blocks. Returns a result dict, or None when nothing to do.
804
+ """Resolve ``Attributes.delivery_option_sof`` and enforce DBM package limits.
805
+
806
+ Miravia ``/product/create`` expects ``Attributes.delivery_option_sof`` to be
807
+ a YES/NO flag: ``"Yes"`` = DBS (seller-fulfilled), ``"No"`` = DBM
808
+ (platform-fulfilled). The legacy ``deliverySof="DBM"/"DBS"`` literal is
809
+ silently ignored by the platform. Internally we normalise to ``"DBM"``/
810
+ ``"DBS"`` for the package-limit logic and always write the flag back as
811
+ Yes/No. When the seller did not choose one it is defaulted here: generally
812
+ DBM (No), but DBS (Yes) when the seller has DBS permission. If the
813
+ effective method is DBM but the package exceeds the DBM limits (weight
814
+ <= 40KG, L+W+H <= 300cm, volumetric weight L*W*H/6000 <= 40), it is
815
+ switched to DBS when permitted, otherwise the caller blocks. Returns a
816
+ result dict, or None when nothing to do.
813
817
  """
814
818
  attrs = product_payload.setdefault("Attributes", {})
815
- delivery_method = str(attrs.get("deliverySof") or "").strip().upper()
819
+
820
+ def _write_flag(method: str) -> None:
821
+ # Canonical wire flag the platform actually honours; replaces the
822
+ # legacy deliverySof value so no stale key lingers.
823
+ attrs["delivery_option_sof"] = "Yes" if method == "DBS" else "No"
824
+ attrs.pop("deliverySof", None)
825
+
826
+ _raw = str(
827
+ attrs.get("delivery_option_sof") or attrs.get("deliverySof") or ""
828
+ ).strip().upper()
829
+ # Accept the wire flag (Yes/No) as well as legacy DBM/DBS codes.
830
+ if _raw in ("YES", "DBS"):
831
+ delivery_method = "DBS"
832
+ elif _raw in ("NO", "DBM"):
833
+ delivery_method = "DBM"
834
+ else:
835
+ delivery_method = ""
816
836
  downgraded_from_dbs = False
817
837
 
818
838
  if delivery_method not in ("DBM", "DBS"):
819
839
  # No explicit entry: default to DBS when the seller is permitted,
820
840
  # otherwise DBM.
821
841
  delivery_method = "DBS" if has_dbs_permission else "DBM"
822
- attrs["deliverySof"] = delivery_method
842
+ _write_flag(delivery_method)
823
843
  elif delivery_method == "DBS" and not has_dbs_permission:
824
844
  # Seller explicitly chose DBS but the account has no DBS permission:
825
845
  # never grant DBS without permission — fall back to DBM and still
826
846
  # enforce the DBM package limits below.
827
847
  delivery_method = "DBM"
828
- attrs["deliverySof"] = "DBM"
848
+ _write_flag(delivery_method)
829
849
  downgraded_from_dbs = True
850
+ else:
851
+ # Normalise an explicit/legacy value to the canonical Yes/No flag.
852
+ _write_flag(delivery_method)
830
853
 
831
854
  # Only DBM carries package limits.
832
855
  if delivery_method != "DBM":
@@ -874,7 +897,7 @@ class PublishModule:
874
897
  # Exceeded DBM limits — check DBS permission
875
898
  if has_dbs_permission:
876
899
  # Auto-switch to DBS
877
- attrs["deliverySof"] = "DBS"
900
+ _write_flag("DBS")
878
901
  return {
879
902
  "switched_to_dbs": True,
880
903
  "reasons": exceeded,
@@ -915,7 +938,9 @@ class PublishModule:
915
938
  seller_sku_required = self._field_is_required(
916
939
  "SellerSku", category_attributes or []
917
940
  )
918
- _validate_create_payload(product_payload, seller_sku_required=seller_sku_required)
941
+ _validate_create_payload(
942
+ product_payload, seller_sku_required=seller_sku_required, draft=draft
943
+ )
919
944
 
920
945
  # Check DBM limits — auto-switch to DBS if exceeded (requires DBS permission)
921
946
  has_dbs = self.product.get_shipping_permission()
@@ -1377,6 +1402,40 @@ class PublishModule:
1377
1402
  if mapping_check:
1378
1403
  return mapping_check
1379
1404
 
1405
+ # Safety warning: honor an explicit --safety-warning in the source flow.
1406
+ # build_create_product_payload defaults the flag to "No" and manual mode
1407
+ # applies the CLI flags at build time, but the Excel/source flow never did
1408
+ # — so --safety-warning was silently ignored here. Apply it now so the
1409
+ # seller's explicit choice is never overwritten by the default.
1410
+ self._apply_safety_cli_override(product_payload)
1411
+
1412
+ # Safety guidance gate: never silently ship an inferred "No". If the seller
1413
+ # has not chosen a value but the source carries real safety content, pause
1414
+ # and ask them to confirm yes/no (or complete the Excel). The flag can only
1415
+ # be auto-populated once the seller answers, per the "surface assumed
1416
+ # values, do not silently replace" rule.
1417
+ if getattr(self.args, "safety_warning", None) is None:
1418
+ safety_evidence = self._detect_safety_content(product_payload, schema)
1419
+ if safety_evidence:
1420
+ current_flag = (product_payload.get("Attributes") or {}).get(
1421
+ "Does_this_product_have_a_safety_warning"
1422
+ )
1423
+ return {
1424
+ "status": "need_safety_warning_confirm",
1425
+ "title": i18n._("pub_safety_confirm_title"),
1426
+ "message": i18n._(
1427
+ "pub_safety_confirm_message",
1428
+ evidence=", ".join(safety_evidence),
1429
+ ),
1430
+ "detected_safety_content": safety_evidence,
1431
+ "current_value": current_flag,
1432
+ "next_actions": [
1433
+ i18n._("pub_safety_confirm_next_yes"),
1434
+ i18n._("pub_safety_confirm_next_no"),
1435
+ i18n._("pub_safety_confirm_next_excel"),
1436
+ ],
1437
+ }
1438
+
1380
1439
  # Stage 3b: unified compliance check (GPSR + Safety Warning + compliance docs)
1381
1440
  draft_issues: list = []
1382
1441
  compliance_check = self._check_compliance_fields(
@@ -1537,8 +1596,8 @@ class PublishModule:
1537
1596
 
1538
1597
  # Structural fields Miravia always requires for any product (not category
1539
1598
  # attributes, so the API never governs them) — always blocking. NOTE:
1540
- # delivery method (Attributes.deliverySof) is intentionally NOT here — it is
1541
- # auto-defaulted (DBM, or DBS when the seller has DBS permission) rather
1599
+ # delivery method (Attributes.delivery_option_sof) is intentionally NOT here — it is
1600
+ # auto-defaulted (DBM=No, or DBS=Yes when the seller has DBS permission) rather
1542
1601
  # than hard-collected; see _check_dbm_limits.
1543
1602
  _CORE_MANDATORY_KEYS = frozenset(
1544
1603
  {
@@ -2048,8 +2107,8 @@ class PublishModule:
2048
2107
  # jeans, so an empty EAN must NOT be flagged as missing.
2049
2108
  api_optional: List[str] = []
2050
2109
 
2051
- # Delivery method (Attributes.deliverySof) is NOT a hard-collect field:
2052
- # Miravia defaults it when absent (DBM, or DBS if the seller has DBS
2110
+ # Delivery method (Attributes.delivery_option_sof) is NOT a hard-collect field:
2111
+ # Miravia defaults it when absent (DBM=No, or DBS=Yes if the seller has DBS
2053
2112
  # permission — see _check_dbm_limits), so an empty value shows as
2054
2113
  # "Optional" here and never blocks.
2055
2114
  if not (getattr(schema, "delivery_method", None) or "").strip():
@@ -2844,7 +2903,8 @@ class PublishModule:
2844
2903
  )
2845
2904
  # Do NOT hard-require SellerSku here. Its mandatory-ness comes from
2846
2905
  # the category attribute API and is enforced in _validate_create_payload
2847
- # (not relaxed by draft). Omit empty values so create can auto-generate.
2906
+ # (draft-aware: a live publish blocks an interface-mandatory empty value;
2907
+ # a draft defers it). Omit empty values so create can auto-generate.
2848
2908
  if seller_sku:
2849
2909
  sku["SellerSku"] = seller_sku
2850
2910
 
@@ -2892,6 +2952,25 @@ class PublishModule:
2892
2952
  if sku_images:
2893
2953
  sku["Images"] = {"Image": sku_images}
2894
2954
 
2955
+ # Country-specific prices live INSIDE each SKU (per the
2956
+ # authoritative /product/create contract), with capitalised keys
2957
+ # ``CountryCode`` / ``Price`` / ``SalePrice``. ``country_general_prices``
2958
+ # supplies the per-country Price and ``country_discounted_prices``
2959
+ # the per-country SalePrice. A product-level list or the old
2960
+ # lowercase ``{"country","price"}`` shape is silently ignored.
2961
+ _cgp = schema.country_general_prices or {}
2962
+ _cdp = schema.country_discounted_prices or {}
2963
+ _country_price = []
2964
+ for _cc in sorted(set(_cgp) | set(_cdp)):
2965
+ _entry: Dict[str, Any] = {"CountryCode": _cc}
2966
+ if _cgp.get(_cc) not in (None, ""):
2967
+ _entry["Price"] = str(_cgp[_cc])
2968
+ if _cdp.get(_cc) not in (None, ""):
2969
+ _entry["SalePrice"] = str(_cdp[_cc])
2970
+ _country_price.append(_entry)
2971
+ if _country_price:
2972
+ sku["CountryPrice"] = _country_price
2973
+
2895
2974
  skus.append(sku)
2896
2975
 
2897
2976
  if len(skus) > _MAX_SKUS_PER_PRODUCT:
@@ -2900,6 +2979,84 @@ class PublishModule:
2900
2979
  )
2901
2980
  return skus
2902
2981
 
2982
+ # Boilerplate default warning text — must NOT be treated as real safety content.
2983
+ _SAFETY_CONTENT_DEFAULT = "no specific safety warnings apply to this product"
2984
+
2985
+ def _apply_safety_cli_override(self, product_payload: Dict[str, Any]) -> None:
2986
+ """Honor ``--safety-warning`` / ``--safety-warning-content`` in the source flow.
2987
+
2988
+ ``build_create_product_payload`` defaults ``Does_this_product_have_a_safety_warning``
2989
+ to "No" when ``schema.safety_warning`` is falsy, and manual mode applies the CLI
2990
+ flags at build time (see the manual path). The Excel/source flow never applied
2991
+ them, so ``--safety-warning`` was silently ignored there. Write the seller's
2992
+ explicit choice here using the same "Yes"/"No" casing the client already emits,
2993
+ so the platform contract is unchanged and only the previously-ignored flag is
2994
+ now respected.
2995
+ """
2996
+ safety_arg = getattr(self.args, "safety_warning", None)
2997
+ if safety_arg is None:
2998
+ return
2999
+ is_yes = str(safety_arg).strip().lower() in ("yes", "1", "true", "y")
3000
+ attributes = product_payload.setdefault("Attributes", {})
3001
+ value = "Yes" if is_yes else "No"
3002
+ attributes["Does this product have a safety warning"] = value
3003
+ attributes["Does_this_product_have_a_safety_warning"] = value
3004
+ content = getattr(self.args, "safety_warning_content", None)
3005
+ if is_yes and content:
3006
+ attributes["Safety_warning_content"] = content
3007
+
3008
+ def _detect_safety_content(
3009
+ self, product_payload: Dict[str, Any], schema: MiraviaListingSchema
3010
+ ) -> List[str]:
3011
+ """Return human-readable evidence that the product has safety info.
3012
+
3013
+ Used to decide whether the auto-defaulted safety flag should be surfaced
3014
+ to the seller instead of shipped silently. Ignores the boilerplate default
3015
+ warning text and a plain "No" flag so we do not raise false prompts. Counts
3016
+ real safety artifacts that came from the source: safety documents
3017
+ (Safety_Date_Sheet / CPSR / CPNP URLs), an affirmative Hazmat flag, a
3018
+ non-default ``Safety_warning_content`` already present, or an explicit
3019
+ ``Does_this_product_have_a_safety_warning=Yes``.
3020
+ """
3021
+ attributes = product_payload.get("Attributes") or {}
3022
+ evidence: List[str] = []
3023
+ for key, val in attributes.items():
3024
+ if not isinstance(val, str) or not val.strip():
3025
+ continue
3026
+ low = key.lower()
3027
+ # Skip the yes/no flag itself (handled explicitly below).
3028
+ if low in (
3029
+ "does this product have a safety warning",
3030
+ "does_this_product_have_a_safety_warning",
3031
+ ):
3032
+ if val.strip().lower() == "yes":
3033
+ evidence.append(f"{key}=Yes")
3034
+ continue
3035
+ if not any(tok in low for tok in ("safety", "cpsr", "cpnp", "hazmat", "warning")):
3036
+ continue
3037
+ # Boilerplate default content is not evidence.
3038
+ if self._SAFETY_CONTENT_DEFAULT in val.strip().lower():
3039
+ continue
3040
+ # A negative/empty Hazmat flag is not evidence.
3041
+ if "hazmat" in low and val.strip().lower() in ("no", "false", "0", "n/a", "-"):
3042
+ continue
3043
+ # Null-like placeholder strings from empty cells are not evidence.
3044
+ if val.strip().lower() in ("none", "null", "n/a", "na", "-"):
3045
+ continue
3046
+ display = val if len(val) <= 60 else val[:57] + "..."
3047
+ evidence.append(f"{key}={display}")
3048
+ sc = getattr(schema, "safety_warning_content", None)
3049
+ if sc and self._SAFETY_CONTENT_DEFAULT not in str(sc).strip().lower():
3050
+ evidence.append("safety_warning_content provided")
3051
+ # Deduplicate while preserving order.
3052
+ seen: set = set()
3053
+ uniq: List[str] = []
3054
+ for item in evidence:
3055
+ if item not in seen:
3056
+ seen.add(item)
3057
+ uniq.append(item)
3058
+ return uniq
3059
+
2903
3060
  def _run_prevalidate(
2904
3061
  self, product_payload: Dict[str, Any]
2905
3062
  ) -> Optional[Dict[str, Any]]:
@@ -3505,6 +3662,7 @@ def _validate_title(title: str) -> None:
3505
3662
  def _validate_create_payload(
3506
3663
  payload: Dict[str, Any],
3507
3664
  seller_sku_required: bool = False,
3665
+ draft: bool = False,
3508
3666
  ) -> None:
3509
3667
  """Validate a Miravia ``/product/create`` Product payload.
3510
3668
 
@@ -3514,7 +3672,9 @@ def _validate_create_payload(
3514
3672
 
3515
3673
  ``seller_sku_required`` is decided by the caller from the category attribute
3516
3674
  API (``_field_is_required``): when False, an empty SellerSku is allowed and
3517
- the create API auto-generates a unique value.
3675
+ the create API auto-generates a unique value. When True but ``draft`` is set,
3676
+ the empty value is deferred to the draft (surfaced as a draft_issue) rather
3677
+ than hard-blocked, consistent with P7 — only a live publish enforces it.
3518
3678
  """
3519
3679
  attributes = payload.get("Attributes") or {}
3520
3680
  title = attributes.get("name")
@@ -3558,7 +3718,11 @@ def _validate_create_payload(
3558
3718
  raise ValueError(i18n._("pub_seller_sku_duplicate_in_product"))
3559
3719
 
3560
3720
  for idx, sku in enumerate(skus, start=1):
3561
- if seller_sku_required and not (sku.get("SellerSku") or sku.get("seller_sku")):
3721
+ if (
3722
+ seller_sku_required
3723
+ and not draft
3724
+ and not (sku.get("SellerSku") or sku.get("seller_sku"))
3725
+ ):
3562
3726
  raise ValueError(i18n._("pub_seller_sku_required", idx=idx))
3563
3727
 
3564
3728
  price = sku.get("price")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miravia_test/miravia-publish-skill",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Miravia Product Publish Skill installer — one command to install the Miravia AI product listing skill into any AI Agent (Qoder / Cursor / Claude Code). Upload materials and let the AI extract data and publish products automatically.",
5
5
  "bin": {
6
6
  "miravia-publish-skill": "bin/cli.js"