makea-cli 0.2.13__tar.gz → 0.2.15__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.
- {makea_cli-0.2.13 → makea_cli-0.2.15}/CHANGELOG.md +36 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/PKG-INFO +3 -3
- {makea_cli-0.2.13 → makea_cli-0.2.15}/README.md +2 -2
- makea_cli-0.2.15/makea_cli/__init__.py +1 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/api_client.py +14 -6
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/metrics/cmd.py +27 -65
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/orders/cmd.py +7 -5
- {makea_cli-0.2.13 → makea_cli-0.2.15}/pyproject.toml +1 -1
- {makea_cli-0.2.13 → makea_cli-0.2.15}/tests/golden_manifest.json +6 -6
- makea_cli-0.2.15/tests/test_currency_conversion.py +106 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/tests/test_pm_reads.py +14 -3
- makea_cli-0.2.13/makea_cli/__init__.py +0 -1
- {makea_cli-0.2.13 → makea_cli-0.2.15}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/.claude/skills/link-supplier-user/SKILL.md +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/.gitignore +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea-collection-upload/SKILL.md +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea-collection-upload/scripts/create_products.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/__main__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/auth_pkce.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/broker.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/_util.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/analytics/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/analytics/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/auth/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/auth/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/brand/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/brand/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/designer/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/designer/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/directory/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/directory/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/document/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/document/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/email/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/email/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/factory/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/factory/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/library/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/library/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/metrics/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/orders/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/product/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/product/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/production/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/production/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/production/pricing_spec.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/supplier/__init__.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/commands/supplier/cmd.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/config.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/errors.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/identity.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/main.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/output.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/posthog_client.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/makea_cli/registry.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/npm/makea-cli/README.md +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/npm/makea-cli/makea-cli.mjs +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/npm/makea-cli/package.json +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/tests/test_broker.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/tests/test_output_contract.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/tests/test_persona_commands.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/tests/test_registry.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/tests/test_supplier_scorecard.py +0 -0
- {makea_cli-0.2.13 → makea_cli-0.2.15}/uv.lock +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.15
|
|
4
|
+
|
|
5
|
+
**Behaviour change: `admin get currency-conversion` returns different
|
|
6
|
+
numbers.** This is not only a bug fix. Anyone who reconciled figures with
|
|
7
|
+
this command should expect them to move, by up to several percent per
|
|
8
|
+
currency. The same call on 2026-10-16:
|
|
9
|
+
|
|
10
|
+
| GBP per | 0.2.14 | 0.2.15 | 1,000 converted, 0.2.14 → 0.2.15 |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| CNY | 0.12 | 0.113 | 120.00 → 113.00 (−5.8%) |
|
|
13
|
+
| USD | 0.76 | 0.752 | 760.00 → 752.00 |
|
|
14
|
+
| EUR | 0.85 | 0.848 | 850.00 → 848.00 |
|
|
15
|
+
|
|
16
|
+
What changed:
|
|
17
|
+
|
|
18
|
+
- Rates now come from the platform (`GET /admin/fx/rates`, backend
|
|
19
|
+
2026-10-16) instead of the CLI fetching the free FX source itself. They are
|
|
20
|
+
rounded **up to 3 decimals**, as the platform rounds them; 0.2.14 rounded
|
|
21
|
+
up to 2, which overstated cheap currencies such as CNY most.
|
|
22
|
+
- With one `--from` and `--amount`, `converted` is the platform's own
|
|
23
|
+
conversion, rounded **up** to the cent; 0.2.14 rounded to the nearest cent.
|
|
24
|
+
- So the numbers now match the platform's prices and revenue metrics, which
|
|
25
|
+
the 0.2.14 ones did not.
|
|
26
|
+
- Output keys are unchanged except `rounding` (now `"up"`, was
|
|
27
|
+
`"ceil_2dp"`), plus new `rate_decimals` and `unavailable`. A currency with
|
|
28
|
+
no rate still fails the command. Needs an admin login, like every `admin`
|
|
29
|
+
command.
|
|
30
|
+
|
|
31
|
+
## 0.2.14
|
|
32
|
+
|
|
33
|
+
- `admin update tracking-status`: help text no longer describes it as "the
|
|
34
|
+
17track webhook path". The 17track integration (registration and webhook)
|
|
35
|
+
was removed from the backend on 2026-10-10; this command, fed by the
|
|
36
|
+
tracking-sweep skill, is now the only way a carrier status reaches an
|
|
37
|
+
order. Behaviour is unchanged.
|
|
38
|
+
|
|
3
39
|
## 0.2.13
|
|
4
40
|
|
|
5
41
|
- `designer get supplier-scorecard [SUPPLIER_ID]`: a factory's track record
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: makea-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: Makea admin CLI (browser login + Makea admin HTTP API)
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: click<9,>=8.1
|
|
@@ -100,7 +100,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
100
100
|
| `makea-cli admin generate pricing-groups` | write(ask) | `POST /admin/production_orders/{id}/details/update` | Lay a production order's variants into pricing groups along one axis (--by style \| colour \| variant \| all) with th… | `production-groups-generate` |
|
|
101
101
|
| `makea-cli admin get active-users` | read | `POST /admin/analytics/query` | Distinct active users per day over the last N days (DAU series). | `analytics-active-users` |
|
|
102
102
|
| `makea-cli admin get analytics-query` | read | `POST /admin/analytics/query` | Run an arbitrary HogQL query (escape hatch). Example: analytics-query "SELECT count() FROM events" | `analytics-query` |
|
|
103
|
-
| `makea-cli admin get currency-conversion` | read | `
|
|
103
|
+
| `makea-cli admin get currency-conversion` | read | `GET /admin/fx/rates` | the platform's own exchange rates and rounding (rate table for several --from; one --from with --amount converts it). | `convert-currency` |
|
|
104
104
|
| `makea-cli admin get feature-usage` | read | `POST /admin/analytics/query` | How many distinct users used a feature recently, plus event volume. Matches $pageview path and feature_time_spent eve… | `analytics-feature-usage` |
|
|
105
105
|
| `makea-cli admin get library-schema` | read | `GET /admin/library_components/schema` | the tech-pack library taxonomy: sections and their library-role field keys (run this before create/update). | `library-schema` |
|
|
106
106
|
| `makea-cli admin get production-pricing` | read | `GET /admin/production_orders/{id}/details` | show a production order's price tiers, MOQ, sizes/colours, pricing groups and cart coverage. | `production-pricing-show` |
|
|
@@ -131,7 +131,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
131
131
|
| `makea-cli admin update quote-request` | write(ask) | `PATCH /admin/quote_requests/{quote_request_id}/update` | set a quote request's stage, status and/or due date. Only what you pass is changed. This is the door that unblocks a … | `update-quote-request` |
|
|
132
132
|
| `makea-cli admin update supplier` | write(ask) | `PUT /admin/supplier/update_supplier` | update an existing supplier from a JSON file (admin). Run `makea-cli get-supplier-profile-schema` FIRST to get the fi… | `update-supplier` |
|
|
133
133
|
| `makea-cli admin update tech-spec` | write(ask) | `POST /admin/products/{id}/technical-specifications` | persist specs from a JSON file. | `save-product-tech-spec` |
|
|
134
|
-
| `makea-cli admin update tracking-status` | write(ask) | `POST /admin/tracking/carrier_status` | apply a carrier status looked up by hand (tracking-sweep skill) to one order
|
|
134
|
+
| `makea-cli admin update tracking-status` | write(ask) | `POST /admin/tracking/carrier_status` | apply a carrier status looked up by hand (tracking-sweep skill) to one order: status, fan-out, delivered emails (--silent: state only). | — |
|
|
135
135
|
| `makea-cli admin update user` | write(ask) | `PUT /admin/user-profiles/{user_id}/update` | admin updates DynamoDB user profile fields and/or Cognito custom:userType. Allowed --user-type values: "Fashion brand… | `update-user-profile` |
|
|
136
136
|
| `makea-cli admin upload financial-document` | write(auto) | `POST /admin/products/{product_id}/financial_documents` | upload one or more files to AWS S3 and return financial document metadata. | `upload-financial-documents` |
|
|
137
137
|
| `makea-cli admin upload link-document` | write(auto) | `POST /admin/upload_link_document` | upload a file to AWS S3 and return document domain metadata for reuse as input in other commands. | `upload-link-document` |
|
|
@@ -88,7 +88,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
88
88
|
| `makea-cli admin generate pricing-groups` | write(ask) | `POST /admin/production_orders/{id}/details/update` | Lay a production order's variants into pricing groups along one axis (--by style \| colour \| variant \| all) with th… | `production-groups-generate` |
|
|
89
89
|
| `makea-cli admin get active-users` | read | `POST /admin/analytics/query` | Distinct active users per day over the last N days (DAU series). | `analytics-active-users` |
|
|
90
90
|
| `makea-cli admin get analytics-query` | read | `POST /admin/analytics/query` | Run an arbitrary HogQL query (escape hatch). Example: analytics-query "SELECT count() FROM events" | `analytics-query` |
|
|
91
|
-
| `makea-cli admin get currency-conversion` | read | `
|
|
91
|
+
| `makea-cli admin get currency-conversion` | read | `GET /admin/fx/rates` | the platform's own exchange rates and rounding (rate table for several --from; one --from with --amount converts it). | `convert-currency` |
|
|
92
92
|
| `makea-cli admin get feature-usage` | read | `POST /admin/analytics/query` | How many distinct users used a feature recently, plus event volume. Matches $pageview path and feature_time_spent eve… | `analytics-feature-usage` |
|
|
93
93
|
| `makea-cli admin get library-schema` | read | `GET /admin/library_components/schema` | the tech-pack library taxonomy: sections and their library-role field keys (run this before create/update). | `library-schema` |
|
|
94
94
|
| `makea-cli admin get production-pricing` | read | `GET /admin/production_orders/{id}/details` | show a production order's price tiers, MOQ, sizes/colours, pricing groups and cart coverage. | `production-pricing-show` |
|
|
@@ -119,7 +119,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
119
119
|
| `makea-cli admin update quote-request` | write(ask) | `PATCH /admin/quote_requests/{quote_request_id}/update` | set a quote request's stage, status and/or due date. Only what you pass is changed. This is the door that unblocks a … | `update-quote-request` |
|
|
120
120
|
| `makea-cli admin update supplier` | write(ask) | `PUT /admin/supplier/update_supplier` | update an existing supplier from a JSON file (admin). Run `makea-cli get-supplier-profile-schema` FIRST to get the fi… | `update-supplier` |
|
|
121
121
|
| `makea-cli admin update tech-spec` | write(ask) | `POST /admin/products/{id}/technical-specifications` | persist specs from a JSON file. | `save-product-tech-spec` |
|
|
122
|
-
| `makea-cli admin update tracking-status` | write(ask) | `POST /admin/tracking/carrier_status` | apply a carrier status looked up by hand (tracking-sweep skill) to one order
|
|
122
|
+
| `makea-cli admin update tracking-status` | write(ask) | `POST /admin/tracking/carrier_status` | apply a carrier status looked up by hand (tracking-sweep skill) to one order: status, fan-out, delivered emails (--silent: state only). | — |
|
|
123
123
|
| `makea-cli admin update user` | write(ask) | `PUT /admin/user-profiles/{user_id}/update` | admin updates DynamoDB user profile fields and/or Cognito custom:userType. Allowed --user-type values: "Fashion brand… | `update-user-profile` |
|
|
124
124
|
| `makea-cli admin upload financial-document` | write(auto) | `POST /admin/products/{product_id}/financial_documents` | upload one or more files to AWS S3 and return financial document metadata. | `upload-financial-documents` |
|
|
125
125
|
| `makea-cli admin upload link-document` | write(auto) | `POST /admin/upload_link_document` | upload a file to AWS S3 and return document domain metadata for reuse as input in other commands. | `upload-link-document` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.15"
|
|
@@ -844,6 +844,18 @@ def get_cumulative_payment_and_platform_revenue(
|
|
|
844
844
|
)
|
|
845
845
|
|
|
846
846
|
|
|
847
|
+
def get_fx_rates(*, to: str, sources: list[str], amount: float | None = None) -> Any:
|
|
848
|
+
"""
|
|
849
|
+
GET /admin/fx/rates — the platform's own exchange rates (rounded up to its
|
|
850
|
+
RATE_DECIMALS), and with one source and an amount, that amount converted the
|
|
851
|
+
way the platform converts it.
|
|
852
|
+
"""
|
|
853
|
+
params: dict[str, Any] = {"to": to, "from": ",".join(sources)}
|
|
854
|
+
if amount is not None:
|
|
855
|
+
params["amount"] = amount
|
|
856
|
+
return get_json("/admin/fx/rates", params=params)
|
|
857
|
+
|
|
858
|
+
|
|
847
859
|
def extract_ai_tech_specifications(product_reference_id: str, *, timeout: float = 300.0) -> Any:
|
|
848
860
|
"""
|
|
849
861
|
GET /admin/products/{id}/extract-tech-specifications — synchronous AI extraction;
|
|
@@ -1576,9 +1588,7 @@ def designer_list_milestones(product_id: str) -> Any:
|
|
|
1576
1588
|
params={"type": "PRODUCTION"},
|
|
1577
1589
|
)
|
|
1578
1590
|
orders = (
|
|
1579
|
-
raw.get("production_orders_details") or raw.get("orders")
|
|
1580
|
-
if isinstance(raw, dict)
|
|
1581
|
-
else raw
|
|
1591
|
+
raw.get("production_orders_details") or raw.get("orders") if isinstance(raw, dict) else raw
|
|
1582
1592
|
)
|
|
1583
1593
|
out = []
|
|
1584
1594
|
for order in orders or []:
|
|
@@ -1604,9 +1614,7 @@ def designer_list_milestones(product_id: str) -> Any:
|
|
|
1604
1614
|
return {"product_id": product_id, "milestones": out}
|
|
1605
1615
|
|
|
1606
1616
|
|
|
1607
|
-
def designer_list_notes(
|
|
1608
|
-
product_id: str, channel_key: str, *, since: str | None = None
|
|
1609
|
-
) -> Any:
|
|
1617
|
+
def designer_list_notes(product_id: str, channel_key: str, *, since: str | None = None) -> Any:
|
|
1610
1618
|
"""A channel's messages, oldest first, optionally only those after
|
|
1611
1619
|
``since`` (ISO date or datetime): what the factory said this week."""
|
|
1612
1620
|
raw = get_json(f"/designer/products/{product_id}/channels/{channel_key}/notes")
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import math
|
|
4
|
-
|
|
5
|
-
import httpx
|
|
6
3
|
import typer
|
|
7
4
|
from rich.console import Console
|
|
8
5
|
|
|
@@ -16,38 +13,6 @@ console = Console()
|
|
|
16
13
|
_GRANULARITIES = ("day", "week", "month")
|
|
17
14
|
_ORDER_TYPES = ("all", "sampling", "production")
|
|
18
15
|
|
|
19
|
-
# Same free FX source the backend uses (FreeCurrencyService), so converted
|
|
20
|
-
# amounts match the platform's revenue metrics.
|
|
21
|
-
_FX_PRIMARY = (
|
|
22
|
-
"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/{base}.json"
|
|
23
|
-
)
|
|
24
|
-
_FX_FALLBACK = "https://latest.currency-api.pages.dev/v1/currencies/{base}.json"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _fx_rate(base: str, target: str) -> float:
|
|
28
|
-
"""Exchange rate base -> target, rounded UP to 2dp exactly like the backend
|
|
29
|
-
(math.ceil(raw * 100) / 100) so totals reconcile with revenue metrics."""
|
|
30
|
-
base, target = base.lower(), target.lower()
|
|
31
|
-
if base == target:
|
|
32
|
-
return 1.0
|
|
33
|
-
last_err: Exception | None = None
|
|
34
|
-
for url in (_FX_PRIMARY.format(base=base), _FX_FALLBACK.format(base=base)):
|
|
35
|
-
try:
|
|
36
|
-
with httpx.Client(timeout=10) as client:
|
|
37
|
-
resp = client.get(url)
|
|
38
|
-
resp.raise_for_status()
|
|
39
|
-
data = resp.json()
|
|
40
|
-
raw = data.get(base, {}).get(target)
|
|
41
|
-
if raw is not None:
|
|
42
|
-
return math.ceil(float(raw) * 100) / 100
|
|
43
|
-
except Exception as e: # noqa: BLE001 - try fallback URL
|
|
44
|
-
last_err = e
|
|
45
|
-
continue
|
|
46
|
-
raise RuntimeError(
|
|
47
|
-
f"FX rate {base.upper()}->{target.upper()} unavailable"
|
|
48
|
-
+ (f": {last_err}" if last_err else "")
|
|
49
|
-
)
|
|
50
|
-
|
|
51
16
|
|
|
52
17
|
def register(app: typer.Typer) -> None:
|
|
53
18
|
@command(
|
|
@@ -141,13 +106,13 @@ def register(app: typer.Typer) -> None:
|
|
|
141
106
|
"get",
|
|
142
107
|
"currency-conversion",
|
|
143
108
|
legacy="convert-currency",
|
|
144
|
-
api="GET
|
|
145
|
-
external="currency-api",
|
|
109
|
+
api="GET /admin/fx/rates",
|
|
146
110
|
help=(
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"one call, then convert
|
|
111
|
+
"GET /admin/fx/rates — convert money between currencies at the "
|
|
112
|
+
"platform's own rates and rounding, so totals reconcile with its "
|
|
113
|
+
"prices and revenue metrics. Pass several source currencies via --from "
|
|
114
|
+
"(comma-separated) to get a full rate table in one call, then convert "
|
|
115
|
+
"locally — avoids repeated lookups."
|
|
151
116
|
),
|
|
152
117
|
)
|
|
153
118
|
def convert_currency(
|
|
@@ -165,17 +130,20 @@ def register(app: typer.Typer) -> None:
|
|
|
165
130
|
help="Optional amount to convert (only meaningful with a single --from).",
|
|
166
131
|
),
|
|
167
132
|
) -> None:
|
|
168
|
-
"""Return source->target rates
|
|
169
|
-
|
|
133
|
+
"""Return source->target rates, and optionally a converted amount, as the
|
|
134
|
+
platform computes them.
|
|
170
135
|
|
|
171
136
|
Use it to collapse a mixed-currency set into one currency: collect the
|
|
172
137
|
distinct currencies, call once with --from a,b,c to get every rate, then
|
|
173
|
-
multiply locally.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{"
|
|
178
|
-
|
|
138
|
+
multiply locally. The rates are the platform's (FreeCurrencyService,
|
|
139
|
+
rounded up to ``rate_decimals``) and come from the backend rather than
|
|
140
|
+
being fetched and rounded here, so they cannot drift from it.
|
|
141
|
+
|
|
142
|
+
Output JSON: {"to", "rates": {CUR: rate, ...}, "rate_decimals",
|
|
143
|
+
"rounding", "source", "unavailable"} plus {"from", "rate", "amount",
|
|
144
|
+
"converted"} when a single --from and --amount are given; "converted" is
|
|
145
|
+
the platform's own conversion, rounding included. A currency with no
|
|
146
|
+
rate fails the command rather than coming back null.
|
|
179
147
|
"""
|
|
180
148
|
sources = [s.strip() for s in from_.split(",") if s.strip()]
|
|
181
149
|
if not sources:
|
|
@@ -183,22 +151,16 @@ def register(app: typer.Typer) -> None:
|
|
|
183
151
|
raise typer.Exit(1)
|
|
184
152
|
|
|
185
153
|
def _do() -> dict:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
"from": sources[0].upper(),
|
|
198
|
-
"rate": rate,
|
|
199
|
-
"amount": amount,
|
|
200
|
-
"converted": round(amount * rate, 2),
|
|
201
|
-
}
|
|
154
|
+
payload = api_client.get_fx_rates(
|
|
155
|
+
to=to,
|
|
156
|
+
sources=sources,
|
|
157
|
+
amount=amount if len(sources) == 1 else None,
|
|
158
|
+
)
|
|
159
|
+
out = (payload or {}).get("result") or {}
|
|
160
|
+
if out.get("unavailable"):
|
|
161
|
+
raise RuntimeError(
|
|
162
|
+
"FX rate unavailable: "
|
|
163
|
+
+ ", ".join(f"{s}->{to.upper()}" for s in out["unavailable"])
|
|
202
164
|
)
|
|
203
165
|
return out
|
|
204
166
|
|
|
@@ -323,14 +323,14 @@ def register(app: typer.Typer) -> None:
|
|
|
323
323
|
api="POST /admin/tracking/carrier_status",
|
|
324
324
|
side_effects=[
|
|
325
325
|
"moves the order to delivered when the status says so",
|
|
326
|
-
"emails and messages the designer, supplier and admins
|
|
326
|
+
"emails and messages the designer, supplier and admins that the parcel arrived (none with --silent)",
|
|
327
327
|
],
|
|
328
328
|
approval="ask",
|
|
329
329
|
idempotent=True,
|
|
330
330
|
help=(
|
|
331
331
|
"POST /admin/tracking/carrier_status — apply a carrier status looked up by hand "
|
|
332
|
-
"(the tracking-sweep skill) to one order
|
|
333
|
-
"
|
|
332
|
+
"(the tracking-sweep skill) to one order: status, fan-out to every order "
|
|
333
|
+
"sharing the number, and the delivered emails."
|
|
334
334
|
),
|
|
335
335
|
)
|
|
336
336
|
def update_tracking_status(
|
|
@@ -341,7 +341,9 @@ def register(app: typer.Typer) -> None:
|
|
|
341
341
|
order_id: str = typer.Argument(..., help="Order id (product id for reference_sample)."),
|
|
342
342
|
tracking_number: str = typer.Argument(..., help="The parcel's tracking number."),
|
|
343
343
|
status: str = typer.Option(
|
|
344
|
-
...,
|
|
344
|
+
...,
|
|
345
|
+
"--status",
|
|
346
|
+
help="Carrier status as the public 17track page shows it, e.g. Delivered, InTransit.",
|
|
345
347
|
),
|
|
346
348
|
sub_status: str | None = typer.Option(None, "--sub-status"),
|
|
347
349
|
event: str | None = typer.Option(None, "--event", help="Latest carrier event text."),
|
|
@@ -358,7 +360,7 @@ def register(app: typer.Typer) -> None:
|
|
|
358
360
|
"""
|
|
359
361
|
Tell Makea what the carrier says about one parcel.
|
|
360
362
|
|
|
361
|
-
Output: JSON
|
|
363
|
+
Output: JSON: success, status, and one entry per
|
|
362
364
|
order the number was fanned out to. Repeating a Delivered update is a
|
|
363
365
|
no-op (no second email). --silent moves the orders without telling
|
|
364
366
|
anyone.
|
|
@@ -980,8 +980,8 @@
|
|
|
980
980
|
"verb": "get",
|
|
981
981
|
"noun": "currency-conversion",
|
|
982
982
|
"access": "read",
|
|
983
|
-
"api": "GET
|
|
984
|
-
"external":
|
|
983
|
+
"api": "GET /admin/fx/rates",
|
|
984
|
+
"external": null,
|
|
985
985
|
"side_effects": [],
|
|
986
986
|
"approval": "auto",
|
|
987
987
|
"reversible": null,
|
|
@@ -990,7 +990,7 @@
|
|
|
990
990
|
"legacy": [
|
|
991
991
|
"convert-currency"
|
|
992
992
|
],
|
|
993
|
-
"help": "
|
|
993
|
+
"help": "GET /admin/fx/rates — convert money between currencies at the platform's own rates and rounding, so totals reconcile with its prices and revenue metrics. Pass several source currencies via --from (comma-separated) to get a full rate table in one call, then convert locally — avoids repeated lookups.",
|
|
994
994
|
"function": "makea_cli.commands.metrics.cmd.register.<locals>.convert_currency",
|
|
995
995
|
"flags": [
|
|
996
996
|
{
|
|
@@ -3574,14 +3574,14 @@
|
|
|
3574
3574
|
"external": null,
|
|
3575
3575
|
"side_effects": [
|
|
3576
3576
|
"moves the order to delivered when the status says so",
|
|
3577
|
-
"emails and messages the designer, supplier and admins
|
|
3577
|
+
"emails and messages the designer, supplier and admins that the parcel arrived (none with --silent)"
|
|
3578
3578
|
],
|
|
3579
3579
|
"approval": "ask",
|
|
3580
3580
|
"reversible": null,
|
|
3581
3581
|
"idempotent": true,
|
|
3582
3582
|
"limit": null,
|
|
3583
3583
|
"legacy": [],
|
|
3584
|
-
"help": "POST /admin/tracking/carrier_status — apply a carrier status looked up by hand (the tracking-sweep skill) to one order
|
|
3584
|
+
"help": "POST /admin/tracking/carrier_status — apply a carrier status looked up by hand (the tracking-sweep skill) to one order: status, fan-out to every order sharing the number, and the delivered emails.",
|
|
3585
3585
|
"function": "makea_cli.commands.orders.cmd.register.<locals>.update_tracking_status",
|
|
3586
3586
|
"flags": [
|
|
3587
3587
|
{
|
|
@@ -3630,7 +3630,7 @@
|
|
|
3630
3630
|
"required": true,
|
|
3631
3631
|
"multiple": false,
|
|
3632
3632
|
"default": null,
|
|
3633
|
-
"help": "17track
|
|
3633
|
+
"help": "Carrier status as the public 17track page shows it, e.g. Delivered, InTransit."
|
|
3634
3634
|
},
|
|
3635
3635
|
{
|
|
3636
3636
|
"name": "sub_status",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""`admin get currency-conversion` reads the platform's rates from the backend.
|
|
2
|
+
|
|
3
|
+
It used to fetch the free FX source itself and round rates to 2 decimals and
|
|
4
|
+
amounts to the nearest cent, while the platform rounds rates up to 3 decimals
|
|
5
|
+
and amounts up to the cent -- so its totals drifted from the platform's. It
|
|
6
|
+
now asks GET /admin/fx/rates and returns what that says.
|
|
7
|
+
|
|
8
|
+
Run with ``.venv/bin/python -m unittest tests.test_currency_conversion``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import unittest
|
|
13
|
+
from unittest import mock
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from click.testing import CliRunner
|
|
17
|
+
|
|
18
|
+
from makea_cli import api_client, identity, output
|
|
19
|
+
from makea_cli.main import build_cli
|
|
20
|
+
|
|
21
|
+
RESULT = {
|
|
22
|
+
"to": "GBP",
|
|
23
|
+
"rates": {"USD": 0.752, "CNY": 0.113},
|
|
24
|
+
"unavailable": [],
|
|
25
|
+
"rate_decimals": 3,
|
|
26
|
+
"rounding": "up",
|
|
27
|
+
"source": "platform (FreeCurrencyService)",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ApiClientTest(unittest.TestCase):
|
|
32
|
+
def test_rates(self):
|
|
33
|
+
with mock.patch.object(api_client, "get_json", return_value={}) as get:
|
|
34
|
+
api_client.get_fx_rates(to="gbp", sources=["usd", "cny"])
|
|
35
|
+
get.assert_called_once_with("/admin/fx/rates", params={"to": "gbp", "from": "usd,cny"})
|
|
36
|
+
|
|
37
|
+
def test_amount(self):
|
|
38
|
+
with mock.patch.object(api_client, "get_json", return_value={}) as get:
|
|
39
|
+
api_client.get_fx_rates(to="gbp", sources=["cny"], amount=250)
|
|
40
|
+
get.assert_called_once_with(
|
|
41
|
+
"/admin/fx/rates", params={"to": "gbp", "from": "cny", "amount": 250}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CommandTest(unittest.TestCase):
|
|
46
|
+
def setUp(self):
|
|
47
|
+
self.cli = build_cli()
|
|
48
|
+
self.runner = CliRunner()
|
|
49
|
+
output.state.force_json = True
|
|
50
|
+
|
|
51
|
+
def tearDown(self):
|
|
52
|
+
output.state.force_json = False
|
|
53
|
+
|
|
54
|
+
def _run(self, args, payload):
|
|
55
|
+
with (
|
|
56
|
+
mock.patch.object(identity, "current_persona", return_value="admin"),
|
|
57
|
+
mock.patch.object(api_client, "get_fx_rates", return_value=payload) as get,
|
|
58
|
+
# Nothing is fetched from an FX source directly any more.
|
|
59
|
+
mock.patch.object(httpx, "Client", side_effect=AssertionError("no network")),
|
|
60
|
+
):
|
|
61
|
+
result = self.runner.invoke(self.cli, ["admin", "get", "currency-conversion", *args])
|
|
62
|
+
return result, get
|
|
63
|
+
|
|
64
|
+
def test_rate_table_is_the_platforms(self):
|
|
65
|
+
result, get = self._run(
|
|
66
|
+
["--to", "gbp", "--from", "usd,cny"], {"success": True, "result": RESULT}
|
|
67
|
+
)
|
|
68
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
69
|
+
get.assert_called_once_with(to="gbp", sources=["usd", "cny"], amount=None)
|
|
70
|
+
self.assertEqual(json.loads(result.output), RESULT)
|
|
71
|
+
|
|
72
|
+
def test_single_amount_is_converted_by_the_platform(self):
|
|
73
|
+
converted = {
|
|
74
|
+
**RESULT,
|
|
75
|
+
"rates": {"CNY": 0.113},
|
|
76
|
+
"from": "CNY",
|
|
77
|
+
"rate": 0.113,
|
|
78
|
+
"amount": 250.0,
|
|
79
|
+
"converted": 28.25,
|
|
80
|
+
}
|
|
81
|
+
result, get = self._run(
|
|
82
|
+
["--to", "gbp", "--from", "cny", "--amount", "250"],
|
|
83
|
+
{"success": True, "result": converted},
|
|
84
|
+
)
|
|
85
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
86
|
+
get.assert_called_once_with(to="gbp", sources=["cny"], amount=250.0)
|
|
87
|
+
self.assertEqual(json.loads(result.output)["converted"], 28.25)
|
|
88
|
+
|
|
89
|
+
def test_amount_with_several_currencies_is_not_sent(self):
|
|
90
|
+
_, get = self._run(
|
|
91
|
+
["--to", "gbp", "--from", "usd,cny", "--amount", "250"],
|
|
92
|
+
{"success": True, "result": RESULT},
|
|
93
|
+
)
|
|
94
|
+
get.assert_called_once_with(to="gbp", sources=["usd", "cny"], amount=None)
|
|
95
|
+
|
|
96
|
+
def test_a_missing_rate_fails_rather_than_returning_null(self):
|
|
97
|
+
missing = {**RESULT, "rates": {"XYZ": None}, "unavailable": ["XYZ"]}
|
|
98
|
+
result, _ = self._run(
|
|
99
|
+
["--to", "gbp", "--from", "xyz"], {"success": True, "result": missing}
|
|
100
|
+
)
|
|
101
|
+
self.assertEqual(result.exit_code, 1, result.output)
|
|
102
|
+
self.assertIn("XYZ->GBP", result.output)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
unittest.main()
|
|
@@ -22,7 +22,11 @@ class PmReadsTest(unittest.TestCase):
|
|
|
22
22
|
"progress_type": "During Production",
|
|
23
23
|
"metadata": {
|
|
24
24
|
"milestones": [
|
|
25
|
-
{
|
|
25
|
+
{
|
|
26
|
+
"title": "Cutting",
|
|
27
|
+
"status": "completed",
|
|
28
|
+
"time_window": ["3", "5"],
|
|
29
|
+
},
|
|
26
30
|
]
|
|
27
31
|
},
|
|
28
32
|
}
|
|
@@ -50,7 +54,12 @@ class PmReadsTest(unittest.TestCase):
|
|
|
50
54
|
raw = {
|
|
51
55
|
"notes": [
|
|
52
56
|
{"note_id": "a", "created_at": "2026-09-30T10:00:00", "content": "old"},
|
|
53
|
-
{
|
|
57
|
+
{
|
|
58
|
+
"note_id": "b",
|
|
59
|
+
"created_at": "2026-10-05T10:00:00",
|
|
60
|
+
"content": "new",
|
|
61
|
+
"replies": [{}],
|
|
62
|
+
},
|
|
54
63
|
]
|
|
55
64
|
}
|
|
56
65
|
with mock.patch.object(api_client, "get_json", return_value=raw):
|
|
@@ -62,7 +71,9 @@ class PmReadsTest(unittest.TestCase):
|
|
|
62
71
|
raw = {"packs": [{"product_id": "p1", "pack_label": "v1", "costing": {"make": 50}}]}
|
|
63
72
|
with mock.patch.object(api_client, "get_json", return_value=raw):
|
|
64
73
|
out = api_client.designer_get_costing("st1")
|
|
65
|
-
self.assertEqual(
|
|
74
|
+
self.assertEqual(
|
|
75
|
+
out["packs"], [{"product_id": "p1", "pack": "v1", "costing": {"make": 50}}]
|
|
76
|
+
)
|
|
66
77
|
|
|
67
78
|
|
|
68
79
|
class ExamFixesTest(unittest.TestCase):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.13"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|