makea-cli 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from makea_cli import api_client
7
+ from makea_cli.commands._util import print_json_or_exit
8
+
9
+ console = Console()
10
+
11
+
12
+ def register(app: typer.Typer) -> None:
13
+ @app.command(
14
+ "list-product-orders",
15
+ help=(
16
+ "GET get_all_product_orders — designer product (SKU) list. "
17
+ "For 大货 use list-all-production-orders or list-production-orders-by-product."
18
+ ),
19
+ )
20
+ def list_product_orders(
21
+ limit: int | None = typer.Option(
22
+ None,
23
+ "--limit",
24
+ "-n",
25
+ help="Optional page size.",
26
+ ),
27
+ offset: int = typer.Option(0, "--offset", help="Pagination offset."),
28
+ user_id: str | None = typer.Option(
29
+ None,
30
+ "--user-id",
31
+ help="If set, only product orders for this designer user id.",
32
+ ),
33
+ searchby: str | None = typer.Option(
34
+ None,
35
+ "--searchby",
36
+ help="Optional search string (server-defined matching).",
37
+ ),
38
+ ) -> None:
39
+ """
40
+ List designer product orders (admin “product” / project rows).
41
+
42
+ This is NOT the same as production (bulk) orders: ``get_all_product_orders`` returns
43
+ product-order records. To see **大货 / production orders**, use
44
+ ``list-all-production-orders`` or ``list-production-orders-by-product <product_id>`` —
45
+ production POs are exposed on ``/admin/production_orders``, not only via this list.
46
+
47
+ Input: optional --limit, --offset, --user-id, --searchby on
48
+ GET /admin/designer/get_all_product_orders.
49
+
50
+ Output: JSON with success, result (serialized product rows), and pagination
51
+ (total, limit, offset, has_more).
52
+ """
53
+ print_json_or_exit(
54
+ console,
55
+ lambda: api_client.get_all_product_orders(
56
+ limit=limit,
57
+ offset=offset,
58
+ user_id=user_id,
59
+ searchby=searchby,
60
+ ),
61
+ )
62
+
63
+ @app.command(
64
+ "list-all-sampling-orders",
65
+ help="GET /admin/sampling_orders/get_all — paginated list of all sampling orders (admin).",
66
+ )
67
+ def list_all_sampling_orders(
68
+ limit: int | None = typer.Option(
69
+ None,
70
+ "--limit",
71
+ "-n",
72
+ help="Optional page size.",
73
+ ),
74
+ offset: int = typer.Option(0, "--offset", help="Pagination offset."),
75
+ no_document_metadata: bool = typer.Option(
76
+ False,
77
+ "--no-document-metadata",
78
+ help="Set include_document_metadata=false (default on API is true).",
79
+ ),
80
+ ) -> None:
81
+ """
82
+ List all sampling orders across products (admin).
83
+
84
+ Input: optional --limit, --offset, and --no-document-metadata (query
85
+ include_document_metadata, default true when omitted).
86
+
87
+ Output: JSON from GET /admin/sampling_orders/get_all (success, orders, pagination, etc.).
88
+ """
89
+ include_meta: bool | None = False if no_document_metadata else None
90
+ print_json_or_exit(
91
+ console,
92
+ lambda: api_client.get_all_sampling_orders(
93
+ limit=limit,
94
+ offset=offset,
95
+ include_document_metadata=include_meta,
96
+ ),
97
+ )
98
+
99
+ @app.command(
100
+ "list-all-production-orders",
101
+ help="GET /admin/production_orders — paginated list of production / restock orders (admin).",
102
+ )
103
+ def list_all_production_orders(
104
+ limit: int | None = typer.Option(
105
+ None,
106
+ "--limit",
107
+ "-n",
108
+ help="Optional page size.",
109
+ ),
110
+ offset: int = typer.Option(0, "--offset", help="Pagination offset."),
111
+ order_type: str | None = typer.Option(
112
+ None,
113
+ "--type",
114
+ help="Filter: PRODUCTION or RESTOCK (omit for all types the API returns).",
115
+ ),
116
+ ) -> None:
117
+ """
118
+ List all production (bulk) orders (admin).
119
+
120
+ Use this (or list-production-orders-by-product) for **大货** PO data — not
121
+ list-product-orders alone.
122
+
123
+ Input: optional --limit, --offset, --type (PRODUCTION | RESTOCK).
124
+
125
+ Output: JSON from GET /admin/production_orders.
126
+ """
127
+ if order_type is not None:
128
+ ot = order_type.strip().upper()
129
+ if ot not in ("PRODUCTION", "RESTOCK"):
130
+ console.print(
131
+ "[red]--type must be PRODUCTION or RESTOCK (or omit).[/red]"
132
+ )
133
+ raise typer.Exit(1)
134
+ order_type = ot
135
+ print_json_or_exit(
136
+ console,
137
+ lambda: api_client.get_all_production_orders(
138
+ limit=limit,
139
+ offset=offset,
140
+ order_type=order_type,
141
+ ),
142
+ )
143
+
144
+ @app.command(
145
+ "list-sampling-orders-by-product",
146
+ help="GET /admin/products/{id}/sampling_orders — sampling orders for one product (admin).",
147
+ )
148
+ def list_sampling_orders_by_product(
149
+ product_reference_id: str = typer.Argument(
150
+ ...,
151
+ help="Product / product-order reference id (UUID path segment).",
152
+ ),
153
+ ) -> None:
154
+ """
155
+ List sampling orders for one product (admin).
156
+
157
+ Input: product_reference_id as URL path on
158
+ GET /admin/products/{product_reference_id}/sampling_orders.
159
+
160
+ Output: JSON { success, result } where result is a list of sampling order payloads.
161
+ """
162
+ print_json_or_exit(
163
+ console,
164
+ lambda: api_client.get_sampling_orders_by_product(product_reference_id),
165
+ )
166
+
167
+ @app.command(
168
+ "list-production-orders-by-product",
169
+ help=(
170
+ "GET /admin/products/{id}/production_orders — 大货 POs for one product; "
171
+ "see also list-all-production-orders."
172
+ ),
173
+ )
174
+ def list_production_orders_by_product(
175
+ product_reference_id: str = typer.Argument(
176
+ ...,
177
+ help="Product / product-order reference id (UUID path segment).",
178
+ ),
179
+ limit: int | None = typer.Option(
180
+ None,
181
+ "--limit",
182
+ "-n",
183
+ help="Optional page size.",
184
+ ),
185
+ offset: int = typer.Option(0, "--offset", help="Pagination offset."),
186
+ ) -> None:
187
+ """
188
+ List production orders for one product (admin).
189
+
190
+ Input: product_reference_id (path) and optional --limit / --offset query params on
191
+ GET /admin/products/{product_reference_id}/production_orders.
192
+
193
+ Output: JSON { success, production_orders, count, product_id }.
194
+ """
195
+ print_json_or_exit(
196
+ console,
197
+ lambda: api_client.get_production_orders_by_product(
198
+ product_reference_id,
199
+ limit=limit,
200
+ offset=offset,
201
+ ),
202
+ )
@@ -0,0 +1,3 @@
1
+ from makea_cli.commands.product.cmd import register
2
+
3
+ __all__ = ["register"]
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from makea_cli import api_client
11
+ from makea_cli.commands._util import print_json_or_exit
12
+
13
+ console = Console()
14
+
15
+
16
+ def _technical_specifications_for_persist(specs: list[Any]) -> list[dict[str, Any]]:
17
+ """Strip extract-only fields; keep the shape expected by POST technical-specifications."""
18
+ out: list[dict[str, Any]] = []
19
+ for raw in specs:
20
+ if not isinstance(raw, dict):
21
+ continue
22
+ if "component" not in raw:
23
+ continue
24
+ out.append(
25
+ {
26
+ "component": raw["component"],
27
+ "specification": list(raw.get("specification") or []),
28
+ "notes_and_quality_expectations": list(
29
+ raw.get("notes_and_quality_expectations") or []
30
+ ),
31
+ "need_proposal_from_supplier": bool(
32
+ raw.get("need_proposal_from_supplier", False)
33
+ ),
34
+ "documents": list(raw.get("documents") or []),
35
+ }
36
+ )
37
+ return out
38
+
39
+
40
+ def _load_technical_specifications_array(raw: Any) -> list[Any]:
41
+ """Accept root JSON as [...] or {\"technical_specifications\": [...]} or extract-shaped dict."""
42
+ if isinstance(raw, list):
43
+ return raw
44
+ if isinstance(raw, dict):
45
+ inner = raw.get("technical_specifications")
46
+ if isinstance(inner, list):
47
+ return inner
48
+ raise ValueError(
49
+ "JSON must be a list of specification objects, or an object with key "
50
+ "'technical_specifications' (e.g. output from extract-ai-tech-spec)."
51
+ )
52
+
53
+
54
+ def register(app: typer.Typer) -> None:
55
+ @app.command(
56
+ "extract-ai-tech-spec",
57
+ help="GET /admin/products/{id}/extract-tech-specifications — AI-generate specs (not saved).",
58
+ )
59
+ def extract_ai_tech_spec(
60
+ product_reference_id: str = typer.Argument(
61
+ ...,
62
+ help="Product / product-order reference id (UUID in the URL path).",
63
+ ),
64
+ timeout: float = typer.Option(
65
+ 300.0,
66
+ "--timeout",
67
+ "-t",
68
+ help="HTTP client timeout in seconds (AI extraction can be slow).",
69
+ min=30.0,
70
+ max=3600.0,
71
+ ),
72
+ ) -> None:
73
+ """
74
+ Run the admin AI pipeline to build Technical Specifications for one product.
75
+
76
+ This calls GET /admin/products/{product_reference_id}/extract-tech-specifications only.
77
+ Results are not written to the product; use save-product-tech-spec to persist.
78
+
79
+ Input: product_reference_id (path). Optional --timeout (default 300s).
80
+
81
+ Output: JSON with success, product_reference_id, technical_specifications, count,
82
+ optional review_notes; or success=false with error.
83
+ """
84
+ print_json_or_exit(
85
+ console,
86
+ lambda: api_client.extract_ai_tech_specifications(
87
+ product_reference_id,
88
+ timeout=timeout,
89
+ ),
90
+ )
91
+
92
+ @app.command(
93
+ "save-product-tech-spec",
94
+ help="POST /admin/products/{id}/technical-specifications — persist specs from a JSON file.",
95
+ )
96
+ def save_product_tech_spec(
97
+ product_reference_id: str = typer.Argument(
98
+ ...,
99
+ help="Product / product-order reference id (UUID in the URL path).",
100
+ ),
101
+ file: Path = typer.Option(
102
+ ...,
103
+ "--file",
104
+ "-f",
105
+ exists=True,
106
+ dir_okay=False,
107
+ readable=True,
108
+ help=(
109
+ "UTF-8 JSON: a list of spec objects, or an object with "
110
+ "'technical_specifications' (e.g. saved extract-ai-tech-spec output)."
111
+ ),
112
+ ),
113
+ timeout: float = typer.Option(
114
+ 120.0,
115
+ "--timeout",
116
+ "-t",
117
+ help="HTTP client timeout in seconds for the multipart POST.",
118
+ min=30.0,
119
+ max=600.0,
120
+ ),
121
+ ) -> None:
122
+ """
123
+ Save technical specifications onto the product (admin).
124
+
125
+ Calls POST /admin/products/{product_reference_id}/technical-specifications with
126
+ multipart form field ``technical_specifications`` set to a JSON array string, matching
127
+ the admin API contract.
128
+
129
+ Input: product_reference_id and --file. The file may be:
130
+ - a JSON array of component rows (same shape as ``technical_specifications`` in extract output);
131
+ - or a JSON object that includes ``technical_specifications`` (e.g. full response from
132
+ extract-ai-tech-spec redirected to a file).
133
+
134
+ Output: JSON from the API (success, technical_specifications, updated_by, message, etc.).
135
+ """
136
+ try:
137
+ raw = json.loads(file.read_text(encoding="utf-8"))
138
+ except (OSError, json.JSONDecodeError) as e:
139
+ console.print(f"[red]Invalid JSON file: {e}[/red]")
140
+ raise typer.Exit(1) from e
141
+ try:
142
+ arr = _load_technical_specifications_array(raw)
143
+ except ValueError as e:
144
+ console.print(f"[red]{e}[/red]")
145
+ raise typer.Exit(1) from e
146
+ specs = _technical_specifications_for_persist(arr)
147
+ if not specs:
148
+ console.print(
149
+ "[red]No valid specification rows after parsing (need 'component' on each).[/red]"
150
+ )
151
+ raise typer.Exit(1)
152
+
153
+ print_json_or_exit(
154
+ console,
155
+ lambda: api_client.update_product_technical_specifications(
156
+ product_reference_id,
157
+ specs,
158
+ timeout=timeout,
159
+ ),
160
+ )
@@ -0,0 +1,3 @@
1
+ from makea_cli.commands.supplier.cmd import register
2
+
3
+ __all__ = ["register"]
@@ -0,0 +1,305 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from makea_cli import api_client
10
+ from makea_cli.commands._util import print_json_or_exit
11
+
12
+ console = Console()
13
+
14
+
15
+ def _normalize_add_supplier_body(raw: dict) -> dict:
16
+ """API expects {"supplier": {...}}; allow file root to be the inner supplier object only."""
17
+ inner = raw.get("supplier")
18
+ if isinstance(inner, dict):
19
+ return raw
20
+ return {"supplier": raw}
21
+
22
+
23
+ def register(app: typer.Typer) -> None:
24
+ @app.command(
25
+ "list-suppliers",
26
+ help="GET /admin/supplier/get_all_suppliers — list all suppliers (admin).",
27
+ )
28
+ def list_suppliers() -> None:
29
+ """
30
+ List all suppliers (admin).
31
+
32
+ Input: none (GET /admin/supplier/get_all_suppliers).
33
+
34
+ Output: JSON body (typically success, result).
35
+ """
36
+ print_json_or_exit(console, api_client.get_all_suppliers)
37
+
38
+ @app.command(
39
+ "add-supplier",
40
+ help="POST /admin/supplier/add_supplier — create supplier from JSON file (admin).",
41
+ )
42
+ def add_supplier(
43
+ file: Path = typer.Option(
44
+ ...,
45
+ "--file",
46
+ "-f",
47
+ exists=True,
48
+ dir_okay=False,
49
+ readable=True,
50
+ help='Path to UTF-8 JSON: either the supplier object alone or {"supplier": {...}}.',
51
+ ),
52
+ ) -> None:
53
+ """
54
+ Create a supplier (admin).
55
+
56
+ HTTP: POST /admin/supplier/add_supplier with JSON body {\"supplier\": <object>}.
57
+
58
+ Input file
59
+ - Root may be the supplier fields object only (recommended), or the full envelope
60
+ {\"supplier\": {...}}.
61
+ - The server parses this with Supplier.to_domain; omitted supplier_id is assigned a
62
+ new UUID; created_at / updated_at are set server-side when missing.
63
+
64
+ Supplier object — fields the backend understands (all optional unless you need them
65
+ in the UI; empty strings are accepted for several core fields):
66
+
67
+ - Identity / classification: supplier_id (omit to auto-generate), name or
68
+ supplier_name, type or supplier_type, location or supplier_location,
69
+ manufacturer_type (string or list), sub_categories, status, website,
70
+ supplier_user_id (Cognito user id if the supplier logs in).
71
+ - Profile: specialization or specialisation, description, internal_notes,
72
+ rating_internal, number_of_employees or factory_size, year_established or
73
+ established_in, machinery_equipment, monthly_capacity, leadtime_guidance,
74
+ sample_leadtime_guidance, MOQ_guidance, MOQ_guidance_type, moq_by_category,
75
+ trade_terms, sourcing_support, tech_pack_support, impexp_license,
76
+ impexp_license_experience, customs_experience, stock_availabilities,
77
+ number_of_in_stock_product, product_update_methods, payment_terms_guidance
78
+ (string or list), currency (string or list), primary_markets, clients,
79
+ end_consumer_focus, material_system, qc_options, has_inhouse_product_development_team,
80
+ product_development_structure, has_sourcing_support, business_registration_number,
81
+ parent_company, extra_info, onboard_source.
82
+ - Primary contact: contact_name, contact_email, contact_phone, contact_title,
83
+ contact_whatsapp_wechat_id.
84
+ - point_of_contact: list of {name, email, phone, title?, whatsapp_wechat_id?}.
85
+ - certificates: list of {name, id_number, file_url?}.
86
+ - Documents (each item a Document-shaped dict): catalogue, images, videos,
87
+ business_license, tax_registration_certificate, insurance_certificate.
88
+ - banking_details (optional object): bank_region (CHINA|EUROPE|INDIA|USA|OTHER),
89
+ beneficiary_name, beneficiary_address, bank_name, bank_address, swift_code;
90
+ iban (EU), account_number (USA/China/India/Other), routing_number (USA),
91
+ ifsc_code (India), phone_number (China), banking_info_files (document list).
92
+
93
+ Output: JSON {success, result} with HTTP 201 on success; result is the stored supplier
94
+ as a flat dict (dataclass asdict), including the assigned supplier_id.
95
+ """
96
+ try:
97
+ raw = json.loads(file.read_text(encoding="utf-8"))
98
+ except (OSError, json.JSONDecodeError) as e:
99
+ console.print(f"[red]Invalid JSON file: {e}[/red]")
100
+ raise typer.Exit(1) from e
101
+ if not isinstance(raw, dict):
102
+ console.print("[red]JSON root must be an object.[/red]")
103
+ raise typer.Exit(1)
104
+ body = _normalize_add_supplier_body(raw)
105
+ print_json_or_exit(console, lambda: api_client.add_supplier(body))
106
+
107
+ @app.command(
108
+ "backfill-supplier-id",
109
+ help=(
110
+ "Migrate supplier_id from a wrong UUID to the Cognito user sub (user_id): "
111
+ "profile + Cognito supplierReferenceId, optional S3/DOCUMENT#, patch product "
112
+ "supplier_links, link→quote-request backfill (archived migration QRs), "
113
+ "sampling/production order supplier fields, optional delete old SUPPLIER# "
114
+ "partition. Default is dry-run; use --apply to write. "
115
+ "HTTP: POST /admin/supplier/backfill_supplier_id_with_quote_requests."
116
+ ),
117
+ )
118
+ def backfill_supplier_id(
119
+ user_id: str = typer.Option(
120
+ ...,
121
+ "--user-id",
122
+ help=(
123
+ "Cognito sub of the supplier user — this becomes the canonical supplier_id "
124
+ "after migration."
125
+ ),
126
+ ),
127
+ old_supplier_id: str = typer.Option(
128
+ ...,
129
+ "--old-supplier-id",
130
+ help=(
131
+ "Current wrong supplier_id stored on SUPPLIER#.../METADATA and links "
132
+ "(the UUID to migrate away from)."
133
+ ),
134
+ ),
135
+ dry_run: bool = typer.Option(
136
+ True,
137
+ "--dry-run/--apply",
138
+ help=(
139
+ "Default: --dry-run (server previews only). "
140
+ "--apply sends dry_run=false and performs Dynamo/Cognito/S3 writes per flags."
141
+ ),
142
+ ),
143
+ skip_s3_documents: bool = typer.Option(
144
+ False,
145
+ "--skip-s3-documents",
146
+ help=(
147
+ "Skip server step 2b: copy suppliers/{old}/documents/* in S3, rewrite "
148
+ "METADATA s3_key paths, migrate DOCUMENT# rows to SUPPLIER#{user_id}."
149
+ ),
150
+ ),
151
+ skip_quote_request_backfill: bool = typer.Option(
152
+ False,
153
+ "--skip-quote-request-backfill",
154
+ help=(
155
+ "Skip per-product link→quote_request backfill AND step 5b (no automatic "
156
+ "archiving of QRs still keyed by old_supplier_id from this run)."
157
+ ),
158
+ ),
159
+ quote_request_only: bool = typer.Option(
160
+ False,
161
+ "--quote-request-only",
162
+ help=(
163
+ "Server skips profile creation, Cognito, link supplier_id patch, and "
164
+ "order patches; only scans products and runs link→QR backfill (+ 5b). "
165
+ "Use when profile/links are already fixed."
166
+ ),
167
+ ),
168
+ delete_old_supplier_at_end: bool = typer.Option(
169
+ False,
170
+ "--delete-old-supplier-at-end",
171
+ help=(
172
+ "After a successful full run, delete all items with PK=SUPPLIER#{old_id}. "
173
+ "Server refuses if old METADATA is not marked migrated or if QRs still "
174
+ "use old_supplier_id. Non-dry-run requires --confirm-delete-old-supplier."
175
+ ),
176
+ ),
177
+ confirm_delete_old_supplier: bool = typer.Option(
178
+ False,
179
+ "--confirm-delete-old-supplier",
180
+ help=(
181
+ "Acknowledge destructive delete of the old supplier partition. Required "
182
+ "with --delete-old-supplier-at-end when using --apply (server enforces too)."
183
+ ),
184
+ ),
185
+ ) -> None:
186
+ """
187
+ Consolidated supplier **user_id ↔ supplier_id** migration (admin).
188
+
189
+ **When to use**
190
+
191
+ Production expects ``supplier_id`` to equal the supplier's Cognito ``sub``
192
+ (``user_id``). If the DynamoDB profile was created under a random UUID
193
+ (``old_supplier_id``), this command runs the full migration on the server.
194
+
195
+ **What runs on the server** (same as ``manage.py backfill_supplier_id``)
196
+
197
+ - Write new profile under ``SUPPLIER#{user_id}``, mark old as migrated.
198
+ - Set Cognito ``custom:supplierReferenceId`` → ``user_id``.
199
+ - Unless ``--skip-s3-documents``: S3 document copy + ``DOCUMENT#`` row migration.
200
+ - Patch ``supplier_links[].supplier_id`` from old → ``user_id``.
201
+ - Link→quote-request backfill (unless skipped); migration QRs are archived.
202
+ - Patch sampling/production orders referencing the old id.
203
+ - Optionally delete the old ``SUPPLIER#{old_supplier_id}`` partition at end.
204
+
205
+ **Not available through this API** (use garmentby-service venv + ``manage.py``):
206
+
207
+ - ``--s3-documents-only``
208
+ - ``--delete-old-supplier-only``
209
+
210
+ **Output**
211
+
212
+ JSON includes ``output`` (server command stdout) and ``errors`` (stderr). Read
213
+ them after dry-run before ``--apply``.
214
+
215
+ **After migration**
216
+
217
+ If quote request **stage/status** still look wrong, repair with
218
+ ``manage.py reconcile_quote_request_stage_status`` on **garmentby-service**
219
+ for the **new** supplier id (usually ``user_id``).
220
+
221
+ **Skill**
222
+
223
+ Operator runbook: ``.claude/skills/supplier-user-id-migration/SKILL.md`` in this repo.
224
+ """
225
+ if (
226
+ delete_old_supplier_at_end
227
+ and not confirm_delete_old_supplier
228
+ and not dry_run
229
+ ):
230
+ console.print(
231
+ "[red]--confirm-delete-old-supplier is required with "
232
+ "--delete-old-supplier-at-end when using --apply.[/red]"
233
+ )
234
+ raise typer.Exit(1)
235
+
236
+ print_json_or_exit(
237
+ console,
238
+ lambda: api_client.backfill_supplier_id_with_quote_requests(
239
+ user_id=user_id,
240
+ old_supplier_id=old_supplier_id,
241
+ dry_run=dry_run,
242
+ skip_s3_documents=skip_s3_documents,
243
+ skip_quote_request_backfill=skip_quote_request_backfill,
244
+ quote_request_only=quote_request_only,
245
+ delete_old_supplier_at_end=delete_old_supplier_at_end,
246
+ confirm_delete_old_supplier=confirm_delete_old_supplier,
247
+ ),
248
+ )
249
+
250
+ @app.command(
251
+ "get-quote-requests-by-supplier-id",
252
+ help=(
253
+ "GET /admin/quote_requests/supplier/{supplier_id} — direct backend query."
254
+ ),
255
+ )
256
+ def get_quote_requests_by_supplier_id(
257
+ supplier_id: str = typer.Argument(..., help="Supplier id to filter by."),
258
+ status: str | None = typer.Option(
259
+ None,
260
+ "--status",
261
+ help="Optional quote request status filter.",
262
+ ),
263
+ limit: int | None = typer.Option(
264
+ None,
265
+ "--limit",
266
+ "-n",
267
+ help="Optional max quote requests to return (backend-enforced).",
268
+ ),
269
+ ) -> None:
270
+ """
271
+ Direct backend lookup by supplier id.
272
+ """
273
+ print_json_or_exit(
274
+ console,
275
+ lambda: api_client.get_quote_requests_by_supplier_id(
276
+ supplier_id=supplier_id,
277
+ status=status,
278
+ limit=limit,
279
+ ),
280
+ )
281
+
282
+ @app.command(
283
+ "get-supplier-links-by-supplier-id",
284
+ help=(
285
+ "GET /admin/product/linked_suppliers/by_supplier/{supplier_id} — direct backend query."
286
+ ),
287
+ )
288
+ def get_supplier_links_by_supplier_id(
289
+ supplier_id: str = typer.Argument(..., help="Supplier id to filter by."),
290
+ link_type: str | None = typer.Option(
291
+ None,
292
+ "--link-type",
293
+ help="Optional filter: SAMPLING or PRODUCTION.",
294
+ ),
295
+ ) -> None:
296
+ """
297
+ Direct backend lookup by supplier id.
298
+ """
299
+ print_json_or_exit(
300
+ console,
301
+ lambda: api_client.get_supplier_links_by_supplier_id(
302
+ supplier_id=supplier_id,
303
+ link_type=link_type,
304
+ ),
305
+ )