amazon-sp-cli 0.1.2__tar.gz → 0.1.4__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.
- {amazon_sp_cli-0.1.2/amazon_sp_cli.egg-info → amazon_sp_cli-0.1.4}/PKG-INFO +1 -1
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli/main.py +169 -84
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4/amazon_sp_cli.egg-info}/PKG-INFO +1 -1
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli.egg-info/SOURCES.txt +1 -1
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/setup.py +1 -1
- amazon_sp_cli-0.1.2/tests/test_coupon.py → amazon_sp_cli-0.1.4/tests/test_sale_price.py +23 -28
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/LICENSE +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/MANIFEST.in +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/README.md +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli/__init__.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli/__main__.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli/auth.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli/client.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli.egg-info/dependency_links.txt +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli.egg-info/entry_points.txt +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli.egg-info/requires.txt +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/amazon_sp_cli.egg-info/top_level.txt +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/pyproject.toml +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/setup.cfg +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/tests/__init__.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/tests/test_auth.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.4}/tests/test_client.py +0 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"""Main CLI entry point for Amazon SP-API CLI."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
|
|
6
7
|
import click
|
|
8
|
+
import yaml
|
|
7
9
|
|
|
8
10
|
from .auth import SPAPIAuth
|
|
9
11
|
from .client import SPAPIClient
|
|
10
12
|
|
|
13
|
+
DEFAULT_CREDENTIALS_PATH = os.path.expanduser("~/.config/amazon-sp-cli/credentials.yml")
|
|
14
|
+
|
|
11
15
|
|
|
12
16
|
def _check_path():
|
|
13
17
|
"""Check if the CLI is accessible in PATH and warn once per day."""
|
|
@@ -62,6 +66,122 @@ def cli(ctx, credentials):
|
|
|
62
66
|
ctx.obj["client"] = SPAPIClient(ctx.obj["auth"])
|
|
63
67
|
|
|
64
68
|
|
|
69
|
+
@cli.group()
|
|
70
|
+
def auth():
|
|
71
|
+
"""Authentication commands."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@auth.command("setup")
|
|
76
|
+
@click.option("--path", default=DEFAULT_CREDENTIALS_PATH, help="Path to save credentials")
|
|
77
|
+
@click.option("--profile", default="default", help="Credential profile name")
|
|
78
|
+
@click.option("--refresh-token", help="Refresh token")
|
|
79
|
+
@click.option("--client-id", help="Client ID")
|
|
80
|
+
@click.option("--client-secret", help="Client secret")
|
|
81
|
+
@click.option("--aws-access-key-id", help="AWS Access Key ID")
|
|
82
|
+
@click.option("--aws-secret-access-key", help="AWS Secret Access Key")
|
|
83
|
+
@click.option("--seller-id", default="A2GKV2AN9F8YG3", help="Seller ID")
|
|
84
|
+
@click.option("--marketplace-id", default="ATVPDKIKX0DER", help="Marketplace ID")
|
|
85
|
+
@click.pass_context
|
|
86
|
+
def auth_setup(
|
|
87
|
+
ctx,
|
|
88
|
+
path,
|
|
89
|
+
profile,
|
|
90
|
+
refresh_token,
|
|
91
|
+
client_id,
|
|
92
|
+
client_secret,
|
|
93
|
+
aws_access_key_id,
|
|
94
|
+
aws_secret_access_key,
|
|
95
|
+
seller_id,
|
|
96
|
+
marketplace_id,
|
|
97
|
+
):
|
|
98
|
+
"""Set up Amazon SP-API credentials.
|
|
99
|
+
|
|
100
|
+
When flags are omitted, falls back to interactive prompts.
|
|
101
|
+
"""
|
|
102
|
+
click.echo("🔐 Amazon SP-API Credential Setup")
|
|
103
|
+
click.echo("=" * 50)
|
|
104
|
+
click.echo()
|
|
105
|
+
|
|
106
|
+
interactive = not all([refresh_token, client_id, client_secret, aws_access_key_id, aws_secret_access_key])
|
|
107
|
+
if interactive:
|
|
108
|
+
click.echo("You'll need the following from your Amazon Developer account:")
|
|
109
|
+
click.echo(" 1. Refresh Token (from LWA authorization)")
|
|
110
|
+
click.echo(" 2. Client ID (from your app registration)")
|
|
111
|
+
click.echo(" 3. Client Secret (from your app registration)")
|
|
112
|
+
click.echo(" 4. AWS Access Key ID")
|
|
113
|
+
click.echo(" 5. AWS Secret Access Key")
|
|
114
|
+
click.echo()
|
|
115
|
+
|
|
116
|
+
profile = profile or click.prompt("Profile name", default="default")
|
|
117
|
+
refresh_token = refresh_token or click.prompt("Refresh token", hide_input=True)
|
|
118
|
+
client_id = client_id or click.prompt("Client ID")
|
|
119
|
+
client_secret = client_secret or click.prompt("Client secret", hide_input=True)
|
|
120
|
+
aws_access_key_id = aws_access_key_id or click.prompt("AWS Access Key ID")
|
|
121
|
+
aws_secret_access_key = aws_secret_access_key or click.prompt("AWS Secret Access Key", hide_input=True)
|
|
122
|
+
|
|
123
|
+
credentials = {
|
|
124
|
+
"version": "1.0",
|
|
125
|
+
profile: {
|
|
126
|
+
"refresh_token": refresh_token,
|
|
127
|
+
"client_id": client_id,
|
|
128
|
+
"client_secret": client_secret,
|
|
129
|
+
"aws_access_key_id": aws_access_key_id,
|
|
130
|
+
"aws_secret_access_key": aws_secret_access_key,
|
|
131
|
+
"seller_id": seller_id,
|
|
132
|
+
"marketplace_id": marketplace_id,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Merge with existing if present
|
|
137
|
+
if os.path.exists(path):
|
|
138
|
+
try:
|
|
139
|
+
with open(path, "r") as f:
|
|
140
|
+
existing = yaml.safe_load(f) or {}
|
|
141
|
+
existing[profile] = credentials[profile]
|
|
142
|
+
credentials = existing
|
|
143
|
+
click.echo(f"\n📝 Merged with existing credentials at {path}")
|
|
144
|
+
except Exception as e:
|
|
145
|
+
click.echo(f"⚠️ Could not read existing file: {e}")
|
|
146
|
+
|
|
147
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
148
|
+
with open(path, "w") as f:
|
|
149
|
+
yaml.dump(credentials, f, default_flow_style=False, sort_keys=False)
|
|
150
|
+
|
|
151
|
+
click.echo(f"✅ Credentials saved to {path}")
|
|
152
|
+
click.echo(f" Profile: {profile}")
|
|
153
|
+
click.echo(f" Seller ID: {seller_id}")
|
|
154
|
+
click.echo(f" Marketplace ID: {marketplace_id}")
|
|
155
|
+
click.echo()
|
|
156
|
+
click.echo("You can now use: python -m amazon_sp_cli.main --profile {profile} get-price <sku>")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@auth.command("show")
|
|
160
|
+
@click.option("--path", default=DEFAULT_CREDENTIALS_PATH, help="Path to credentials file")
|
|
161
|
+
@click.pass_context
|
|
162
|
+
def auth_show(ctx, path):
|
|
163
|
+
"""Show configured profiles (without secrets)."""
|
|
164
|
+
if not os.path.exists(path):
|
|
165
|
+
click.echo(f"❌ No credentials file found at {path}")
|
|
166
|
+
click.echo("Run: python -m amazon_sp_cli.main auth setup")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
with open(path, "r") as f:
|
|
170
|
+
creds = yaml.safe_load(f) or {}
|
|
171
|
+
|
|
172
|
+
click.echo(f"\n📄 Credentials file: {path}")
|
|
173
|
+
click.echo("-" * 40)
|
|
174
|
+
|
|
175
|
+
for profile, data in creds.items():
|
|
176
|
+
if profile == "version":
|
|
177
|
+
continue
|
|
178
|
+
click.echo(f"Profile: {profile}")
|
|
179
|
+
click.echo(f" Client ID: {data.get('client_id', 'N/A')[:20]}...")
|
|
180
|
+
click.echo(f" Seller ID: {data.get('seller_id', 'N/A')}")
|
|
181
|
+
click.echo(f" Marketplace ID: {data.get('marketplace_id', 'N/A')}")
|
|
182
|
+
click.echo()
|
|
183
|
+
|
|
184
|
+
|
|
65
185
|
@cli.command()
|
|
66
186
|
@click.argument("sku")
|
|
67
187
|
@click.pass_context
|
|
@@ -189,43 +309,33 @@ def create_discount(ctx, sku, percent, all_variations):
|
|
|
189
309
|
@click.argument("discount", type=float)
|
|
190
310
|
@click.option(
|
|
191
311
|
"--type",
|
|
192
|
-
"
|
|
312
|
+
"discount_type",
|
|
193
313
|
type=click.Choice(["percentage", "fixed"]),
|
|
194
314
|
default="percentage",
|
|
195
|
-
help="
|
|
315
|
+
help="Discount type: percentage or fixed amount off",
|
|
196
316
|
)
|
|
197
|
-
@click.option("--min-purchase", type=float, default=0, help="Minimum purchase amount required")
|
|
198
317
|
@click.option("--start-date", help="Start date (YYYY-MM-DD). Defaults to today")
|
|
199
318
|
@click.option("--end-date", help="End date (YYYY-MM-DD). Defaults to 30 days from start")
|
|
200
|
-
@click.option("--
|
|
201
|
-
@click.option("--customer-budget", type=float, help="Maximum discount per customer (defaults to full discount)")
|
|
202
|
-
@click.option("--prime-only", is_flag=True, help="Prime members only")
|
|
203
|
-
@click.option("--clip-coupon", is_flag=True, default=True, help="Require customers to clip coupon (default: True)")
|
|
204
|
-
@click.option("--output", "-o", type=click.File("w"), help="Save coupon data to file")
|
|
319
|
+
@click.option("--output", "-o", type=click.File("w"), help="Save price adjustment data to file")
|
|
205
320
|
@click.pass_context
|
|
206
|
-
def
|
|
321
|
+
def sale_price(
|
|
207
322
|
ctx,
|
|
208
323
|
sku,
|
|
209
324
|
discount,
|
|
210
|
-
|
|
211
|
-
min_purchase,
|
|
325
|
+
discount_type,
|
|
212
326
|
start_date,
|
|
213
327
|
end_date,
|
|
214
|
-
budget,
|
|
215
|
-
customer_budget,
|
|
216
|
-
prime_only,
|
|
217
|
-
clip_coupon,
|
|
218
328
|
output,
|
|
219
329
|
):
|
|
220
|
-
"""
|
|
330
|
+
"""Generate sale price data for a SKU.
|
|
221
331
|
|
|
222
|
-
|
|
223
|
-
the
|
|
332
|
+
SP-API does not support direct sale price creation. This generates
|
|
333
|
+
the feed data you can submit via the Feeds API or use in Seller Central.
|
|
224
334
|
|
|
225
335
|
Examples:
|
|
226
|
-
amz-sp
|
|
227
|
-
amz-sp
|
|
228
|
-
amz-sp
|
|
336
|
+
amz-sp sale-price PAW2603190101 20
|
|
337
|
+
amz-sp sale-price PAW2603190101 5 --type fixed
|
|
338
|
+
amz-sp sale-price PAW2603190101 15 --start-date 2026-05-01 --end-date 2026-05-31
|
|
229
339
|
"""
|
|
230
340
|
client = ctx.obj["client"]
|
|
231
341
|
|
|
@@ -251,94 +361,69 @@ def create_coupon(
|
|
|
251
361
|
end_str = end.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
252
362
|
|
|
253
363
|
# Calculate discount values
|
|
254
|
-
if
|
|
364
|
+
if discount_type == "percentage":
|
|
255
365
|
discount_amount = round(current_price * discount / 100, 2)
|
|
256
366
|
discount_display = f"{discount}%"
|
|
257
367
|
else:
|
|
258
368
|
discount_amount = discount
|
|
259
369
|
discount_display = f"${discount}"
|
|
260
370
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
# Customer budget defaults to discount amount if not specified
|
|
264
|
-
per_customer = customer_budget or discount_amount
|
|
371
|
+
new_sale_price = max(0, current_price - discount_amount)
|
|
265
372
|
|
|
266
|
-
# Build
|
|
267
|
-
|
|
268
|
-
"
|
|
373
|
+
# Build sale price feed data
|
|
374
|
+
feed = {
|
|
375
|
+
"sku": sku,
|
|
376
|
+
"asin": response.get("summaries", [{}])[0].get("asin"),
|
|
377
|
+
"pricing": {
|
|
378
|
+
"original_price": current_price,
|
|
379
|
+
"sale_price": new_sale_price,
|
|
380
|
+
"discount_amount": discount_amount,
|
|
381
|
+
"discount_display": discount_display,
|
|
382
|
+
},
|
|
383
|
+
"schedule": {
|
|
384
|
+
"start_date": start_str,
|
|
385
|
+
"end_date": end_str,
|
|
386
|
+
"duration_days": (end - start).days,
|
|
387
|
+
},
|
|
388
|
+
"feed_data": {
|
|
389
|
+
"messageId": 1,
|
|
269
390
|
"sku": sku,
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
391
|
+
"operationType": "PARTIAL_UPDATE",
|
|
392
|
+
"productType": "PET_TOY",
|
|
393
|
+
"attributes": {
|
|
394
|
+
"list_price": [{"currency": "USD", "value": current_price}],
|
|
395
|
+
"sale_price": [
|
|
396
|
+
{
|
|
397
|
+
"currency": "USD",
|
|
398
|
+
"value": new_sale_price,
|
|
399
|
+
"effective_date": start_str,
|
|
400
|
+
"end_date": end_str,
|
|
401
|
+
}
|
|
402
|
+
],
|
|
275
403
|
},
|
|
276
|
-
"pricing": {
|
|
277
|
-
"original_price": current_price,
|
|
278
|
-
"discounted_price": sale_price,
|
|
279
|
-
"savings": discount_amount,
|
|
280
|
-
},
|
|
281
|
-
"requirements": {
|
|
282
|
-
"minimum_purchase": min_purchase if min_purchase > 0 else None,
|
|
283
|
-
"prime_only": prime_only,
|
|
284
|
-
"clip_required": clip_coupon,
|
|
285
|
-
},
|
|
286
|
-
"schedule": {
|
|
287
|
-
"start_date": start_str,
|
|
288
|
-
"end_date": end_str,
|
|
289
|
-
"duration_days": (end - start).days,
|
|
290
|
-
},
|
|
291
|
-
"budget": {
|
|
292
|
-
"total_budget": budget,
|
|
293
|
-
"per_customer_max": per_customer,
|
|
294
|
-
"estimated_redemptions": int(budget / discount_amount) if discount_amount > 0 else 0,
|
|
295
|
-
},
|
|
296
|
-
"status": "DRAFT",
|
|
297
404
|
},
|
|
298
|
-
"seller_central_steps": [
|
|
299
|
-
"1. Go to Seller Central → Advertising → Coupons",
|
|
300
|
-
"2. Click 'Create a new coupon'",
|
|
301
|
-
"3. Search for the ASIN or SKU above",
|
|
302
|
-
"4. Select discount type: " + ("Percentage Off" if coupon_type == "percentage" else "Money Off"),
|
|
303
|
-
"5. Enter discount value: " + str(discount),
|
|
304
|
-
"6. Set budget: $" + str(budget),
|
|
305
|
-
"7. Set schedule: " + start_str + " to " + end_str,
|
|
306
|
-
f"8. {'Enable Prime-only targeting' if prime_only else 'Target all customers'}",
|
|
307
|
-
"9. Review and submit",
|
|
308
|
-
],
|
|
309
|
-
"notes": [
|
|
310
|
-
"Coupons typically take 4-8 hours to activate after submission",
|
|
311
|
-
"Amazon charges $0.60 per redemption (US marketplace)",
|
|
312
|
-
"Coupon will display on product detail page and search results",
|
|
313
|
-
f"Estimated cost per redemption: ${discount_amount + 0.60:.2f} (discount + Amazon fee)",
|
|
314
|
-
],
|
|
315
405
|
}
|
|
316
406
|
|
|
317
|
-
# Remove None values
|
|
318
|
-
if coupon["coupon_specification"]["requirements"]["minimum_purchase"] is None:
|
|
319
|
-
del coupon["coupon_specification"]["requirements"]["minimum_purchase"]
|
|
320
|
-
|
|
321
407
|
# Output
|
|
322
|
-
output_json = json.dumps(
|
|
408
|
+
output_json = json.dumps(feed, indent=2)
|
|
323
409
|
|
|
324
410
|
if output:
|
|
325
411
|
output.write(output_json)
|
|
326
|
-
click.echo(f"✓
|
|
412
|
+
click.echo(f"✓ Sale price data saved to {output.name}")
|
|
327
413
|
|
|
328
414
|
click.echo(output_json)
|
|
329
415
|
|
|
330
416
|
# Summary
|
|
331
417
|
click.echo("\n" + "=" * 50)
|
|
332
|
-
click.echo("SUMMARY")
|
|
418
|
+
click.echo("SALE PRICE SUMMARY")
|
|
333
419
|
click.echo("=" * 50)
|
|
334
420
|
click.echo(f"SKU: {sku}")
|
|
335
421
|
click.echo(f"Discount: {discount_display}")
|
|
336
|
-
click.echo(f"Price: ${current_price} → ${
|
|
337
|
-
click.echo(f"Budget: ${budget}")
|
|
422
|
+
click.echo(f"Price: ${current_price} → ${new_sale_price}")
|
|
338
423
|
click.echo(f"Duration: {start_str} to {end_str}")
|
|
339
|
-
click.echo(
|
|
340
|
-
click.echo("
|
|
341
|
-
click.echo("
|
|
424
|
+
click.echo("\n⚠️ SP-API does not support direct sale price creation.")
|
|
425
|
+
click.echo(" Submit the feed_data above via the Feeds API")
|
|
426
|
+
click.echo(" or update manually in Seller Central.")
|
|
342
427
|
|
|
343
428
|
except Exception as e:
|
|
344
429
|
click.echo(f"Error: {e}", err=True)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for
|
|
1
|
+
"""Tests for sale-price command."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from unittest.mock import Mock, patch
|
|
@@ -9,8 +9,8 @@ from click.testing import CliRunner
|
|
|
9
9
|
from amazon_sp_cli.main import cli
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class
|
|
13
|
-
"""Test
|
|
12
|
+
class TestSalePrice:
|
|
13
|
+
"""Test sale-price command."""
|
|
14
14
|
|
|
15
15
|
@pytest.fixture
|
|
16
16
|
def mock_listing_response(self):
|
|
@@ -27,8 +27,8 @@ class TestCreateCoupon:
|
|
|
27
27
|
|
|
28
28
|
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
29
29
|
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
30
|
-
def
|
|
31
|
-
"""Test
|
|
30
|
+
def test_sale_price_percentage(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
|
|
31
|
+
"""Test generating percentage sale price."""
|
|
32
32
|
mock_client = Mock()
|
|
33
33
|
mock_client.get_listing.return_value = mock_listing_response
|
|
34
34
|
mock_client_class.return_value = mock_client
|
|
@@ -36,18 +36,18 @@ class TestCreateCoupon:
|
|
|
36
36
|
mock_auth = Mock()
|
|
37
37
|
mock_auth_class.return_value = mock_auth
|
|
38
38
|
|
|
39
|
-
result = runner.invoke(cli, ["
|
|
39
|
+
result = runner.invoke(cli, ["sale-price", "TEST-SKU", "20"])
|
|
40
40
|
|
|
41
41
|
assert result.exit_code == 0
|
|
42
42
|
assert "20.0%" in result.output
|
|
43
43
|
assert "$29.99" in result.output
|
|
44
|
-
assert "$23.99" in result.output
|
|
45
|
-
assert "
|
|
44
|
+
assert "$23.99" in result.output
|
|
45
|
+
assert "feed_data" in result.output
|
|
46
46
|
|
|
47
47
|
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
48
48
|
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
49
|
-
def
|
|
50
|
-
"""Test
|
|
49
|
+
def test_sale_price_fixed(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
|
|
50
|
+
"""Test generating fixed amount sale price."""
|
|
51
51
|
mock_client = Mock()
|
|
52
52
|
mock_client.get_listing.return_value = mock_listing_response
|
|
53
53
|
mock_client_class.return_value = mock_client
|
|
@@ -55,16 +55,16 @@ class TestCreateCoupon:
|
|
|
55
55
|
mock_auth = Mock()
|
|
56
56
|
mock_auth_class.return_value = mock_auth
|
|
57
57
|
|
|
58
|
-
result = runner.invoke(cli, ["
|
|
58
|
+
result = runner.invoke(cli, ["sale-price", "TEST-SKU", "5", "--type", "fixed"])
|
|
59
59
|
|
|
60
60
|
assert result.exit_code == 0
|
|
61
61
|
assert "$5" in result.output
|
|
62
|
-
assert "$24.99" in result.output
|
|
62
|
+
assert "$24.99" in result.output
|
|
63
63
|
|
|
64
64
|
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
65
65
|
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
66
|
-
def
|
|
67
|
-
"""Test
|
|
66
|
+
def test_sale_price_with_dates(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
|
|
67
|
+
"""Test generating sale price with custom dates."""
|
|
68
68
|
mock_client = Mock()
|
|
69
69
|
mock_client.get_listing.return_value = mock_listing_response
|
|
70
70
|
mock_client_class.return_value = mock_client
|
|
@@ -75,12 +75,9 @@ class TestCreateCoupon:
|
|
|
75
75
|
result = runner.invoke(
|
|
76
76
|
cli,
|
|
77
77
|
[
|
|
78
|
-
"
|
|
78
|
+
"sale-price",
|
|
79
79
|
"TEST-SKU",
|
|
80
80
|
"15",
|
|
81
|
-
"--prime-only",
|
|
82
|
-
"--budget",
|
|
83
|
-
"1000",
|
|
84
81
|
"--start-date",
|
|
85
82
|
"2026-05-01",
|
|
86
83
|
"--end-date",
|
|
@@ -89,15 +86,13 @@ class TestCreateCoupon:
|
|
|
89
86
|
)
|
|
90
87
|
|
|
91
88
|
assert result.exit_code == 0
|
|
92
|
-
assert "Prime Only: Yes" in result.output
|
|
93
|
-
assert "$1000" in result.output
|
|
94
89
|
assert "2026-05-01" in result.output
|
|
95
90
|
assert "2026-06-01" in result.output
|
|
96
91
|
|
|
97
92
|
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
98
93
|
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
99
|
-
def
|
|
100
|
-
"""Test saving
|
|
94
|
+
def test_sale_price_output_file(self, mock_client_class, mock_auth_class, runner, mock_listing_response):
|
|
95
|
+
"""Test saving sale price data to file."""
|
|
101
96
|
mock_client = Mock()
|
|
102
97
|
mock_client.get_listing.return_value = mock_listing_response
|
|
103
98
|
mock_client_class.return_value = mock_client
|
|
@@ -106,20 +101,20 @@ class TestCreateCoupon:
|
|
|
106
101
|
mock_auth_class.return_value = mock_auth
|
|
107
102
|
|
|
108
103
|
with runner.isolated_filesystem():
|
|
109
|
-
result = runner.invoke(cli, ["
|
|
104
|
+
result = runner.invoke(cli, ["sale-price", "TEST-SKU", "10", "-o", "sale-price.json"])
|
|
110
105
|
|
|
111
106
|
assert result.exit_code == 0
|
|
112
107
|
assert "saved to" in result.output
|
|
113
108
|
|
|
114
109
|
# Verify file was created
|
|
115
|
-
with open("
|
|
110
|
+
with open("sale-price.json") as f:
|
|
116
111
|
data = json.load(f)
|
|
117
|
-
assert data["
|
|
118
|
-
assert data["
|
|
112
|
+
assert data["sku"] == "TEST-SKU"
|
|
113
|
+
assert data["pricing"]["discount_display"] == "10.0%"
|
|
119
114
|
|
|
120
115
|
@patch("amazon_sp_cli.main.SPAPIAuth")
|
|
121
116
|
@patch("amazon_sp_cli.main.SPAPIClient")
|
|
122
|
-
def
|
|
117
|
+
def test_sale_price_no_price(self, mock_client_class, mock_auth_class, runner):
|
|
123
118
|
"""Test error when listing has no price."""
|
|
124
119
|
mock_client = Mock()
|
|
125
120
|
mock_client.get_listing.return_value = {
|
|
@@ -131,7 +126,7 @@ class TestCreateCoupon:
|
|
|
131
126
|
mock_auth = Mock()
|
|
132
127
|
mock_auth_class.return_value = mock_auth
|
|
133
128
|
|
|
134
|
-
result = runner.invoke(cli, ["
|
|
129
|
+
result = runner.invoke(cli, ["sale-price", "TEST-SKU", "20"])
|
|
135
130
|
|
|
136
131
|
assert result.exit_code != 0
|
|
137
132
|
assert "Error" in result.output
|
|
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
|