makea-cli 0.1.21__tar.gz → 0.1.23__tar.gz

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.
Files changed (51) hide show
  1. {makea_cli-0.1.21 → makea_cli-0.1.23}/PKG-INFO +5 -2
  2. {makea_cli-0.1.21 → makea_cli-0.1.23}/README.md +4 -1
  3. makea_cli-0.1.23/makea_cli/__init__.py +1 -0
  4. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/api_client.py +40 -0
  5. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/supplier/cmd.py +616 -57
  6. {makea_cli-0.1.21 → makea_cli-0.1.23}/pyproject.toml +1 -1
  7. makea_cli-0.1.21/makea_cli/__init__.py +0 -1
  8. {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
  9. {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
  10. {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/link-supplier-user/SKILL.md +0 -0
  11. {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
  12. {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
  13. {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
  14. {makea_cli-0.1.21 → makea_cli-0.1.23}/.gitignore +0 -0
  15. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea-collection-upload/SKILL.md +0 -0
  16. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea-collection-upload/scripts/create_products.py +0 -0
  17. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/__main__.py +0 -0
  18. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/auth_pkce.py +0 -0
  19. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/__init__.py +0 -0
  20. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/_util.py +0 -0
  21. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/analytics/__init__.py +0 -0
  22. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/analytics/cmd.py +0 -0
  23. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/auth/__init__.py +0 -0
  24. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/auth/cmd.py +0 -0
  25. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/designer/__init__.py +0 -0
  26. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/designer/cmd.py +0 -0
  27. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/directory/__init__.py +0 -0
  28. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/directory/cmd.py +0 -0
  29. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/document/__init__.py +0 -0
  30. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/document/cmd.py +0 -0
  31. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/email/__init__.py +0 -0
  32. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/email/cmd.py +0 -0
  33. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/library/__init__.py +0 -0
  34. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/library/cmd.py +0 -0
  35. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/metrics/__init__.py +0 -0
  36. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/metrics/cmd.py +0 -0
  37. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/orders/__init__.py +0 -0
  38. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/orders/cmd.py +0 -0
  39. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/product/__init__.py +0 -0
  40. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/product/cmd.py +0 -0
  41. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/production/__init__.py +0 -0
  42. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/production/cmd.py +0 -0
  43. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/production/pricing_spec.py +0 -0
  44. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/supplier/__init__.py +0 -0
  45. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/config.py +0 -0
  46. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/main.py +0 -0
  47. {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/posthog_client.py +0 -0
  48. {makea_cli-0.1.21 → makea_cli-0.1.23}/npm/makea-cli/README.md +0 -0
  49. {makea_cli-0.1.21 → makea_cli-0.1.23}/npm/makea-cli/makea-cli.mjs +0 -0
  50. {makea_cli-0.1.21 → makea_cli-0.1.23}/npm/makea-cli/package.json +0 -0
  51. {makea_cli-0.1.21 → makea_cli-0.1.23}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: makea-cli
3
- Version: 0.1.21
3
+ Version: 0.1.23
4
4
  Summary: Makea admin CLI (browser login + Makea admin HTTP API)
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: httpx>=0.27
@@ -80,9 +80,12 @@ makea-cli auth --logout
80
80
  - `makea-cli create-user <email> --user-type "<type>" [--password ...] [--name ...] [--company-name ...] [--username ...]` — 注册用户并直接确认 Cognito 账号(POST `/admin/users/create`,不发验证码邮件,创建后即可用该密码登录)
81
81
  - `makea-cli list-suppliers` — 列出供应商(需管理员权限)
82
82
  - `makea-cli create-quote-request -s <supplier_id> -p <product_reference_id> [--quote-type SAMPLING|PRODUCTION] [--message ...] [--due-date YYYY-MM-DD]` — 创建并发送 quote request(POST `/admin/supplier/create_quote_request`)
83
+ - `makea-cli get-quote-requests [--product-id <product_id>] [--quote-request-id <id>]` — 列某个产品下的 quote requests,或读单条完整内容(GET `/admin/quote_requests`)
84
+ - `makea-cli update-quote-request <quote_request_id> [--stage SAMPLING] [--status PENDING_SUPPLIER] [--due-date YYYY-MM-DD | --clear-due-date]` — 改 quote request 的 stage / status / 截止日(PATCH `/admin/quote_requests/{id}/update`)。**代供应商报价前常常要先用它**:quote request 停在 ENGAGEMENT(供应商还没接单)时 `submit-supplier-quote` 会被拒;过了 due date 也会被拒
83
85
  - `makea-cli get-quote-requests-by-supplier-id <supplier_id> [--status <status>] [-n <limit>]` — 直连后端按 supplier_id 查询 quote requests
84
86
  - `makea-cli get-supplier-links-by-supplier-id <supplier_id> [--link-type SAMPLING|PRODUCTION]` — 直连后端按 supplier_id 查询 supplier links
85
- - `makea-cli submit-supplier-quote <quote_request_id> --link-name ... --currency ... --sampling-price-amount ... --sampling-price-valid-until ... --production-price-tiers-json ... --sample-lead-time ... --lead-time ... --deliverables-json ... --customer-note ...` — 以供应商身份向指定 quote request 新增 quote(完整参数说明见 `--help`)
87
+ - `makea-cli submit-supplier-quote <quote_request_id> --link-name ... --currency ... --sampling-price-value ... --sampling-price-valid-until ... --sample-lead-time ... --deliverables-json ... --customer-note ...` — 以 admin 身份代供应商向指定 quote request 新增 quote。大货阶梯价(`--production-price-tiers-json`)和 `--lead-time` 现在是可选的;样衣加做费、MOQ 口径、寄料地址(CMT / full package 打样报价必填)、可选尺码颜色、规格行、typed link 等字段见 `--help`。pricing groups 提交时给不进去,提交完用 `update-supplier-quote` 补
88
+ - `makea-cli update-supplier-quote <product_id> <link_id> [--sampling-price-value ... --sampling-price-valid-until ...] [--production-price-tiers-json ...] [--pricing-groups-json ...] [...]` — 改一条已提交的 quote(supplier link):只发你传的字段,没传的后端不动;pricing groups(多套阶梯价)只有这条命令能写(POST `/admin/product/edit_supplier_link_override_info`)
86
89
  - `makea-cli list-product-orders` — 列出所有设计师产品订单(GET `/admin/designer/get_all_product_orders`)
87
90
  - `makea-cli list-sampling-orders-by-product <id>` — 按产品 reference id 列打样单
88
91
  - `makea-cli list-production-orders-by-product <id>` — 按产品 reference id 列大货单
@@ -69,9 +69,12 @@ makea-cli auth --logout
69
69
  - `makea-cli create-user <email> --user-type "<type>" [--password ...] [--name ...] [--company-name ...] [--username ...]` — 注册用户并直接确认 Cognito 账号(POST `/admin/users/create`,不发验证码邮件,创建后即可用该密码登录)
70
70
  - `makea-cli list-suppliers` — 列出供应商(需管理员权限)
71
71
  - `makea-cli create-quote-request -s <supplier_id> -p <product_reference_id> [--quote-type SAMPLING|PRODUCTION] [--message ...] [--due-date YYYY-MM-DD]` — 创建并发送 quote request(POST `/admin/supplier/create_quote_request`)
72
+ - `makea-cli get-quote-requests [--product-id <product_id>] [--quote-request-id <id>]` — 列某个产品下的 quote requests,或读单条完整内容(GET `/admin/quote_requests`)
73
+ - `makea-cli update-quote-request <quote_request_id> [--stage SAMPLING] [--status PENDING_SUPPLIER] [--due-date YYYY-MM-DD | --clear-due-date]` — 改 quote request 的 stage / status / 截止日(PATCH `/admin/quote_requests/{id}/update`)。**代供应商报价前常常要先用它**:quote request 停在 ENGAGEMENT(供应商还没接单)时 `submit-supplier-quote` 会被拒;过了 due date 也会被拒
72
74
  - `makea-cli get-quote-requests-by-supplier-id <supplier_id> [--status <status>] [-n <limit>]` — 直连后端按 supplier_id 查询 quote requests
73
75
  - `makea-cli get-supplier-links-by-supplier-id <supplier_id> [--link-type SAMPLING|PRODUCTION]` — 直连后端按 supplier_id 查询 supplier links
74
- - `makea-cli submit-supplier-quote <quote_request_id> --link-name ... --currency ... --sampling-price-amount ... --sampling-price-valid-until ... --production-price-tiers-json ... --sample-lead-time ... --lead-time ... --deliverables-json ... --customer-note ...` — 以供应商身份向指定 quote request 新增 quote(完整参数说明见 `--help`)
76
+ - `makea-cli submit-supplier-quote <quote_request_id> --link-name ... --currency ... --sampling-price-value ... --sampling-price-valid-until ... --sample-lead-time ... --deliverables-json ... --customer-note ...` — 以 admin 身份代供应商向指定 quote request 新增 quote。大货阶梯价(`--production-price-tiers-json`)和 `--lead-time` 现在是可选的;样衣加做费、MOQ 口径、寄料地址(CMT / full package 打样报价必填)、可选尺码颜色、规格行、typed link 等字段见 `--help`。pricing groups 提交时给不进去,提交完用 `update-supplier-quote` 补
77
+ - `makea-cli update-supplier-quote <product_id> <link_id> [--sampling-price-value ... --sampling-price-valid-until ...] [--production-price-tiers-json ...] [--pricing-groups-json ...] [...]` — 改一条已提交的 quote(supplier link):只发你传的字段,没传的后端不动;pricing groups(多套阶梯价)只有这条命令能写(POST `/admin/product/edit_supplier_link_override_info`)
75
78
  - `makea-cli list-product-orders` — 列出所有设计师产品订单(GET `/admin/designer/get_all_product_orders`)
76
79
  - `makea-cli list-sampling-orders-by-product <id>` — 按产品 reference id 列打样单
77
80
  - `makea-cli list-production-orders-by-product <id>` — 按产品 reference id 列大货单
@@ -0,0 +1 @@
1
+ __version__ = "0.1.23"
@@ -294,6 +294,25 @@ def get_quote_requests(*, product_id: str | None = None) -> Any:
294
294
  return get_json("/admin/quote_requests", params=params)
295
295
 
296
296
 
297
+ def get_quote_request(*, quote_request_id: str) -> Any:
298
+ """
299
+ GET /admin/quote_requests/{quote_request_id} — one quote request in full.
300
+ """
301
+ return get_json(f"/admin/quote_requests/{quote_request_id}")
302
+
303
+
304
+ def update_quote_request(
305
+ *,
306
+ quote_request_id: str,
307
+ body: dict[str, Any],
308
+ ) -> Any:
309
+ """
310
+ PATCH /admin/quote_requests/{quote_request_id}/update — set a quote request's
311
+ stage, status and/or due date. Only the keys present in ``body`` are applied.
312
+ """
313
+ return patch_json(f"/admin/quote_requests/{quote_request_id}/update", body)
314
+
315
+
297
316
  def create_quote_request(
298
317
  *,
299
318
  supplier_id: str,
@@ -390,6 +409,27 @@ def submit_admin_quote_for_quote_request(
390
409
  return post_json(f"/admin/quote_requests/{quote_request_id}/submit_quote", body)
391
410
 
392
411
 
412
+ def edit_supplier_link_override_info(
413
+ *,
414
+ product_id: str,
415
+ link_id: str,
416
+ override_info: dict[str, Any],
417
+ ) -> Any:
418
+ """
419
+ POST /admin/product/edit_supplier_link_override_info — admin updates an existing
420
+ supplier link (a submitted quote) in place. Only the keys present in
421
+ ``override_info`` are applied; the rest of the link is left alone.
422
+ """
423
+ return post_json(
424
+ "/admin/product/edit_supplier_link_override_info",
425
+ {
426
+ "product_id": product_id,
427
+ "link_id": link_id,
428
+ "override_info": override_info,
429
+ },
430
+ )
431
+
432
+
393
433
  def upload_admin_link_document(
394
434
  *,
395
435
  file_path: Path,
@@ -20,6 +20,86 @@ def _normalize_add_supplier_body(raw: dict) -> dict:
20
20
  return {"supplier": raw}
21
21
 
22
22
 
23
+ MOQ_APPLIES_TO_VALUES = ("style", "size", "color")
24
+
25
+ # Mirrors garmentby_service.domain.QuoteRequest — kept here only so `--help` can
26
+ # name the values; the backend is the one that validates.
27
+ QUOTE_REQUEST_STAGES = ("ENGAGEMENT", "SAMPLING", "PRODUCTION")
28
+ QUOTE_REQUEST_STATUSES = (
29
+ "PENDING_MERCHANT",
30
+ "PENDING_SUPPLIER",
31
+ "NEEDS_CLARIFICATION",
32
+ "COMPLETED",
33
+ "ORDER_PLACED",
34
+ "TERMINATED",
35
+ "RESAMPLE_QUOTE_REQUESTED",
36
+ )
37
+ PRODUCTION_FLOW_STATUSES = (
38
+ "PENDING_QUOTE",
39
+ "QUOTE_SUBMITTED",
40
+ "QUOTE_CONFIRMED",
41
+ "AWAITING_DEPOSIT",
42
+ "DEPOSIT_PAID",
43
+ "IN_PRODUCTION",
44
+ "AWAITING_FINAL_PAYMENT",
45
+ "FINAL_PAYMENT_PAID",
46
+ "SHIPPED",
47
+ "COMPLETED",
48
+ "CANCELLED",
49
+ )
50
+
51
+
52
+ def _parse_json_option(raw: str, option_name: str) -> object:
53
+ """Parse a JSON-valued option, failing with the option's own name."""
54
+ try:
55
+ return json.loads(raw)
56
+ except json.JSONDecodeError as e:
57
+ console.print(f"[red]{option_name} must be valid JSON: {e}[/red]")
58
+ raise typer.Exit(1) from e
59
+
60
+
61
+ def _parse_json_list_option(raw: str, option_name: str) -> list:
62
+ parsed = _parse_json_option(raw, option_name)
63
+ if not isinstance(parsed, list):
64
+ console.print(f"[red]{option_name} must be a JSON array (list).[/red]")
65
+ raise typer.Exit(1)
66
+ return parsed
67
+
68
+
69
+ def _parse_json_object_option(raw: str, option_name: str) -> dict:
70
+ parsed = _parse_json_option(raw, option_name)
71
+ if not isinstance(parsed, dict):
72
+ console.print(f"[red]{option_name} must be a JSON object.[/red]")
73
+ raise typer.Exit(1)
74
+ return parsed
75
+
76
+
77
+ def _validate_moq_applies_to(values: list[str]) -> list[str]:
78
+ """Normalize --moq-applies-to; the backend silently drops unknown values."""
79
+ normalized = [str(v).strip().lower() for v in values if str(v).strip()]
80
+ unknown = [v for v in normalized if v not in MOQ_APPLIES_TO_VALUES]
81
+ if unknown:
82
+ console.print(
83
+ f"[red]--moq-applies-to accepts only {', '.join(MOQ_APPLIES_TO_VALUES)} "
84
+ f"(got: {', '.join(unknown)}).[/red]"
85
+ )
86
+ raise typer.Exit(1)
87
+ return [v for v in MOQ_APPLIES_TO_VALUES if v in set(normalized)]
88
+
89
+
90
+ def _validate_choice(value: str, allowed: tuple[str, ...], option_name: str) -> str:
91
+ """Reject an out-of-range option before it costs a round trip."""
92
+ if value not in allowed:
93
+ console.print(f"[red]{option_name} must be one of {', '.join(allowed)}.[/red]")
94
+ raise typer.Exit(1)
95
+ return value
96
+
97
+
98
+ def _none_if_null_literal(value: str) -> str | None:
99
+ """Let an option clear a stored value by passing the literal `null`."""
100
+ return None if value.strip().lower() in ("null", "none") else value
101
+
102
+
23
103
  def register(app: typer.Typer) -> None:
24
104
  @app.command(
25
105
  "list-suppliers",
@@ -468,11 +548,131 @@ def register(app: typer.Typer) -> None:
468
548
  ),
469
549
  )
470
550
 
551
+ @app.command(
552
+ "get-quote-requests",
553
+ help=(
554
+ "GET /admin/quote_requests — list quote requests, optionally for one product. "
555
+ "Pass --quote-request-id to read a single one in full "
556
+ "(GET /admin/quote_requests/{id})."
557
+ ),
558
+ )
559
+ def get_quote_requests(
560
+ product_id: str | None = typer.Option(
561
+ None,
562
+ "--product-id",
563
+ "-p",
564
+ help="product_id: only quote requests on this product (product_reference_id).",
565
+ ),
566
+ quote_request_id: str | None = typer.Option(
567
+ None,
568
+ "--quote-request-id",
569
+ help="quote_request_id: read this one quote request in full instead of a list.",
570
+ ),
571
+ ) -> None:
572
+ """
573
+ List quote requests, or read one.
574
+ """
575
+ if quote_request_id:
576
+ print_json_or_exit(
577
+ console,
578
+ lambda: api_client.get_quote_request(quote_request_id=quote_request_id),
579
+ )
580
+ return
581
+ print_json_or_exit(
582
+ console,
583
+ lambda: api_client.get_quote_requests(product_id=product_id),
584
+ )
585
+
586
+ @app.command(
587
+ "update-quote-request",
588
+ help=(
589
+ "PATCH /admin/quote_requests/{quote_request_id}/update — set a quote "
590
+ "request's stage, status and/or due date. Only what you pass is changed. "
591
+ "This is the door that unblocks a submit: submit-supplier-quote requires "
592
+ "stage SAMPLING or PRODUCTION, and a quote request sits in ENGAGEMENT "
593
+ "until the supplier accepts it."
594
+ ),
595
+ )
596
+ def update_quote_request(
597
+ quote_request_id: str = typer.Argument(
598
+ ...,
599
+ help="quote_request_id: the quote request to update.",
600
+ ),
601
+ stage: str | None = typer.Option(
602
+ None,
603
+ "--stage",
604
+ help=(
605
+ "current_stage: ENGAGEMENT (supplier has not accepted yet), SAMPLING or "
606
+ "PRODUCTION. Moving to SAMPLING/PRODUCTION by hand skips the supplier's "
607
+ "own acceptance, so accepted_at / terms_accepted_at stay empty."
608
+ ),
609
+ ),
610
+ status: str | None = typer.Option(
611
+ None,
612
+ "--status",
613
+ help=(
614
+ "status: for ENGAGEMENT/SAMPLING one of "
615
+ f"{', '.join(QUOTE_REQUEST_STATUSES)}; "
616
+ "for PRODUCTION one of "
617
+ f"{', '.join(PRODUCTION_FLOW_STATUSES)}. "
618
+ "The backend validates against whichever stage the request ends up in."
619
+ ),
620
+ ),
621
+ due_date: str | None = typer.Option(
622
+ None,
623
+ "--due-date",
624
+ help=(
625
+ "due_date: ISO date (YYYY-MM-DD). A quote request past its due date "
626
+ "cannot be quoted, so this is the other thing that blocks a submit."
627
+ ),
628
+ ),
629
+ clear_due_date: bool = typer.Option(
630
+ False,
631
+ "--clear-due-date",
632
+ help="Send due_date as null, leaving the quote request with no deadline.",
633
+ ),
634
+ ) -> None:
635
+ """
636
+ Update a quote request's stage, status and/or due date.
637
+ """
638
+ if due_date and clear_due_date:
639
+ console.print("[red]Pass either --due-date or --clear-due-date, not both.[/red]")
640
+ raise typer.Exit(1)
641
+
642
+ body: dict[str, object] = {}
643
+ if stage is not None:
644
+ body["current_stage"] = _validate_choice(
645
+ stage.strip().upper(), QUOTE_REQUEST_STAGES, "--stage"
646
+ )
647
+ if status is not None:
648
+ body["status"] = status.strip().upper()
649
+ if due_date:
650
+ body["due_date"] = due_date.strip()
651
+ elif clear_due_date:
652
+ body["due_date"] = None
653
+
654
+ if not body:
655
+ console.print(
656
+ "[red]Nothing to update: pass --stage, --status, --due-date or "
657
+ "--clear-due-date.[/red]"
658
+ )
659
+ raise typer.Exit(1)
660
+
661
+ print_json_or_exit(
662
+ console,
663
+ lambda: api_client.update_quote_request(
664
+ quote_request_id=quote_request_id,
665
+ body=body,
666
+ ),
667
+ )
668
+
471
669
  @app.command(
472
670
  "submit-supplier-quote",
473
671
  help=(
474
672
  "POST /admin/quote_requests/{quote_request_id}/submit_quote — "
475
- "admin submits a new quote (link) to a quote request."
673
+ "admin submits a new quote (link) to a quote request. "
674
+ "Pricing groups cannot be sent here (the submit endpoint ignores them) — "
675
+ "set them afterwards with `update-supplier-quote`."
476
676
  ),
477
677
  )
478
678
  def submit_supplier_quote(
@@ -490,10 +690,14 @@ def register(app: typer.Typer) -> None:
490
690
  "--currency",
491
691
  help="currency: quote currency (used by sampling price and fees).",
492
692
  ),
493
- sampling_price_amount: float = typer.Option(
693
+ sampling_price_value: float = typer.Option(
494
694
  ...,
695
+ "--sampling-price-value",
495
696
  "--sampling-price-amount",
496
- help="sampling_price.amount: sampling price amount (required, number).",
697
+ help=(
698
+ "sampling_price.value: sampling price (required, number). "
699
+ "Sent as `value` — the backend reads no other key."
700
+ ),
497
701
  ),
498
702
  sampling_price_valid_until: str = typer.Option(
499
703
  ...,
@@ -503,25 +707,11 @@ def register(app: typer.Typer) -> None:
503
707
  "(required, ISO datetime string, e.g. 2026-05-01T00:00:00+00:00)."
504
708
  ),
505
709
  ),
506
- production_price_tiers_json: str = typer.Option(
507
- ...,
508
- "--production-price-tiers-json",
509
- help=(
510
- "production_price_tiers: tiered production price array "
511
- "(required, JSON string). "
512
- 'Example: \'[{"min_qty":100,"max_qty":499,"unit_price":12.5}]\''
513
- ),
514
- ),
515
710
  sample_lead_time: str = typer.Option(
516
711
  ...,
517
712
  "--sample-lead-time",
518
713
  help="sample_lead_time: sampling lead time (required, string).",
519
714
  ),
520
- lead_time: str = typer.Option(
521
- ...,
522
- "--lead-time",
523
- help="lead_time: production lead time (required, string).",
524
- ),
525
715
  deliverables_json: str = typer.Option(
526
716
  ...,
527
717
  "--deliverables-json",
@@ -535,6 +725,28 @@ def register(app: typer.Typer) -> None:
535
725
  "--customer-note",
536
726
  help="customer_note: note for customer/platform (required, string; empty string allowed).",
537
727
  ),
728
+ production_price_tiers_json: str | None = typer.Option(
729
+ None,
730
+ "--production-price-tiers-json",
731
+ help=(
732
+ "production_price_tiers: tiered production price array "
733
+ "(optional since TECH-93 — a factory may quote the sample only). "
734
+ 'Example: \'[{"min_qty":100,"max_qty":499,"unit_price":12.5}]\''
735
+ ),
736
+ ),
737
+ lead_time: str | None = typer.Option(
738
+ None,
739
+ "--lead-time",
740
+ help="lead_time: production lead time (optional since TECH-93, string).",
741
+ ),
742
+ extra_sample_price_value: float | None = typer.Option(
743
+ None,
744
+ "--extra-sample-price-value",
745
+ help=(
746
+ "extra_sample_price.value: per-unit charge for extra samples "
747
+ "(optional, number; currency defaults to --currency)."
748
+ ),
749
+ ),
538
750
  link_id: str | None = typer.Option(
539
751
  None,
540
752
  "--link-id",
@@ -550,6 +762,14 @@ def register(app: typer.Typer) -> None:
550
762
  "--moq",
551
763
  help="moq: minimum order quantity (optional, integer).",
552
764
  ),
765
+ moq_applies_to: list[str] | None = typer.Option(
766
+ None,
767
+ "--moq-applies-to",
768
+ help=(
769
+ "moq_applies_to: what the MOQ counts (optional, repeatable): "
770
+ "style, size, color. Empty means the supplier did not say."
771
+ ),
772
+ ),
553
773
  production_price_incoterm: str | None = typer.Option(
554
774
  None,
555
775
  "--production-price-incoterm",
@@ -560,6 +780,63 @@ def register(app: typer.Typer) -> None:
560
780
  "--production-payment-term",
561
781
  help='production_payment_term: production payment term/deposit ratio (optional, e.g. "40" or "40%").',
562
782
  ),
783
+ material_ship_to_json: str | None = typer.Option(
784
+ None,
785
+ "--material-ship-to-json",
786
+ help=(
787
+ "material_ship_to_address: where the buyer ships materials "
788
+ "(JSON object with street, city, state, postal_code, country). "
789
+ "REQUIRED by the backend for a CMT or full-package SAMPLING quote — "
790
+ "street, city and country must be non-empty. "
791
+ 'Example: \'{"street":"1 Test Rd","city":"Guangzhou","country":"CN"}\''
792
+ ),
793
+ ),
794
+ available_sizes_json: str | None = typer.Option(
795
+ None,
796
+ "--available-sizes-json",
797
+ help=(
798
+ "available_sizes: sizes the piece comes in (optional, JSON array of strings). "
799
+ 'Example: \'["S","M","L"]\''
800
+ ),
801
+ ),
802
+ available_colors_json: str | None = typer.Option(
803
+ None,
804
+ "--available-colors-json",
805
+ help=(
806
+ "available_colors: colours the piece comes in (optional, JSON array of objects). "
807
+ 'Example: \'[{"color_name":"Black","hex_code":"#000000","pantone_code":"Black 6 C"}]\''
808
+ ),
809
+ ),
810
+ size_chart_document_id: str | None = typer.Option(
811
+ None,
812
+ "--size-chart-document-id",
813
+ help=(
814
+ "size_chart_document_id: the quote's size chart document (optional). "
815
+ 'Pass an empty string ("") to send it as null and leave the link without one.'
816
+ ),
817
+ ),
818
+ component_specifications_json: str | None = typer.Option(
819
+ None,
820
+ "--component-specifications-json",
821
+ help=(
822
+ "component_specifications: free-form specification rows (optional, JSON array). "
823
+ 'Example: \'[{"component":"Fabric","specification":"100% cotton, 220gsm"}]\''
824
+ ),
825
+ ),
826
+ link_type_kind: str | None = typer.Option(
827
+ None,
828
+ "--type",
829
+ help=(
830
+ "type: typed-link kind (optional; default MANUFACTURE). "
831
+ "MANUFACTURE, PRODUCT_DEVELOP, or a material kind (FABRIC, BUTTON, ZIPPER, "
832
+ "ELASTIC, LABEL, HOOK, SNAP, CORD, BUCKLE, TAPE, PATCH, LACE, EYELET)."
833
+ ),
834
+ ),
835
+ attributes_json: str | None = typer.Option(
836
+ None,
837
+ "--attributes-json",
838
+ help="attributes: LinkAttributes-shaped SKU metadata (optional, JSON object).",
839
+ ),
563
840
  document_ids_json: str | None = typer.Option(
564
841
  None,
565
842
  "--document-ids-json",
@@ -594,76 +871,78 @@ def register(app: typer.Typer) -> None:
594
871
  """
595
872
  Submit a new quote link to a quote request as admin.
596
873
  """
597
-
598
- def _parse_json_option(raw: str, option_name: str) -> object:
599
- try:
600
- return json.loads(raw)
601
- except json.JSONDecodeError as e:
602
- console.print(f"[red]{option_name} must be valid JSON: {e}[/red]")
603
- raise typer.Exit(1) from e
604
-
605
- production_price_tiers = _parse_json_option(
606
- production_price_tiers_json, "--production-price-tiers-json"
607
- )
608
- deliverables = _parse_json_option(deliverables_json, "--deliverables-json")
609
- if not isinstance(production_price_tiers, list):
610
- console.print("[red]--production-price-tiers-json must be a JSON array (list).[/red]")
611
- raise typer.Exit(1)
612
- if not isinstance(deliverables, list):
613
- console.print("[red]--deliverables-json must be a JSON array (list).[/red]")
614
- raise typer.Exit(1)
874
+ deliverables = _parse_json_list_option(deliverables_json, "--deliverables-json")
615
875
 
616
876
  body: dict[str, object] = {
617
877
  "link_name": link_name,
618
878
  "currency": currency,
619
879
  "sampling_price": {
620
- "amount": sampling_price_amount,
880
+ # `value`, not `amount`: Price.to_domain reads `value` and nothing
881
+ # else, so an `amount` key used to land the quote at zero.
882
+ "value": sampling_price_value,
621
883
  "currency": currency,
622
884
  "valid_until": sampling_price_valid_until,
623
885
  },
624
- "production_price_tiers": production_price_tiers,
625
886
  "sample_lead_time": sample_lead_time,
626
- "lead_time": lead_time,
627
887
  "deliverables": deliverables,
628
888
  "customer_note": customer_note,
629
889
  }
630
890
 
891
+ if production_price_tiers_json is not None:
892
+ body["production_price_tiers"] = _parse_json_list_option(
893
+ production_price_tiers_json, "--production-price-tiers-json"
894
+ )
895
+ if lead_time is not None:
896
+ body["lead_time"] = lead_time
897
+ if extra_sample_price_value is not None:
898
+ body["extra_sample_price"] = {
899
+ "value": extra_sample_price_value,
900
+ "currency": currency,
901
+ }
631
902
  if link_id:
632
903
  body["link_id"] = link_id
633
904
  if link_type:
634
905
  body["link_type"] = link_type
635
906
  if moq is not None:
636
907
  body["moq"] = moq
908
+ if moq_applies_to:
909
+ body["moq_applies_to"] = _validate_moq_applies_to(moq_applies_to)
637
910
  if production_price_incoterm is not None:
638
911
  body["production_price_incoterm"] = production_price_incoterm
639
912
  if production_payment_term is not None:
640
913
  body["production_payment_term"] = production_payment_term
914
+ if material_ship_to_json is not None:
915
+ body["material_ship_to_address"] = _parse_json_object_option(
916
+ material_ship_to_json, "--material-ship-to-json"
917
+ )
918
+ if available_sizes_json is not None:
919
+ body["available_sizes"] = _parse_json_list_option(
920
+ available_sizes_json, "--available-sizes-json"
921
+ )
922
+ if available_colors_json is not None:
923
+ body["available_colors"] = _parse_json_list_option(
924
+ available_colors_json, "--available-colors-json"
925
+ )
926
+ if size_chart_document_id is not None:
927
+ body["size_chart_document_id"] = size_chart_document_id or None
928
+ if component_specifications_json is not None:
929
+ body["component_specifications"] = _parse_json_list_option(
930
+ component_specifications_json, "--component-specifications-json"
931
+ )
932
+ if link_type_kind is not None:
933
+ body["type"] = link_type_kind
934
+ if attributes_json is not None:
935
+ body["attributes"] = _parse_json_object_option(attributes_json, "--attributes-json")
641
936
  if document_ids_json is not None:
642
- parsed = _parse_json_option(document_ids_json, "--document-ids-json")
643
- if not isinstance(parsed, list):
644
- console.print("[red]--document-ids-json must be a JSON array (list).[/red]")
645
- raise typer.Exit(1)
646
- body["document_ids"] = parsed
937
+ body["document_ids"] = _parse_json_list_option(document_ids_json, "--document-ids-json")
647
938
  if additional_deliverables_json is not None:
648
- parsed = _parse_json_option(
939
+ body["additional_deliverables"] = _parse_json_list_option(
649
940
  additional_deliverables_json, "--additional-deliverables-json"
650
941
  )
651
- if not isinstance(parsed, list):
652
- console.print(
653
- "[red]--additional-deliverables-json must be a JSON array (list).[/red]"
654
- )
655
- raise typer.Exit(1)
656
- body["additional_deliverables"] = parsed
657
942
  if one_time_production_fees_json is not None:
658
- parsed = _parse_json_option(
943
+ body["one_time_production_fees"] = _parse_json_list_option(
659
944
  one_time_production_fees_json, "--one-time-production-fees-json"
660
945
  )
661
- if not isinstance(parsed, list):
662
- console.print(
663
- "[red]--one-time-production-fees-json must be a JSON array (list).[/red]"
664
- )
665
- raise typer.Exit(1)
666
- body["one_time_production_fees"] = parsed
667
946
  if refundable_terms_json is not None:
668
947
  body["refundable_terms"] = _parse_json_option(
669
948
  refundable_terms_json, "--refundable-terms-json"
@@ -686,3 +965,283 @@ def register(app: typer.Typer) -> None:
686
965
  body=body,
687
966
  ),
688
967
  )
968
+
969
+ @app.command(
970
+ "update-supplier-quote",
971
+ help=(
972
+ "POST /admin/product/edit_supplier_link_override_info — admin updates an "
973
+ "existing quote (supplier link) in place. Only the options you pass are "
974
+ "sent, and the backend leaves every field you omit alone. "
975
+ "The override door does not read extra_sample_price, moq_applies_to, "
976
+ "material_ship_to_address or size_chart_document_id — those can only be "
977
+ "set at submit time."
978
+ ),
979
+ )
980
+ def update_supplier_quote(
981
+ product_id: str = typer.Argument(
982
+ ...,
983
+ help="product_id: the product the quote (supplier link) hangs off.",
984
+ ),
985
+ link_id: str = typer.Argument(
986
+ ...,
987
+ help="link_id: the supplier link to update.",
988
+ ),
989
+ supplier_id: str | None = typer.Option(
990
+ None,
991
+ "--supplier-id",
992
+ help="supplier_id: reassign the link to another supplier (validated by the backend).",
993
+ ),
994
+ link_name: str | None = typer.Option(
995
+ None,
996
+ "--link-name",
997
+ help="link_name: quote name.",
998
+ ),
999
+ currency: str | None = typer.Option(
1000
+ None,
1001
+ "--currency",
1002
+ help="currency: quote currency (also the fallback currency for fees).",
1003
+ ),
1004
+ sampling_price_value: float | None = typer.Option(
1005
+ None,
1006
+ "--sampling-price-value",
1007
+ help=(
1008
+ "sampling_price.value: new sampling price. The whole sampling_price "
1009
+ "object is replaced, so --sampling-price-valid-until is required with it."
1010
+ ),
1011
+ ),
1012
+ sampling_price_valid_until: str | None = typer.Option(
1013
+ None,
1014
+ "--sampling-price-valid-until",
1015
+ help="sampling_price.valid_until: expiry to store with the new sampling price.",
1016
+ ),
1017
+ sampling_price_options_json: str | None = typer.Option(
1018
+ None,
1019
+ "--sampling-price-options-json",
1020
+ help="sampling_price_options: multi-option sampling prices (JSON array).",
1021
+ ),
1022
+ production_price_tiers_json: str | None = typer.Option(
1023
+ None,
1024
+ "--production-price-tiers-json",
1025
+ help=(
1026
+ "production_price_tiers: the link's single tier schedule (JSON array; "
1027
+ "pass [] to clear it). "
1028
+ 'Example: \'[{"min_qty":100,"max_qty":499,"unit_price":12.5}]\''
1029
+ ),
1030
+ ),
1031
+ pricing_groups_json: str | None = typer.Option(
1032
+ None,
1033
+ "--pricing-groups-json",
1034
+ help=(
1035
+ "pricing_groups: several tier schedules on one quote (JSON array; "
1036
+ "pass [] to clear them). A group names the cart keys it prices "
1037
+ '("{size},{color_hex}") and carries its own tiers, MOQ and incoterm; '
1038
+ "tier_basis/moq_basis are VARIANT, GROUP (default) or ORDER, and "
1039
+ "max_qty 0 means an open-ended top tier. "
1040
+ 'Example: \'[{"name":"Core","variants":["S,#000000"],'
1041
+ '"price_tiers":[{"min_qty":100,"max_qty":0,"unit_price":12.5}],'
1042
+ '"tier_basis":"VARIANT"}]\''
1043
+ ),
1044
+ ),
1045
+ moq: int | None = typer.Option(
1046
+ None,
1047
+ "--moq",
1048
+ help="moq: minimum order quantity.",
1049
+ ),
1050
+ sample_lead_time: str | None = typer.Option(
1051
+ None,
1052
+ "--sample-lead-time",
1053
+ help="sample_lead_time: sampling lead time.",
1054
+ ),
1055
+ lead_time: str | None = typer.Option(
1056
+ None,
1057
+ "--lead-time",
1058
+ help="lead_time: production lead time.",
1059
+ ),
1060
+ production_price_incoterm: str | None = typer.Option(
1061
+ None,
1062
+ "--production-price-incoterm",
1063
+ help=(
1064
+ "production_price_incoterm: EXW or FOB; pass `null` to clear it. "
1065
+ "An unrecognised value is rejected by the backend."
1066
+ ),
1067
+ ),
1068
+ production_payment_term: str | None = typer.Option(
1069
+ None,
1070
+ "--production-payment-term",
1071
+ help='production_payment_term: deposit ratio, e.g. "40". Pass `null` to clear it.',
1072
+ ),
1073
+ customer_note: str | None = typer.Option(
1074
+ None,
1075
+ "--customer-note",
1076
+ help="customer_note: note for customer/platform (empty string allowed).",
1077
+ ),
1078
+ deliverables_json: str | None = typer.Option(
1079
+ None,
1080
+ "--deliverables-json",
1081
+ help="deliverables: deliverables array (JSON), e.g. '[\"size_set\"]'.",
1082
+ ),
1083
+ additional_deliverables_json: str | None = typer.Option(
1084
+ None,
1085
+ "--additional-deliverables-json",
1086
+ help="additional_deliverables: extra deliverables array (JSON).",
1087
+ ),
1088
+ one_time_production_fees_json: str | None = typer.Option(
1089
+ None,
1090
+ "--one-time-production-fees-json",
1091
+ help="one_time_production_fees: one-time production fees array (JSON).",
1092
+ ),
1093
+ refundable_terms_json: str | None = typer.Option(
1094
+ None,
1095
+ "--refundable-terms-json",
1096
+ help="refundable_terms: refundable terms (JSON object; pass `null` to clear).",
1097
+ ),
1098
+ available_sizes_json: str | None = typer.Option(
1099
+ None,
1100
+ "--available-sizes-json",
1101
+ help=(
1102
+ "available_sizes: sizes the quote is priced against (JSON array of strings). "
1103
+ "Pricing groups claim cart keys built from these."
1104
+ ),
1105
+ ),
1106
+ available_colors_json: str | None = typer.Option(
1107
+ None,
1108
+ "--available-colors-json",
1109
+ help=(
1110
+ "available_colors: colours the quote is priced against (JSON array of "
1111
+ "{color_name, hex_code, pantone_code?})."
1112
+ ),
1113
+ ),
1114
+ component_specifications_json: str | None = typer.Option(
1115
+ None,
1116
+ "--component-specifications-json",
1117
+ help="component_specifications: free-form specification rows (JSON array).",
1118
+ ),
1119
+ link_type_kind: str | None = typer.Option(
1120
+ None,
1121
+ "--type",
1122
+ help="type: typed-link kind (MANUFACTURE, PRODUCT_DEVELOP, FABRIC, BUTTON, ...).",
1123
+ ),
1124
+ attributes_json: str | None = typer.Option(
1125
+ None,
1126
+ "--attributes-json",
1127
+ help="attributes: LinkAttributes-shaped SKU metadata (JSON object).",
1128
+ ),
1129
+ document_ids_json: str | None = typer.Option(
1130
+ None,
1131
+ "--document-ids-json",
1132
+ help="document_ids: related document ID array (JSON), e.g. '[\"doc_1\"]'.",
1133
+ ),
1134
+ ) -> None:
1135
+ """
1136
+ Update an existing quote (supplier link) as admin.
1137
+ """
1138
+ override: dict[str, object] = {}
1139
+
1140
+ if (sampling_price_value is None) != (sampling_price_valid_until is None):
1141
+ console.print(
1142
+ "[red]--sampling-price-value and --sampling-price-valid-until must be "
1143
+ "passed together: the backend replaces the whole sampling_price object, "
1144
+ "so sending one without the other drops the other.[/red]"
1145
+ )
1146
+ raise typer.Exit(1)
1147
+
1148
+ if supplier_id is not None:
1149
+ override["supplier_id"] = supplier_id
1150
+ if link_name is not None:
1151
+ override["link_name"] = link_name
1152
+ if currency is not None:
1153
+ override["currency"] = currency
1154
+ if sampling_price_value is not None:
1155
+ override["sampling_price"] = {
1156
+ "value": sampling_price_value,
1157
+ "valid_until": sampling_price_valid_until,
1158
+ **({"currency": currency} if currency else {}),
1159
+ }
1160
+ if sampling_price_options_json is not None:
1161
+ override["sampling_price_options"] = _parse_json_list_option(
1162
+ sampling_price_options_json, "--sampling-price-options-json"
1163
+ )
1164
+ if production_price_tiers_json is not None:
1165
+ override["production_price_tiers"] = _parse_json_list_option(
1166
+ production_price_tiers_json, "--production-price-tiers-json"
1167
+ )
1168
+ if pricing_groups_json is not None:
1169
+ override["pricing_groups"] = _parse_json_list_option(
1170
+ pricing_groups_json, "--pricing-groups-json"
1171
+ )
1172
+ if moq is not None:
1173
+ override["moq"] = moq
1174
+ if sample_lead_time is not None:
1175
+ override["sample_lead_time"] = sample_lead_time
1176
+ if lead_time is not None:
1177
+ override["lead_time"] = lead_time
1178
+ if production_price_incoterm is not None:
1179
+ override["production_price_incoterm"] = _none_if_null_literal(production_price_incoterm)
1180
+ if production_payment_term is not None:
1181
+ override["production_payment_term"] = _none_if_null_literal(production_payment_term)
1182
+ if customer_note is not None:
1183
+ override["customer_note"] = customer_note
1184
+ if deliverables_json is not None:
1185
+ override["deliverables"] = _parse_json_list_option(
1186
+ deliverables_json, "--deliverables-json"
1187
+ )
1188
+ if additional_deliverables_json is not None:
1189
+ override["additional_deliverables"] = _parse_json_list_option(
1190
+ additional_deliverables_json, "--additional-deliverables-json"
1191
+ )
1192
+ if one_time_production_fees_json is not None:
1193
+ override["one_time_production_fees"] = _parse_json_list_option(
1194
+ one_time_production_fees_json, "--one-time-production-fees-json"
1195
+ )
1196
+ if refundable_terms_json is not None:
1197
+ override["refundable_terms"] = _parse_json_option(
1198
+ refundable_terms_json, "--refundable-terms-json"
1199
+ )
1200
+ if available_sizes_json is not None:
1201
+ override["available_sizes"] = _parse_json_list_option(
1202
+ available_sizes_json, "--available-sizes-json"
1203
+ )
1204
+ if available_colors_json is not None:
1205
+ override["available_colors"] = _parse_json_list_option(
1206
+ available_colors_json, "--available-colors-json"
1207
+ )
1208
+ if component_specifications_json is not None:
1209
+ override["component_specifications"] = _parse_json_list_option(
1210
+ component_specifications_json, "--component-specifications-json"
1211
+ )
1212
+ if link_type_kind is not None:
1213
+ override["type"] = link_type_kind
1214
+ if attributes_json is not None:
1215
+ override["attributes"] = _parse_json_object_option(attributes_json, "--attributes-json")
1216
+ if document_ids_json is not None:
1217
+ override["document_ids"] = _parse_json_list_option(
1218
+ document_ids_json, "--document-ids-json"
1219
+ )
1220
+
1221
+ if not override:
1222
+ console.print(
1223
+ "[red]Nothing to update: pass at least one field option "
1224
+ "(the backend rejects an empty override_info).[/red]"
1225
+ )
1226
+ raise typer.Exit(1)
1227
+
1228
+ # Pricing groups claim cart keys built from the link's own variant lists.
1229
+ # Groups without them price variants no order can contain, so say so
1230
+ # rather than let the quote look fine and misprice later.
1231
+ if override.get("pricing_groups") and not (
1232
+ "available_sizes" in override or "available_colors" in override
1233
+ ):
1234
+ console.print(
1235
+ "[yellow]Warning: --pricing-groups-json without --available-sizes-json / "
1236
+ "--available-colors-json relies on the variant lists already stored on the "
1237
+ "link. Check them with `get-supplier-links-by-supplier-id`.[/yellow]"
1238
+ )
1239
+
1240
+ print_json_or_exit(
1241
+ console,
1242
+ lambda: api_client.edit_supplier_link_override_info(
1243
+ product_id=product_id,
1244
+ link_id=link_id,
1245
+ override_info=override,
1246
+ ),
1247
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "makea-cli"
7
- version = "0.1.21"
7
+ version = "0.1.23"
8
8
  description = "Makea admin CLI (browser login + Makea admin HTTP API)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1 +0,0 @@
1
- __version__ = "0.1.21"
File without changes
File without changes
File without changes