makea-cli 0.2.2__tar.gz → 0.2.5__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.2 → makea_cli-0.2.5}/CHANGELOG.md +41 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/PKG-INFO +7 -1
- {makea_cli-0.2.2 → makea_cli-0.2.5}/README.md +6 -0
- makea_cli-0.2.5/makea_cli/__init__.py +1 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/api_client.py +51 -2
- makea_cli-0.2.5/makea_cli/broker.py +83 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/brand/cmd.py +193 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/supplier/cmd.py +24 -4
- {makea_cli-0.2.2 → makea_cli-0.2.5}/pyproject.toml +1 -1
- {makea_cli-0.2.2 → makea_cli-0.2.5}/tests/golden_manifest.json +309 -1
- makea_cli-0.2.5/tests/test_broker.py +106 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/tests/test_persona_commands.py +24 -2
- makea_cli-0.2.2/makea_cli/__init__.py +0 -1
- {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/link-supplier-user/SKILL.md +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/.gitignore +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea-collection-upload/SKILL.md +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea-collection-upload/scripts/create_products.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/__main__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/auth_pkce.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/_util.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/analytics/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/analytics/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/auth/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/auth/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/brand/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/designer/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/designer/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/directory/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/directory/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/document/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/document/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/email/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/email/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/factory/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/factory/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/library/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/library/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/metrics/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/metrics/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/orders/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/orders/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/product/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/product/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/production/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/production/cmd.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/production/pricing_spec.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/commands/supplier/__init__.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/config.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/errors.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/identity.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/main.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/output.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/posthog_client.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/makea_cli/registry.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/npm/makea-cli/README.md +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/npm/makea-cli/makea-cli.mjs +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/npm/makea-cli/package.json +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/tests/test_output_contract.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/tests/test_registry.py +0 -0
- {makea_cli-0.2.2 → makea_cli-0.2.5}/uv.lock +0 -0
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.5
|
|
4
|
+
|
|
5
|
+
The production journey, for the Claude agent on the product channel.
|
|
6
|
+
|
|
7
|
+
- `designer set production-cart <production_order_id> --qty 'SIZE,#RRGGBB=COUNT' …`
|
|
8
|
+
(`auto`): the run's size x colour quantities. Replaces the whole cart, and is
|
|
9
|
+
refused once the run is locked.
|
|
10
|
+
- `designer set production-lock <production_order_id> [--unlock]` (`ask`):
|
|
11
|
+
place the order. Locking is what makes the deposit payable; it can be
|
|
12
|
+
reopened until the deposit is paid.
|
|
13
|
+
- `designer get production-price <production_order_id> [--phase final]`: the
|
|
14
|
+
deposit (or balance) for the quantities on the run now, line by line.
|
|
15
|
+
|
|
16
|
+
## 0.2.4
|
|
17
|
+
|
|
18
|
+
For Beeboo acting for a brand through the CLI (the Claude agent on the product
|
|
19
|
+
channel).
|
|
20
|
+
|
|
21
|
+
- **Token broker mode.** With `MAKEA_TOKEN_BROKER=<unix socket>` the CLI sends
|
|
22
|
+
its own argv to the broker and uses the token it gets back
|
|
23
|
+
(`x-impersonation-token`) for that one invocation. There is no fallback to a
|
|
24
|
+
cached login in this mode: a command that did not come through the host's
|
|
25
|
+
gate gets no token.
|
|
26
|
+
- `designer submit tech-pack <product_id>` (`ask`): confirm the tech pack.
|
|
27
|
+
- `designer create quote-award <product_id> --quote <supplier link id>` (`ask`):
|
|
28
|
+
choose the quote to sample with. Creates or reuses a DRAFT sampling order and
|
|
29
|
+
prints its id.
|
|
30
|
+
- `designer create checkout --key sampling:<id> [--key …] --accept-terms`
|
|
31
|
+
(`ask`): a Stripe checkout link for one or more charges (sampling,
|
|
32
|
+
production deposit/final, shipping, misc). It is the one command near a
|
|
33
|
+
payment endpoint: it creates the session and nothing is charged until the
|
|
34
|
+
brand pays on Stripe's page. Bank transfers and subscriptions stay out.
|
|
35
|
+
|
|
36
|
+
## 0.2.3
|
|
37
|
+
|
|
38
|
+
- `admin update quote --ip-schedule-hidden/--ip-schedule-visible`: hide a
|
|
39
|
+
supplier's IP Schedule from the customer, or show it again. Visible unless set.
|
|
40
|
+
- `--ip-schedule-json` help describes the version 2 schedule: two buckets
|
|
41
|
+
(`production_files`, tech pack and BOM included, and `physical_tooling`),
|
|
42
|
+
"not created" by default, and optional.
|
|
43
|
+
|
|
3
44
|
## 0.2.2
|
|
4
45
|
|
|
5
46
|
Order lifecycle, exposed in tiers rather than all at once.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: makea-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
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
|
|
@@ -144,9 +144,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
144
144
|
|
|
145
145
|
| 命令 | 读/写 | 后端 | 说明 | 旧名 |
|
|
146
146
|
|---|---|---|---|---|
|
|
147
|
+
| `makea-cli designer create checkout` | write(ask) | `POST /designer/payments/basket/create-session` | a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_o… | — |
|
|
147
148
|
| `makea-cli designer create collection` | write(auto) | `POST /designer/collections/create` | create or update a designer collection. | `designer-create-collection` |
|
|
148
149
|
| `makea-cli designer create note` | write(ask) | `POST /designer/products/{product_id}/notes` | post a message into a product's conversation. | — |
|
|
149
150
|
| `makea-cli designer create product` | write(ask) | `POST /designer/product/create_or_update_product` | payload must match manufacture/specifications (all frontend fields; categories/subcategories + collection_id in --hel… | `designer-upsert-product` |
|
|
151
|
+
| `makea-cli designer create quote-award` | write(ask) | `POST /designer/quotes/group_pay_prepare` | choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat … | — |
|
|
150
152
|
| `makea-cli designer create rfq` | write(ask) | `POST /designer/product/send_rfq` | ask the chosen suppliers to quote on a product. --supplier repeats; --makea-sourcing also asks Makea to source. | — |
|
|
151
153
|
| `makea-cli designer create sampling-order` | write(ask) | `POST /designer/sampling_orders` | order samples. --file is the full order payload (product_order_id, items, fees …); run --dry-run first and read the b… | — |
|
|
152
154
|
| `makea-cli designer delete sampling-order` | write(never) | `DELETE /designer/sampling_orders/{sampling_order_id}` | delete a DRAFT sample order. The backend refuses any other status. Graded `never`. | — |
|
|
@@ -155,6 +157,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
155
157
|
| `makea-cli designer get collection` | read | `GET /designer/collections/{collection_id}/overview` | one collection with its products, orders and lifecycle in a single read. | — |
|
|
156
158
|
| `makea-cli designer get lifecycle` | read | `GET /designer/product_orders/{product_order_id}/lifecycle` | which stage this product is in. | — |
|
|
157
159
|
| `makea-cli designer get product` | read | `GET /designer/get_product` | one product: status, documents, prices, links. | — |
|
|
160
|
+
| `makea-cli designer get production-price` | read | `GET /designer/production_orders/{production_order_id}/calculate_price` | what the deposit (or the balance, --phase final) comes to for the quantities on the run right now, line by line. | — |
|
|
158
161
|
| `makea-cli designer get quote-advice` | read | `GET /designer/product/quote_advice` | which quote to award and why, from cache. Reading never spends a model call; the web app is what asks for a fresh one. | — |
|
|
159
162
|
| `makea-cli designer get tech-pack` | read | `GET /designer/products/{product_id}/technical-specifications` | the product's tech pack. | — |
|
|
160
163
|
| `makea-cli designer get tech-pack-history` | read | `GET /designer/products/{product_id}/specification-change-history` | who changed which spec field, when, and from what. | — |
|
|
@@ -171,8 +174,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
171
174
|
| `makea-cli designer list task` | read | `GET /designer/tasks` | what the platform is waiting on you for. | — |
|
|
172
175
|
| `makea-cli designer search note` | read | `GET /designer/products/{product_id}/channel_search` | full-text search across a product's conversation; --channel narrows to one channel key. | — |
|
|
173
176
|
| `makea-cli designer set product-collection` | write(auto) | `POST /designer/products/{id}/link-to-collection` | assign product to a collection. | `designer-link-product-to-collection` |
|
|
177
|
+
| `makea-cli designer set production-cart` | write(auto) | `PUT /designer/production_orders/{production_order_id}/cart` | the quantity of every size and colour on the run. Replaces the whole cart: pass every cell you want kept. --qty repea… | — |
|
|
178
|
+
| `makea-cli designer set production-lock` | write(ask) | `POST /designer/production_orders/{production_order_id}/configuration-lock` | place the order with the quantities on the run (--unlock to reopen them). Locking is what makes the deposit payable: … | — |
|
|
174
179
|
| `makea-cli designer submit production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/submit` | submit a 大货 order. This is the commitment, so --dry-run and confirm with the user first. | — |
|
|
175
180
|
| `makea-cli designer submit sampling-order` | write(ask) | `POST /designer/sampling_orders/{sampling_order_id}/confirm` | confirm a sample order so the supplier starts. Run --dry-run first and read it back to the user. | — |
|
|
181
|
+
| `makea-cli designer submit tech-pack` | write(ask) | `POST /designer/products/{product_id}/technical-specifications/confirm` | confirm the tech pack, the step before anyone is asked to quote. | — |
|
|
176
182
|
| `makea-cli designer update action` | write(auto) | `POST /designer/actions/{action_id}/{acknowledge\|snooze\|dismiss}` | respond to one of the pending actions from `designer list action`. An action is closed by the fact it asks for, not b… | — |
|
|
177
183
|
| `makea-cli designer update note` | write(ask) | `POST /designer/products/{product_id}/notes/{note_id}/{accept\|reject}` | decide on a task the assistant proposed on a product. | — |
|
|
178
184
|
| `makea-cli designer update product` | write(auto) | `POST /designer/product/update_basic_info` | patch fields on a product. --cover-image / --back-image take a document id and are what the assistant's set_product_i… | — |
|
|
@@ -132,9 +132,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
132
132
|
|
|
133
133
|
| 命令 | 读/写 | 后端 | 说明 | 旧名 |
|
|
134
134
|
|---|---|---|---|---|
|
|
135
|
+
| `makea-cli designer create checkout` | write(ask) | `POST /designer/payments/basket/create-session` | a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_o… | — |
|
|
135
136
|
| `makea-cli designer create collection` | write(auto) | `POST /designer/collections/create` | create or update a designer collection. | `designer-create-collection` |
|
|
136
137
|
| `makea-cli designer create note` | write(ask) | `POST /designer/products/{product_id}/notes` | post a message into a product's conversation. | — |
|
|
137
138
|
| `makea-cli designer create product` | write(ask) | `POST /designer/product/create_or_update_product` | payload must match manufacture/specifications (all frontend fields; categories/subcategories + collection_id in --hel… | `designer-upsert-product` |
|
|
139
|
+
| `makea-cli designer create quote-award` | write(ask) | `POST /designer/quotes/group_pay_prepare` | choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat … | — |
|
|
138
140
|
| `makea-cli designer create rfq` | write(ask) | `POST /designer/product/send_rfq` | ask the chosen suppliers to quote on a product. --supplier repeats; --makea-sourcing also asks Makea to source. | — |
|
|
139
141
|
| `makea-cli designer create sampling-order` | write(ask) | `POST /designer/sampling_orders` | order samples. --file is the full order payload (product_order_id, items, fees …); run --dry-run first and read the b… | — |
|
|
140
142
|
| `makea-cli designer delete sampling-order` | write(never) | `DELETE /designer/sampling_orders/{sampling_order_id}` | delete a DRAFT sample order. The backend refuses any other status. Graded `never`. | — |
|
|
@@ -143,6 +145,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
143
145
|
| `makea-cli designer get collection` | read | `GET /designer/collections/{collection_id}/overview` | one collection with its products, orders and lifecycle in a single read. | — |
|
|
144
146
|
| `makea-cli designer get lifecycle` | read | `GET /designer/product_orders/{product_order_id}/lifecycle` | which stage this product is in. | — |
|
|
145
147
|
| `makea-cli designer get product` | read | `GET /designer/get_product` | one product: status, documents, prices, links. | — |
|
|
148
|
+
| `makea-cli designer get production-price` | read | `GET /designer/production_orders/{production_order_id}/calculate_price` | what the deposit (or the balance, --phase final) comes to for the quantities on the run right now, line by line. | — |
|
|
146
149
|
| `makea-cli designer get quote-advice` | read | `GET /designer/product/quote_advice` | which quote to award and why, from cache. Reading never spends a model call; the web app is what asks for a fresh one. | — |
|
|
147
150
|
| `makea-cli designer get tech-pack` | read | `GET /designer/products/{product_id}/technical-specifications` | the product's tech pack. | — |
|
|
148
151
|
| `makea-cli designer get tech-pack-history` | read | `GET /designer/products/{product_id}/specification-change-history` | who changed which spec field, when, and from what. | — |
|
|
@@ -159,8 +162,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
159
162
|
| `makea-cli designer list task` | read | `GET /designer/tasks` | what the platform is waiting on you for. | — |
|
|
160
163
|
| `makea-cli designer search note` | read | `GET /designer/products/{product_id}/channel_search` | full-text search across a product's conversation; --channel narrows to one channel key. | — |
|
|
161
164
|
| `makea-cli designer set product-collection` | write(auto) | `POST /designer/products/{id}/link-to-collection` | assign product to a collection. | `designer-link-product-to-collection` |
|
|
165
|
+
| `makea-cli designer set production-cart` | write(auto) | `PUT /designer/production_orders/{production_order_id}/cart` | the quantity of every size and colour on the run. Replaces the whole cart: pass every cell you want kept. --qty repea… | — |
|
|
166
|
+
| `makea-cli designer set production-lock` | write(ask) | `POST /designer/production_orders/{production_order_id}/configuration-lock` | place the order with the quantities on the run (--unlock to reopen them). Locking is what makes the deposit payable: … | — |
|
|
162
167
|
| `makea-cli designer submit production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/submit` | submit a 大货 order. This is the commitment, so --dry-run and confirm with the user first. | — |
|
|
163
168
|
| `makea-cli designer submit sampling-order` | write(ask) | `POST /designer/sampling_orders/{sampling_order_id}/confirm` | confirm a sample order so the supplier starts. Run --dry-run first and read it back to the user. | — |
|
|
169
|
+
| `makea-cli designer submit tech-pack` | write(ask) | `POST /designer/products/{product_id}/technical-specifications/confirm` | confirm the tech pack, the step before anyone is asked to quote. | — |
|
|
164
170
|
| `makea-cli designer update action` | write(auto) | `POST /designer/actions/{action_id}/{acknowledge\|snooze\|dismiss}` | respond to one of the pending actions from `designer list action`. An action is closed by the fact it asks for, not b… | — |
|
|
165
171
|
| `makea-cli designer update note` | write(ask) | `POST /designer/products/{product_id}/notes/{note_id}/{accept\|reject}` | decide on a task the assistant proposed on a product. | — |
|
|
166
172
|
| `makea-cli designer update product` | write(auto) | `POST /designer/product/update_basic_info` | patch fields on a product. --cover-image / --back-image take a document id and are what the assistant's set_product_i… | — |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.5"
|
|
@@ -12,6 +12,7 @@ import httpx
|
|
|
12
12
|
from contextvars import ContextVar
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
|
|
15
|
+
from makea_cli import broker
|
|
15
16
|
from makea_cli.auth_pkce import ensure_fresh_id_token
|
|
16
17
|
from makea_cli.config import api_base_url
|
|
17
18
|
from makea_cli.errors import ApiError, AuthError, UnknownOutcomeError
|
|
@@ -53,8 +54,20 @@ def _token() -> str:
|
|
|
53
54
|
raise AuthError(str(e)) from e
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
def _auth_headers() -> dict[str, str]:
|
|
58
|
+
"""Who this request is from.
|
|
59
|
+
|
|
60
|
+
Under a token broker the token is a delegation the backend reads from the
|
|
61
|
+
impersonation header; there is no bearer at all, so a command that did not
|
|
62
|
+
come through the host's gate has nothing to send.
|
|
63
|
+
"""
|
|
64
|
+
if broker.broker_path():
|
|
65
|
+
return {"x-impersonation-token": broker.brokered_token() or ""}
|
|
66
|
+
return {"Authorization": f"Bearer {_token()}"}
|
|
67
|
+
|
|
68
|
+
|
|
56
69
|
def _headers() -> dict[str, str]:
|
|
57
|
-
headers = {
|
|
70
|
+
headers = {**_auth_headers(), "Accept": "application/json"}
|
|
58
71
|
if state.idempotency_key:
|
|
59
72
|
headers["Idempotency-Key"] = state.idempotency_key
|
|
60
73
|
return headers
|
|
@@ -254,7 +267,7 @@ def post_multipart_with_files(
|
|
|
254
267
|
def _download(path: str, params: dict[str, Any], *, timeout: float) -> tuple[bytes, str | None]:
|
|
255
268
|
"""GET a file (or follow the redirect to its presigned URL); returns (bytes, filename)."""
|
|
256
269
|
url = f"{api_base_url()}{path}"
|
|
257
|
-
headers = {
|
|
270
|
+
headers = {**_auth_headers(), "Accept": "*/*"}
|
|
258
271
|
try:
|
|
259
272
|
with httpx.Client(timeout=timeout, follow_redirects=True) as client:
|
|
260
273
|
r = client.get(url, headers=headers, params=params)
|
|
@@ -1185,6 +1198,42 @@ def designer_send_rfq(body: dict[str, Any]) -> Any:
|
|
|
1185
1198
|
return post_json("/designer/product/send_rfq", body)
|
|
1186
1199
|
|
|
1187
1200
|
|
|
1201
|
+
def designer_confirm_tech_spec(product_id: str) -> Any:
|
|
1202
|
+
"""POST /designer/products/{id}/technical-specifications/confirm — confirm the tech pack."""
|
|
1203
|
+
return post_json(f"/designer/products/{product_id}/technical-specifications/confirm", {})
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def designer_award_quotes(body: dict[str, Any]) -> Any:
|
|
1207
|
+
"""POST /designer/quotes/group_pay_prepare — DRAFT sampling orders for chosen quotes."""
|
|
1208
|
+
return post_json("/designer/quotes/group_pay_prepare", body)
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def designer_create_checkout(body: dict[str, Any]) -> Any:
|
|
1212
|
+
"""POST /designer/payments/basket/create-session — a Stripe checkout for charges."""
|
|
1213
|
+
return post_json("/designer/payments/basket/create-session", body)
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def designer_production_price(production_order_id: str, phase: str) -> Any:
|
|
1217
|
+
"""GET /designer/production_orders/{id}/calculate_price — deposit or balance, itemised."""
|
|
1218
|
+
return get_json(
|
|
1219
|
+
f"/designer/production_orders/{production_order_id}/calculate_price",
|
|
1220
|
+
params={"payment_type": phase},
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def designer_set_production_cart(production_order_id: str, cart: dict[str, int]) -> Any:
|
|
1225
|
+
"""PUT /designer/production_orders/{id}/cart — replace the size x colour quantities."""
|
|
1226
|
+
return put_json(f"/designer/production_orders/{production_order_id}/cart", {"cart_items": cart})
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def designer_set_production_lock(production_order_id: str, locked: bool) -> Any:
|
|
1230
|
+
"""POST /designer/production_orders/{id}/configuration-lock — place or reopen the run."""
|
|
1231
|
+
return post_json(
|
|
1232
|
+
f"/designer/production_orders/{production_order_id}/configuration-lock",
|
|
1233
|
+
{"locked": locked},
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
|
|
1188
1237
|
def designer_list_collections() -> Any:
|
|
1189
1238
|
"""GET /designer/collections — the brand's collections."""
|
|
1190
1239
|
return get_json("/designer/collections")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Token broker client: how the CLI acts for someone without holding their token.
|
|
2
|
+
|
|
3
|
+
A host that runs the CLI on a user's behalf (the Beeboo agent runner) does not
|
|
4
|
+
put that user's token in the environment, where any process in the sandbox
|
|
5
|
+
could read it. It runs a broker on a unix socket and names it in
|
|
6
|
+
``MAKEA_TOKEN_BROKER``. Before a command runs, the host's gate has already
|
|
7
|
+
looked at the exact command line and left a one-time ticket for it; the CLI
|
|
8
|
+
sends its own argv and gets back a token that is good for this invocation.
|
|
9
|
+
|
|
10
|
+
A process that did not come through the gate (``python -c``, ``curl``, a CLI
|
|
11
|
+
call wrapped in a script) has no ticket and gets nothing. That is the whole
|
|
12
|
+
point, so there is deliberately no fallback to a cached login in this mode.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import socket
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
from makea_cli.errors import AuthError
|
|
23
|
+
|
|
24
|
+
BROKER_ENV = "MAKEA_TOKEN_BROKER"
|
|
25
|
+
|
|
26
|
+
# A broker lives on the same box; anything slower than this is a broker that
|
|
27
|
+
# is not there.
|
|
28
|
+
_TIMEOUT_SECONDS = 10.0
|
|
29
|
+
|
|
30
|
+
# One invocation spends one ticket. Every request the command makes reuses the
|
|
31
|
+
# token the ticket bought.
|
|
32
|
+
_cached: dict[str, str] | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def broker_path() -> str | None:
|
|
36
|
+
path = (os.environ.get(BROKER_ENV) or "").strip()
|
|
37
|
+
return path or None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def invocation_argv() -> list[str]:
|
|
41
|
+
"""This process's command line, as the gate saw it (without the program name)."""
|
|
42
|
+
return list(sys.argv[1:])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def exchange(path: str, argv: list[str]) -> dict[str, str]:
|
|
46
|
+
"""Trade this command line for ``{"token", "persona"}``, or raise AuthError."""
|
|
47
|
+
request = json.dumps({"argv": argv}).encode("utf-8") + b"\n"
|
|
48
|
+
try:
|
|
49
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
50
|
+
sock.settimeout(_TIMEOUT_SECONDS)
|
|
51
|
+
sock.connect(path)
|
|
52
|
+
sock.sendall(request)
|
|
53
|
+
chunks = []
|
|
54
|
+
while True:
|
|
55
|
+
chunk = sock.recv(65536)
|
|
56
|
+
if not chunk:
|
|
57
|
+
break
|
|
58
|
+
chunks.append(chunk)
|
|
59
|
+
if chunk.endswith(b"\n"):
|
|
60
|
+
break
|
|
61
|
+
except OSError as e:
|
|
62
|
+
raise AuthError(f"token broker unavailable ({e.__class__.__name__})") from e
|
|
63
|
+
try:
|
|
64
|
+
reply = json.loads(b"".join(chunks).decode("utf-8") or "{}")
|
|
65
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
66
|
+
raise AuthError("token broker sent an unreadable reply") from e
|
|
67
|
+
if not reply.get("ok") or not reply.get("token"):
|
|
68
|
+
raise AuthError(
|
|
69
|
+
str(reply.get("error") or "the token broker refused this command"),
|
|
70
|
+
detail={"broker": True},
|
|
71
|
+
)
|
|
72
|
+
return {"token": str(reply["token"]), "persona": str(reply.get("persona") or "")}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def brokered_token() -> str | None:
|
|
76
|
+
"""The token for this invocation when a broker is configured, else None."""
|
|
77
|
+
global _cached
|
|
78
|
+
path = broker_path()
|
|
79
|
+
if not path:
|
|
80
|
+
return None
|
|
81
|
+
if _cached is None:
|
|
82
|
+
_cached = exchange(path, invocation_argv())
|
|
83
|
+
return _cached["token"]
|
|
@@ -817,3 +817,196 @@ def register(app: typer.Typer) -> None:
|
|
|
817
817
|
if makea_sourcing:
|
|
818
818
|
body["makea_sourcing"] = True
|
|
819
819
|
print_json_or_exit(console, lambda: api_client.designer_send_rfq(body))
|
|
820
|
+
|
|
821
|
+
# --- the sampling journey: confirm the pack, award a quote, pay -------------
|
|
822
|
+
|
|
823
|
+
@command(
|
|
824
|
+
app,
|
|
825
|
+
"designer",
|
|
826
|
+
"submit",
|
|
827
|
+
"tech-pack",
|
|
828
|
+
api="POST /designer/products/{product_id}/technical-specifications/confirm",
|
|
829
|
+
side_effects=[
|
|
830
|
+
"locks the tech pack as the version suppliers quote against",
|
|
831
|
+
"opens the product for sourcing",
|
|
832
|
+
],
|
|
833
|
+
approval="ask",
|
|
834
|
+
help=(
|
|
835
|
+
"POST /designer/products/{id}/technical-specifications/confirm — confirm the "
|
|
836
|
+
"tech pack, the step before anyone is asked to quote."
|
|
837
|
+
),
|
|
838
|
+
)
|
|
839
|
+
def submit_tech_pack(
|
|
840
|
+
product_id: str = typer.Argument(..., help="Product id."),
|
|
841
|
+
) -> None:
|
|
842
|
+
"""Confirm a product's tech pack."""
|
|
843
|
+
print_json_or_exit(console, lambda: api_client.designer_confirm_tech_spec(product_id))
|
|
844
|
+
|
|
845
|
+
@command(
|
|
846
|
+
app,
|
|
847
|
+
"designer",
|
|
848
|
+
"create",
|
|
849
|
+
"quote-award",
|
|
850
|
+
api="POST /designer/quotes/group_pay_prepare",
|
|
851
|
+
side_effects=[
|
|
852
|
+
"creates (or reuses) a DRAFT sampling order on each chosen quote",
|
|
853
|
+
"nothing is charged until that order is paid",
|
|
854
|
+
],
|
|
855
|
+
approval="ask",
|
|
856
|
+
help=(
|
|
857
|
+
"POST /designer/quotes/group_pay_prepare — choose the supplier quote to sample "
|
|
858
|
+
"with. --quote is the supplier link id from `designer list supplier-link`; "
|
|
859
|
+
"repeat --option for the quote's optional extras. Prints the sampling_order_ids "
|
|
860
|
+
"to pay with `designer create checkout --key sampling:<id>`."
|
|
861
|
+
),
|
|
862
|
+
)
|
|
863
|
+
def create_quote_award(
|
|
864
|
+
product_id: str = typer.Argument(..., help="Product id."),
|
|
865
|
+
quote: str = typer.Option(..., "--quote", help="Supplier link id of the chosen quote."),
|
|
866
|
+
option: list[str] = typer.Option(
|
|
867
|
+
[], "--option", help="Optional extra on the quote to include (repeat)."
|
|
868
|
+
),
|
|
869
|
+
no_base_sampling: bool = typer.Option(
|
|
870
|
+
False, "--no-base-sampling", help="Leave the quote's base sample out."
|
|
871
|
+
),
|
|
872
|
+
notes: str | None = typer.Option(None, "--notes", help="Note for the supplier."),
|
|
873
|
+
) -> None:
|
|
874
|
+
"""Award a quote: the sampling order that pays for it."""
|
|
875
|
+
selection: dict[str, Any] = {"product_id": product_id, "link_id": quote}
|
|
876
|
+
if option:
|
|
877
|
+
selection["selected_option_ids"] = list(option)
|
|
878
|
+
if no_base_sampling:
|
|
879
|
+
selection["include_base_sampling"] = False
|
|
880
|
+
if notes:
|
|
881
|
+
selection["notes"] = notes
|
|
882
|
+
print_json_or_exit(
|
|
883
|
+
console, lambda: api_client.designer_award_quotes({"selections": [selection]})
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
@command(
|
|
887
|
+
app,
|
|
888
|
+
"designer",
|
|
889
|
+
"create",
|
|
890
|
+
"checkout",
|
|
891
|
+
api="POST /designer/payments/basket/create-session",
|
|
892
|
+
side_effects=[
|
|
893
|
+
"records the brand's acceptance of the sampling/production terms",
|
|
894
|
+
"creates a Stripe checkout session; nothing is charged until it is paid there",
|
|
895
|
+
],
|
|
896
|
+
approval="ask",
|
|
897
|
+
help=(
|
|
898
|
+
"POST /designer/payments/basket/create-session — a Stripe checkout link for one "
|
|
899
|
+
"or more charges. --key repeats: sampling:<sampling_order_id>, "
|
|
900
|
+
"production:<production_order_id>:deposit|final, shipping:<id>, misc:<id> "
|
|
901
|
+
"(the `key` of each payable in `designer list payment`). Prints checkout_url."
|
|
902
|
+
),
|
|
903
|
+
)
|
|
904
|
+
def create_checkout(
|
|
905
|
+
key: list[str] = typer.Option(..., "--key", help="Charge key to pay (repeat)."),
|
|
906
|
+
accept_terms: bool = typer.Option(
|
|
907
|
+
False,
|
|
908
|
+
"--accept-terms",
|
|
909
|
+
help="The brand accepts the sampling/production terms for these charges.",
|
|
910
|
+
),
|
|
911
|
+
) -> None:
|
|
912
|
+
"""Create a Stripe checkout link for charges."""
|
|
913
|
+
body: dict[str, Any] = {"keys": list(key)}
|
|
914
|
+
if accept_terms:
|
|
915
|
+
body["terms_accepted"] = True
|
|
916
|
+
print_json_or_exit(console, lambda: api_client.designer_create_checkout(body))
|
|
917
|
+
|
|
918
|
+
# --- the production journey: quantities, place the order, pay the deposit ---
|
|
919
|
+
|
|
920
|
+
@command(
|
|
921
|
+
app,
|
|
922
|
+
"designer",
|
|
923
|
+
"get",
|
|
924
|
+
"production-price",
|
|
925
|
+
api="GET /designer/production_orders/{production_order_id}/calculate_price",
|
|
926
|
+
help=(
|
|
927
|
+
"GET /designer/production_orders/{id}/calculate_price — what the deposit (or the "
|
|
928
|
+
"balance, --phase final) comes to for the quantities on the run right now, line "
|
|
929
|
+
"by line."
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
def get_production_price(
|
|
933
|
+
production_order_id: str = typer.Argument(..., help="Production order id."),
|
|
934
|
+
phase: str = typer.Option("deposit", "--phase", help="deposit or final."),
|
|
935
|
+
) -> None:
|
|
936
|
+
"""Price a production order's deposit or balance."""
|
|
937
|
+
if phase not in ("deposit", "final"):
|
|
938
|
+
raise typer.BadParameter("--phase is deposit or final")
|
|
939
|
+
print_json_or_exit(
|
|
940
|
+
console,
|
|
941
|
+
lambda: api_client.designer_production_price(production_order_id, phase),
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
@command(
|
|
945
|
+
app,
|
|
946
|
+
"designer",
|
|
947
|
+
"set",
|
|
948
|
+
"production-cart",
|
|
949
|
+
api="PUT /designer/production_orders/{production_order_id}/cart",
|
|
950
|
+
side_effects=["replaces the run's quantities; nothing is ordered until it is locked"],
|
|
951
|
+
approval="auto",
|
|
952
|
+
help=(
|
|
953
|
+
"PUT /designer/production_orders/{id}/cart — the quantity of every size and "
|
|
954
|
+
"colour on the run. Replaces the whole cart: pass every cell you want kept. "
|
|
955
|
+
"--qty repeats as 'SIZE,#RRGGBB=COUNT' (quote it: # starts a shell comment), "
|
|
956
|
+
"with the sizes and colour hex codes from "
|
|
957
|
+
"the order's available_sizes / available_colors (`designer list "
|
|
958
|
+
"production-order --product <id>`). Refused once the run is locked."
|
|
959
|
+
),
|
|
960
|
+
)
|
|
961
|
+
def set_production_cart(
|
|
962
|
+
production_order_id: str = typer.Argument(..., help="Production order id."),
|
|
963
|
+
qty: list[str] = typer.Option(
|
|
964
|
+
..., "--qty", help="SIZE,#RRGGBB=COUNT for one cell (repeat)."
|
|
965
|
+
),
|
|
966
|
+
) -> None:
|
|
967
|
+
"""Set a production order's size x colour quantities."""
|
|
968
|
+
cart: dict[str, int] = {}
|
|
969
|
+
for cell in qty:
|
|
970
|
+
key, sep, count = cell.rpartition("=")
|
|
971
|
+
if not sep or "," not in key:
|
|
972
|
+
raise typer.BadParameter(f"--qty {cell!r}: expected SIZE,#RRGGBB=COUNT")
|
|
973
|
+
try:
|
|
974
|
+
n = int(count)
|
|
975
|
+
except ValueError as e:
|
|
976
|
+
raise typer.BadParameter(f"--qty {cell!r}: COUNT must be a whole number") from e
|
|
977
|
+
if n < 0:
|
|
978
|
+
raise typer.BadParameter(f"--qty {cell!r}: COUNT cannot be negative")
|
|
979
|
+
size, _, colour = key.partition(",")
|
|
980
|
+
cart[f"{size.strip()},{colour.strip()}"] = n
|
|
981
|
+
print_json_or_exit(
|
|
982
|
+
console,
|
|
983
|
+
lambda: api_client.designer_set_production_cart(production_order_id, cart),
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
@command(
|
|
987
|
+
app,
|
|
988
|
+
"designer",
|
|
989
|
+
"set",
|
|
990
|
+
"production-lock",
|
|
991
|
+
api="POST /designer/production_orders/{production_order_id}/configuration-lock",
|
|
992
|
+
side_effects=[
|
|
993
|
+
"places the order: the quantities are final and the deposit becomes payable",
|
|
994
|
+
"can be unlocked again until the deposit is paid",
|
|
995
|
+
],
|
|
996
|
+
approval="ask",
|
|
997
|
+
help=(
|
|
998
|
+
"POST /designer/production_orders/{id}/configuration-lock — place the order "
|
|
999
|
+
"with the quantities on the run (--unlock to reopen them). Locking is what makes "
|
|
1000
|
+
"the deposit payable: `designer create checkout --key "
|
|
1001
|
+
"production:<id>:deposit --accept-terms`."
|
|
1002
|
+
),
|
|
1003
|
+
)
|
|
1004
|
+
def set_production_lock(
|
|
1005
|
+
production_order_id: str = typer.Argument(..., help="Production order id."),
|
|
1006
|
+
unlock: bool = typer.Option(False, "--unlock", help="Reopen the quantities instead."),
|
|
1007
|
+
) -> None:
|
|
1008
|
+
"""Lock (place) or unlock a production order's configuration."""
|
|
1009
|
+
print_json_or_exit(
|
|
1010
|
+
console,
|
|
1011
|
+
lambda: api_client.designer_set_production_lock(production_order_id, not unlock),
|
|
1012
|
+
)
|
|
@@ -688,7 +688,8 @@ def register(app: typer.Typer) -> None:
|
|
|
688
688
|
Read one quote request in full.
|
|
689
689
|
"""
|
|
690
690
|
print_json_or_exit(
|
|
691
|
-
console,
|
|
691
|
+
console,
|
|
692
|
+
lambda: api_client.get_quote_request(quote_request_id=quote_request_id),
|
|
692
693
|
)
|
|
693
694
|
|
|
694
695
|
@command(
|
|
@@ -988,9 +989,18 @@ def register(app: typer.Typer) -> None:
|
|
|
988
989
|
help=(
|
|
989
990
|
"ip_schedule: who owns each asset the job creates (optional, JSON object). "
|
|
990
991
|
"Omit when the factory did not state it — makea must not invent an answer "
|
|
991
|
-
"on its behalf. If supplied it has to be COMPLETE
|
|
992
|
-
"
|
|
993
|
-
"
|
|
992
|
+
"on its behalf. If supplied it has to be COMPLETE or the API returns 400 "
|
|
993
|
+
"listing what is missing. Schema v2: "
|
|
994
|
+
'{"schema_version":"2","background_ip_used":false,"assets":['
|
|
995
|
+
'{"asset_type":"production_files","applicable":true,'
|
|
996
|
+
'"transfer":"transfers_to_client","delivery":"on_full_payment"},'
|
|
997
|
+
'{"asset_type":"physical_tooling","applicable":false}]} '
|
|
998
|
+
"-- both asset types answered (applicable false, the default, is a valid "
|
|
999
|
+
"answer; production_files includes the tech pack and BOM), "
|
|
1000
|
+
"background_ip_used true needs background_ip_declaration, and a "
|
|
1001
|
+
"PRODUCTION link also needs production_location and subcontractors_declared. "
|
|
1002
|
+
"To hide it from the customer afterwards, update the quote with "
|
|
1003
|
+
"--ip-schedule-hidden."
|
|
994
1004
|
),
|
|
995
1005
|
),
|
|
996
1006
|
resample_request_id: str | None = typer.Option(
|
|
@@ -1250,6 +1260,14 @@ def register(app: typer.Typer) -> None:
|
|
|
1250
1260
|
"the factory could not have submitted itself."
|
|
1251
1261
|
),
|
|
1252
1262
|
),
|
|
1263
|
+
ip_schedule_hidden: bool | None = typer.Option(
|
|
1264
|
+
None,
|
|
1265
|
+
"--ip-schedule-hidden/--ip-schedule-visible",
|
|
1266
|
+
help=(
|
|
1267
|
+
"ip_schedule_hidden_from_customer: hide this quote's IP Schedule from "
|
|
1268
|
+
"the customer, or show it again. Visible unless set."
|
|
1269
|
+
),
|
|
1270
|
+
),
|
|
1253
1271
|
available_sizes_json: str | None = typer.Option(
|
|
1254
1272
|
None,
|
|
1255
1273
|
"--available-sizes-json",
|
|
@@ -1350,6 +1368,8 @@ def register(app: typer.Typer) -> None:
|
|
|
1350
1368
|
)
|
|
1351
1369
|
if ip_schedule_json is not None:
|
|
1352
1370
|
override["ip_schedule"] = _parse_json_option(ip_schedule_json, "--ip-schedule-json")
|
|
1371
|
+
if ip_schedule_hidden is not None:
|
|
1372
|
+
override["ip_schedule_hidden_from_customer"] = ip_schedule_hidden
|
|
1353
1373
|
if refundable_terms_json is not None:
|
|
1354
1374
|
override["refundable_terms"] = _parse_json_option(
|
|
1355
1375
|
refundable_terms_json, "--refundable-terms-json"
|
|
@@ -2890,7 +2890,7 @@
|
|
|
2890
2890
|
"required": false,
|
|
2891
2891
|
"multiple": false,
|
|
2892
2892
|
"default": null,
|
|
2893
|
-
"help": "ip_schedule: who owns each asset the job creates (optional, JSON object). Omit when the factory did not state it — makea must not invent an answer on its behalf. If supplied it has to be COMPLETE
|
|
2893
|
+
"help": "ip_schedule: who owns each asset the job creates (optional, JSON object). Omit when the factory did not state it — makea must not invent an answer on its behalf. If supplied it has to be COMPLETE or the API returns 400 listing what is missing. Schema v2: {\"schema_version\":\"2\",\"background_ip_used\":false,\"assets\":[{\"asset_type\":\"production_files\",\"applicable\":true,\"transfer\":\"transfers_to_client\",\"delivery\":\"on_full_payment\"},{\"asset_type\":\"physical_tooling\",\"applicable\":false}]} -- both asset types answered (applicable false, the default, is a valid answer; production_files includes the tech pack and BOM), background_ip_used true needs background_ip_declaration, and a PRODUCTION link also needs production_location and subcontractors_declared. To hide it from the customer afterwards, update the quote with --ip-schedule-hidden."
|
|
2894
2894
|
},
|
|
2895
2895
|
{
|
|
2896
2896
|
"name": "resample_request_id",
|
|
@@ -3287,6 +3287,18 @@
|
|
|
3287
3287
|
"default": null,
|
|
3288
3288
|
"help": "ip_schedule: who owns each asset the job creates (JSON object; pass `null` to clear). A supplied schedule must be COMPLETE — the API refuses a half-answered one, so a quote can never carry a schedule the factory could not have submitted itself."
|
|
3289
3289
|
},
|
|
3290
|
+
{
|
|
3291
|
+
"name": "ip_schedule_hidden",
|
|
3292
|
+
"kind": "option",
|
|
3293
|
+
"opts": [
|
|
3294
|
+
"--ip-schedule-hidden"
|
|
3295
|
+
],
|
|
3296
|
+
"type": "flag",
|
|
3297
|
+
"required": false,
|
|
3298
|
+
"multiple": false,
|
|
3299
|
+
"default": null,
|
|
3300
|
+
"help": "ip_schedule_hidden_from_customer: hide this quote's IP Schedule from the customer, or show it again. Visible unless set."
|
|
3301
|
+
},
|
|
3290
3302
|
{
|
|
3291
3303
|
"name": "available_sizes_json",
|
|
3292
3304
|
"kind": "option",
|
|
@@ -4471,6 +4483,52 @@
|
|
|
4471
4483
|
}
|
|
4472
4484
|
]
|
|
4473
4485
|
},
|
|
4486
|
+
{
|
|
4487
|
+
"name": "designer create checkout",
|
|
4488
|
+
"persona": "designer",
|
|
4489
|
+
"verb": "create",
|
|
4490
|
+
"noun": "checkout",
|
|
4491
|
+
"access": "write",
|
|
4492
|
+
"api": "POST /designer/payments/basket/create-session",
|
|
4493
|
+
"external": null,
|
|
4494
|
+
"side_effects": [
|
|
4495
|
+
"records the brand's acceptance of the sampling/production terms",
|
|
4496
|
+
"creates a Stripe checkout session; nothing is charged until it is paid there"
|
|
4497
|
+
],
|
|
4498
|
+
"approval": "ask",
|
|
4499
|
+
"reversible": null,
|
|
4500
|
+
"idempotent": null,
|
|
4501
|
+
"limit": null,
|
|
4502
|
+
"legacy": [],
|
|
4503
|
+
"help": "POST /designer/payments/basket/create-session — a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_order_id>:deposit|final, shipping:<id>, misc:<id> (the `key` of each payable in `designer list payment`). Prints checkout_url.",
|
|
4504
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.create_checkout",
|
|
4505
|
+
"flags": [
|
|
4506
|
+
{
|
|
4507
|
+
"name": "key",
|
|
4508
|
+
"kind": "option",
|
|
4509
|
+
"opts": [
|
|
4510
|
+
"--key"
|
|
4511
|
+
],
|
|
4512
|
+
"type": "text",
|
|
4513
|
+
"required": true,
|
|
4514
|
+
"multiple": true,
|
|
4515
|
+
"default": null,
|
|
4516
|
+
"help": "Charge key to pay (repeat)."
|
|
4517
|
+
},
|
|
4518
|
+
{
|
|
4519
|
+
"name": "accept_terms",
|
|
4520
|
+
"kind": "option",
|
|
4521
|
+
"opts": [
|
|
4522
|
+
"--accept-terms"
|
|
4523
|
+
],
|
|
4524
|
+
"type": "flag",
|
|
4525
|
+
"required": false,
|
|
4526
|
+
"multiple": false,
|
|
4527
|
+
"default": false,
|
|
4528
|
+
"help": "The brand accepts the sampling/production terms for these charges."
|
|
4529
|
+
}
|
|
4530
|
+
]
|
|
4531
|
+
},
|
|
4474
4532
|
{
|
|
4475
4533
|
"name": "designer create collection",
|
|
4476
4534
|
"persona": "designer",
|
|
@@ -4711,6 +4769,88 @@
|
|
|
4711
4769
|
}
|
|
4712
4770
|
]
|
|
4713
4771
|
},
|
|
4772
|
+
{
|
|
4773
|
+
"name": "designer create quote-award",
|
|
4774
|
+
"persona": "designer",
|
|
4775
|
+
"verb": "create",
|
|
4776
|
+
"noun": "quote-award",
|
|
4777
|
+
"access": "write",
|
|
4778
|
+
"api": "POST /designer/quotes/group_pay_prepare",
|
|
4779
|
+
"external": null,
|
|
4780
|
+
"side_effects": [
|
|
4781
|
+
"creates (or reuses) a DRAFT sampling order on each chosen quote",
|
|
4782
|
+
"nothing is charged until that order is paid"
|
|
4783
|
+
],
|
|
4784
|
+
"approval": "ask",
|
|
4785
|
+
"reversible": null,
|
|
4786
|
+
"idempotent": null,
|
|
4787
|
+
"limit": null,
|
|
4788
|
+
"legacy": [],
|
|
4789
|
+
"help": "POST /designer/quotes/group_pay_prepare — choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat --option for the quote's optional extras. Prints the sampling_order_ids to pay with `designer create checkout --key sampling:<id>`.",
|
|
4790
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.create_quote_award",
|
|
4791
|
+
"flags": [
|
|
4792
|
+
{
|
|
4793
|
+
"name": "product_id",
|
|
4794
|
+
"kind": "argument",
|
|
4795
|
+
"opts": [
|
|
4796
|
+
"product_id"
|
|
4797
|
+
],
|
|
4798
|
+
"type": "text",
|
|
4799
|
+
"required": true,
|
|
4800
|
+
"multiple": false,
|
|
4801
|
+
"default": null,
|
|
4802
|
+
"help": "Product id."
|
|
4803
|
+
},
|
|
4804
|
+
{
|
|
4805
|
+
"name": "quote",
|
|
4806
|
+
"kind": "option",
|
|
4807
|
+
"opts": [
|
|
4808
|
+
"--quote"
|
|
4809
|
+
],
|
|
4810
|
+
"type": "text",
|
|
4811
|
+
"required": true,
|
|
4812
|
+
"multiple": false,
|
|
4813
|
+
"default": null,
|
|
4814
|
+
"help": "Supplier link id of the chosen quote."
|
|
4815
|
+
},
|
|
4816
|
+
{
|
|
4817
|
+
"name": "option",
|
|
4818
|
+
"kind": "option",
|
|
4819
|
+
"opts": [
|
|
4820
|
+
"--option"
|
|
4821
|
+
],
|
|
4822
|
+
"type": "text",
|
|
4823
|
+
"required": false,
|
|
4824
|
+
"multiple": true,
|
|
4825
|
+
"default": [],
|
|
4826
|
+
"help": "Optional extra on the quote to include (repeat)."
|
|
4827
|
+
},
|
|
4828
|
+
{
|
|
4829
|
+
"name": "no_base_sampling",
|
|
4830
|
+
"kind": "option",
|
|
4831
|
+
"opts": [
|
|
4832
|
+
"--no-base-sampling"
|
|
4833
|
+
],
|
|
4834
|
+
"type": "flag",
|
|
4835
|
+
"required": false,
|
|
4836
|
+
"multiple": false,
|
|
4837
|
+
"default": false,
|
|
4838
|
+
"help": "Leave the quote's base sample out."
|
|
4839
|
+
},
|
|
4840
|
+
{
|
|
4841
|
+
"name": "notes",
|
|
4842
|
+
"kind": "option",
|
|
4843
|
+
"opts": [
|
|
4844
|
+
"--notes"
|
|
4845
|
+
],
|
|
4846
|
+
"type": "text",
|
|
4847
|
+
"required": false,
|
|
4848
|
+
"multiple": false,
|
|
4849
|
+
"default": null,
|
|
4850
|
+
"help": "Note for the supplier."
|
|
4851
|
+
}
|
|
4852
|
+
]
|
|
4853
|
+
},
|
|
4714
4854
|
{
|
|
4715
4855
|
"name": "designer create rfq",
|
|
4716
4856
|
"persona": "designer",
|
|
@@ -5061,6 +5201,49 @@
|
|
|
5061
5201
|
}
|
|
5062
5202
|
]
|
|
5063
5203
|
},
|
|
5204
|
+
{
|
|
5205
|
+
"name": "designer get production-price",
|
|
5206
|
+
"persona": "designer",
|
|
5207
|
+
"verb": "get",
|
|
5208
|
+
"noun": "production-price",
|
|
5209
|
+
"access": "read",
|
|
5210
|
+
"api": "GET /designer/production_orders/{production_order_id}/calculate_price",
|
|
5211
|
+
"external": null,
|
|
5212
|
+
"side_effects": [],
|
|
5213
|
+
"approval": "auto",
|
|
5214
|
+
"reversible": null,
|
|
5215
|
+
"idempotent": null,
|
|
5216
|
+
"limit": null,
|
|
5217
|
+
"legacy": [],
|
|
5218
|
+
"help": "GET /designer/production_orders/{id}/calculate_price — what the deposit (or the balance, --phase final) comes to for the quantities on the run right now, line by line.",
|
|
5219
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.get_production_price",
|
|
5220
|
+
"flags": [
|
|
5221
|
+
{
|
|
5222
|
+
"name": "production_order_id",
|
|
5223
|
+
"kind": "argument",
|
|
5224
|
+
"opts": [
|
|
5225
|
+
"production_order_id"
|
|
5226
|
+
],
|
|
5227
|
+
"type": "text",
|
|
5228
|
+
"required": true,
|
|
5229
|
+
"multiple": false,
|
|
5230
|
+
"default": null,
|
|
5231
|
+
"help": "Production order id."
|
|
5232
|
+
},
|
|
5233
|
+
{
|
|
5234
|
+
"name": "phase",
|
|
5235
|
+
"kind": "option",
|
|
5236
|
+
"opts": [
|
|
5237
|
+
"--phase"
|
|
5238
|
+
],
|
|
5239
|
+
"type": "text",
|
|
5240
|
+
"required": false,
|
|
5241
|
+
"multiple": false,
|
|
5242
|
+
"default": "deposit",
|
|
5243
|
+
"help": "deposit or final."
|
|
5244
|
+
}
|
|
5245
|
+
]
|
|
5246
|
+
},
|
|
5064
5247
|
{
|
|
5065
5248
|
"name": "designer get quote-advice",
|
|
5066
5249
|
"persona": "designer",
|
|
@@ -5599,6 +5782,97 @@
|
|
|
5599
5782
|
}
|
|
5600
5783
|
]
|
|
5601
5784
|
},
|
|
5785
|
+
{
|
|
5786
|
+
"name": "designer set production-cart",
|
|
5787
|
+
"persona": "designer",
|
|
5788
|
+
"verb": "set",
|
|
5789
|
+
"noun": "production-cart",
|
|
5790
|
+
"access": "write",
|
|
5791
|
+
"api": "PUT /designer/production_orders/{production_order_id}/cart",
|
|
5792
|
+
"external": null,
|
|
5793
|
+
"side_effects": [
|
|
5794
|
+
"replaces the run's quantities; nothing is ordered until it is locked"
|
|
5795
|
+
],
|
|
5796
|
+
"approval": "auto",
|
|
5797
|
+
"reversible": null,
|
|
5798
|
+
"idempotent": null,
|
|
5799
|
+
"limit": null,
|
|
5800
|
+
"legacy": [],
|
|
5801
|
+
"help": "PUT /designer/production_orders/{id}/cart — the quantity of every size and colour on the run. Replaces the whole cart: pass every cell you want kept. --qty repeats as 'SIZE,#RRGGBB=COUNT' (quote it: # starts a shell comment), with the sizes and colour hex codes from the order's available_sizes / available_colors (`designer list production-order --product <id>`). Refused once the run is locked.",
|
|
5802
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.set_production_cart",
|
|
5803
|
+
"flags": [
|
|
5804
|
+
{
|
|
5805
|
+
"name": "production_order_id",
|
|
5806
|
+
"kind": "argument",
|
|
5807
|
+
"opts": [
|
|
5808
|
+
"production_order_id"
|
|
5809
|
+
],
|
|
5810
|
+
"type": "text",
|
|
5811
|
+
"required": true,
|
|
5812
|
+
"multiple": false,
|
|
5813
|
+
"default": null,
|
|
5814
|
+
"help": "Production order id."
|
|
5815
|
+
},
|
|
5816
|
+
{
|
|
5817
|
+
"name": "qty",
|
|
5818
|
+
"kind": "option",
|
|
5819
|
+
"opts": [
|
|
5820
|
+
"--qty"
|
|
5821
|
+
],
|
|
5822
|
+
"type": "text",
|
|
5823
|
+
"required": true,
|
|
5824
|
+
"multiple": true,
|
|
5825
|
+
"default": null,
|
|
5826
|
+
"help": "SIZE,#RRGGBB=COUNT for one cell (repeat)."
|
|
5827
|
+
}
|
|
5828
|
+
]
|
|
5829
|
+
},
|
|
5830
|
+
{
|
|
5831
|
+
"name": "designer set production-lock",
|
|
5832
|
+
"persona": "designer",
|
|
5833
|
+
"verb": "set",
|
|
5834
|
+
"noun": "production-lock",
|
|
5835
|
+
"access": "write",
|
|
5836
|
+
"api": "POST /designer/production_orders/{production_order_id}/configuration-lock",
|
|
5837
|
+
"external": null,
|
|
5838
|
+
"side_effects": [
|
|
5839
|
+
"places the order: the quantities are final and the deposit becomes payable",
|
|
5840
|
+
"can be unlocked again until the deposit is paid"
|
|
5841
|
+
],
|
|
5842
|
+
"approval": "ask",
|
|
5843
|
+
"reversible": null,
|
|
5844
|
+
"idempotent": null,
|
|
5845
|
+
"limit": null,
|
|
5846
|
+
"legacy": [],
|
|
5847
|
+
"help": "POST /designer/production_orders/{id}/configuration-lock — place the order with the quantities on the run (--unlock to reopen them). Locking is what makes the deposit payable: `designer create checkout --key production:<id>:deposit --accept-terms`.",
|
|
5848
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.set_production_lock",
|
|
5849
|
+
"flags": [
|
|
5850
|
+
{
|
|
5851
|
+
"name": "production_order_id",
|
|
5852
|
+
"kind": "argument",
|
|
5853
|
+
"opts": [
|
|
5854
|
+
"production_order_id"
|
|
5855
|
+
],
|
|
5856
|
+
"type": "text",
|
|
5857
|
+
"required": true,
|
|
5858
|
+
"multiple": false,
|
|
5859
|
+
"default": null,
|
|
5860
|
+
"help": "Production order id."
|
|
5861
|
+
},
|
|
5862
|
+
{
|
|
5863
|
+
"name": "unlock",
|
|
5864
|
+
"kind": "option",
|
|
5865
|
+
"opts": [
|
|
5866
|
+
"--unlock"
|
|
5867
|
+
],
|
|
5868
|
+
"type": "flag",
|
|
5869
|
+
"required": false,
|
|
5870
|
+
"multiple": false,
|
|
5871
|
+
"default": false,
|
|
5872
|
+
"help": "Reopen the quantities instead."
|
|
5873
|
+
}
|
|
5874
|
+
]
|
|
5875
|
+
},
|
|
5602
5876
|
{
|
|
5603
5877
|
"name": "designer submit production-order",
|
|
5604
5878
|
"persona": "designer",
|
|
@@ -5667,6 +5941,40 @@
|
|
|
5667
5941
|
}
|
|
5668
5942
|
]
|
|
5669
5943
|
},
|
|
5944
|
+
{
|
|
5945
|
+
"name": "designer submit tech-pack",
|
|
5946
|
+
"persona": "designer",
|
|
5947
|
+
"verb": "submit",
|
|
5948
|
+
"noun": "tech-pack",
|
|
5949
|
+
"access": "write",
|
|
5950
|
+
"api": "POST /designer/products/{product_id}/technical-specifications/confirm",
|
|
5951
|
+
"external": null,
|
|
5952
|
+
"side_effects": [
|
|
5953
|
+
"locks the tech pack as the version suppliers quote against",
|
|
5954
|
+
"opens the product for sourcing"
|
|
5955
|
+
],
|
|
5956
|
+
"approval": "ask",
|
|
5957
|
+
"reversible": null,
|
|
5958
|
+
"idempotent": null,
|
|
5959
|
+
"limit": null,
|
|
5960
|
+
"legacy": [],
|
|
5961
|
+
"help": "POST /designer/products/{id}/technical-specifications/confirm — confirm the tech pack, the step before anyone is asked to quote.",
|
|
5962
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.submit_tech_pack",
|
|
5963
|
+
"flags": [
|
|
5964
|
+
{
|
|
5965
|
+
"name": "product_id",
|
|
5966
|
+
"kind": "argument",
|
|
5967
|
+
"opts": [
|
|
5968
|
+
"product_id"
|
|
5969
|
+
],
|
|
5970
|
+
"type": "text",
|
|
5971
|
+
"required": true,
|
|
5972
|
+
"multiple": false,
|
|
5973
|
+
"default": null,
|
|
5974
|
+
"help": "Product id."
|
|
5975
|
+
}
|
|
5976
|
+
]
|
|
5977
|
+
},
|
|
5670
5978
|
{
|
|
5671
5979
|
"name": "designer update action",
|
|
5672
5980
|
"persona": "designer",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Token broker mode: the CLI trades its own argv for a token, and has no fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
import tempfile
|
|
9
|
+
import threading
|
|
10
|
+
import unittest
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
from makea_cli import api_client, broker
|
|
14
|
+
from makea_cli.errors import AuthError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _FakeBroker:
|
|
18
|
+
"""A unix-socket broker that answers one canned reply per connection."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, reply: dict):
|
|
21
|
+
self.reply = reply
|
|
22
|
+
self.seen: list[dict] = []
|
|
23
|
+
self._dir = tempfile.mkdtemp()
|
|
24
|
+
self.path = os.path.join(self._dir, "b.sock")
|
|
25
|
+
self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
26
|
+
self._sock.bind(self.path)
|
|
27
|
+
self._sock.listen(4)
|
|
28
|
+
self._thread = threading.Thread(target=self._serve, daemon=True)
|
|
29
|
+
self._thread.start()
|
|
30
|
+
|
|
31
|
+
def _serve(self):
|
|
32
|
+
while True:
|
|
33
|
+
try:
|
|
34
|
+
conn, _ = self._sock.accept()
|
|
35
|
+
except OSError:
|
|
36
|
+
return
|
|
37
|
+
with conn:
|
|
38
|
+
data = b""
|
|
39
|
+
while not data.endswith(b"\n"):
|
|
40
|
+
chunk = conn.recv(4096)
|
|
41
|
+
if not chunk:
|
|
42
|
+
break
|
|
43
|
+
data += chunk
|
|
44
|
+
self.seen.append(json.loads(data.decode() or "{}"))
|
|
45
|
+
conn.sendall(json.dumps(self.reply).encode() + b"\n")
|
|
46
|
+
|
|
47
|
+
def close(self):
|
|
48
|
+
self._sock.close()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BrokerTests(unittest.TestCase):
|
|
52
|
+
def setUp(self):
|
|
53
|
+
broker._cached = None
|
|
54
|
+
|
|
55
|
+
def tearDown(self):
|
|
56
|
+
broker._cached = None
|
|
57
|
+
|
|
58
|
+
def test_argv_is_exchanged_for_a_token(self):
|
|
59
|
+
fake = _FakeBroker({"ok": True, "token": "delegation_abc", "persona": "designer"})
|
|
60
|
+
try:
|
|
61
|
+
with mock.patch.dict(os.environ, {broker.BROKER_ENV: fake.path}):
|
|
62
|
+
with mock.patch("sys.argv", ["makea-cli", "designer", "list", "product"]):
|
|
63
|
+
headers = api_client._headers()
|
|
64
|
+
finally:
|
|
65
|
+
fake.close()
|
|
66
|
+
self.assertEqual(headers["x-impersonation-token"], "delegation_abc")
|
|
67
|
+
self.assertNotIn("Authorization", headers)
|
|
68
|
+
self.assertEqual(fake.seen, [{"argv": ["designer", "list", "product"]}])
|
|
69
|
+
|
|
70
|
+
def test_one_invocation_spends_one_ticket(self):
|
|
71
|
+
fake = _FakeBroker({"ok": True, "token": "delegation_abc"})
|
|
72
|
+
try:
|
|
73
|
+
with mock.patch.dict(os.environ, {broker.BROKER_ENV: fake.path}):
|
|
74
|
+
api_client._headers()
|
|
75
|
+
api_client._headers()
|
|
76
|
+
finally:
|
|
77
|
+
fake.close()
|
|
78
|
+
self.assertEqual(len(fake.seen), 1)
|
|
79
|
+
|
|
80
|
+
def test_a_refusal_is_an_auth_error_not_a_login_fallback(self):
|
|
81
|
+
fake = _FakeBroker({"ok": False, "error": "no ticket for this command"})
|
|
82
|
+
try:
|
|
83
|
+
with mock.patch.dict(os.environ, {broker.BROKER_ENV: fake.path}):
|
|
84
|
+
with mock.patch.object(api_client, "ensure_fresh_id_token") as login:
|
|
85
|
+
with self.assertRaises(AuthError) as ctx:
|
|
86
|
+
api_client._headers()
|
|
87
|
+
login.assert_not_called()
|
|
88
|
+
finally:
|
|
89
|
+
fake.close()
|
|
90
|
+
self.assertIn("no ticket", ctx.exception.message)
|
|
91
|
+
|
|
92
|
+
def test_a_missing_broker_is_an_auth_error(self):
|
|
93
|
+
with mock.patch.dict(os.environ, {broker.BROKER_ENV: "/nonexistent/b.sock"}):
|
|
94
|
+
with self.assertRaises(AuthError):
|
|
95
|
+
api_client._headers()
|
|
96
|
+
|
|
97
|
+
def test_without_a_broker_the_login_is_used(self):
|
|
98
|
+
env = {k: v for k, v in os.environ.items() if k != broker.BROKER_ENV}
|
|
99
|
+
with mock.patch.dict(os.environ, env, clear=True):
|
|
100
|
+
with mock.patch.object(api_client, "ensure_fresh_id_token", return_value="jwt"):
|
|
101
|
+
headers = api_client._headers()
|
|
102
|
+
self.assertEqual(headers["Authorization"], "Bearer jwt")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
unittest.main()
|
|
@@ -395,7 +395,16 @@ class OrderLifecycleTests(_PersonaCase):
|
|
|
395
395
|
|
|
396
396
|
|
|
397
397
|
class NoMoneyPathTests(unittest.TestCase):
|
|
398
|
-
"""No CLI command may
|
|
398
|
+
"""No CLI command may move money; one may hand the brand a link to pay.
|
|
399
|
+
|
|
400
|
+
``designer create checkout`` creates a Stripe checkout session and nothing
|
|
401
|
+
else: the charge happens on Stripe's page, entered by the brand. It is the
|
|
402
|
+
only write allowed near a payment endpoint, and it always asks first,
|
|
403
|
+
because it also records the brand's acceptance of the terms. Bank
|
|
404
|
+
transfers, subscriptions and every other payment write stay out.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
CHECKOUT_LINK = "designer create checkout"
|
|
399
408
|
|
|
400
409
|
def test_no_command_touches_money(self):
|
|
401
410
|
from makea_cli import registry
|
|
@@ -406,6 +415,19 @@ class NoMoneyPathTests(unittest.TestCase):
|
|
|
406
415
|
offenders = [
|
|
407
416
|
(s.name, s.api)
|
|
408
417
|
for s in registry.specs()
|
|
409
|
-
if not s.external
|
|
418
|
+
if not s.external
|
|
419
|
+
and any(f in s.api for f in forbidden)
|
|
420
|
+
and s.access == "write"
|
|
421
|
+
and s.name != self.CHECKOUT_LINK
|
|
410
422
|
]
|
|
411
423
|
self.assertEqual(offenders, [], "a write command reaches money movement")
|
|
424
|
+
|
|
425
|
+
def test_the_checkout_link_asks_and_only_creates_a_session(self):
|
|
426
|
+
from makea_cli import registry
|
|
427
|
+
from makea_cli.main import build_cli
|
|
428
|
+
|
|
429
|
+
build_cli()
|
|
430
|
+
spec = registry.spec_for("designer", "create", "checkout")
|
|
431
|
+
self.assertIsNotNone(spec)
|
|
432
|
+
self.assertEqual(spec.approval, "ask")
|
|
433
|
+
self.assertEqual(spec.api, "POST /designer/payments/basket/create-session")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
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
|