amazon-sp-cli 0.1.2__tar.gz → 0.1.3__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.3}/PKG-INFO +1 -1
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli/main.py +49 -84
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3/amazon_sp_cli.egg-info}/PKG-INFO +1 -1
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/SOURCES.txt +1 -1
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/setup.py +1 -1
- amazon_sp_cli-0.1.2/tests/test_coupon.py → amazon_sp_cli-0.1.3/tests/test_sale_price.py +23 -28
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/LICENSE +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/MANIFEST.in +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/README.md +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli/__init__.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli/__main__.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli/auth.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli/client.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/dependency_links.txt +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/entry_points.txt +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/requires.txt +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/amazon_sp_cli.egg-info/top_level.txt +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/pyproject.toml +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/setup.cfg +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/tests/__init__.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/tests/test_auth.py +0 -0
- {amazon_sp_cli-0.1.2 → amazon_sp_cli-0.1.3}/tests/test_client.py +0 -0
|
@@ -189,43 +189,33 @@ def create_discount(ctx, sku, percent, all_variations):
|
|
|
189
189
|
@click.argument("discount", type=float)
|
|
190
190
|
@click.option(
|
|
191
191
|
"--type",
|
|
192
|
-
"
|
|
192
|
+
"discount_type",
|
|
193
193
|
type=click.Choice(["percentage", "fixed"]),
|
|
194
194
|
default="percentage",
|
|
195
|
-
help="
|
|
195
|
+
help="Discount type: percentage or fixed amount off",
|
|
196
196
|
)
|
|
197
|
-
@click.option("--min-purchase", type=float, default=0, help="Minimum purchase amount required")
|
|
198
197
|
@click.option("--start-date", help="Start date (YYYY-MM-DD). Defaults to today")
|
|
199
198
|
@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")
|
|
199
|
+
@click.option("--output", "-o", type=click.File("w"), help="Save price adjustment data to file")
|
|
205
200
|
@click.pass_context
|
|
206
|
-
def
|
|
201
|
+
def sale_price(
|
|
207
202
|
ctx,
|
|
208
203
|
sku,
|
|
209
204
|
discount,
|
|
210
|
-
|
|
211
|
-
min_purchase,
|
|
205
|
+
discount_type,
|
|
212
206
|
start_date,
|
|
213
207
|
end_date,
|
|
214
|
-
budget,
|
|
215
|
-
customer_budget,
|
|
216
|
-
prime_only,
|
|
217
|
-
clip_coupon,
|
|
218
208
|
output,
|
|
219
209
|
):
|
|
220
|
-
"""
|
|
210
|
+
"""Generate sale price data for a SKU.
|
|
221
211
|
|
|
222
|
-
|
|
223
|
-
the
|
|
212
|
+
SP-API does not support direct sale price creation. This generates
|
|
213
|
+
the feed data you can submit via the Feeds API or use in Seller Central.
|
|
224
214
|
|
|
225
215
|
Examples:
|
|
226
|
-
amz-sp
|
|
227
|
-
amz-sp
|
|
228
|
-
amz-sp
|
|
216
|
+
amz-sp sale-price PAW2603190101 20
|
|
217
|
+
amz-sp sale-price PAW2603190101 5 --type fixed
|
|
218
|
+
amz-sp sale-price PAW2603190101 15 --start-date 2026-05-01 --end-date 2026-05-31
|
|
229
219
|
"""
|
|
230
220
|
client = ctx.obj["client"]
|
|
231
221
|
|
|
@@ -251,94 +241,69 @@ def create_coupon(
|
|
|
251
241
|
end_str = end.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
252
242
|
|
|
253
243
|
# Calculate discount values
|
|
254
|
-
if
|
|
244
|
+
if discount_type == "percentage":
|
|
255
245
|
discount_amount = round(current_price * discount / 100, 2)
|
|
256
246
|
discount_display = f"{discount}%"
|
|
257
247
|
else:
|
|
258
248
|
discount_amount = discount
|
|
259
249
|
discount_display = f"${discount}"
|
|
260
250
|
|
|
261
|
-
|
|
251
|
+
new_sale_price = max(0, current_price - discount_amount)
|
|
262
252
|
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
253
|
+
# Build sale price feed data
|
|
254
|
+
feed = {
|
|
255
|
+
"sku": sku,
|
|
256
|
+
"asin": response.get("summaries", [{}])[0].get("asin"),
|
|
257
|
+
"pricing": {
|
|
258
|
+
"original_price": current_price,
|
|
259
|
+
"sale_price": new_sale_price,
|
|
260
|
+
"discount_amount": discount_amount,
|
|
261
|
+
"discount_display": discount_display,
|
|
262
|
+
},
|
|
263
|
+
"schedule": {
|
|
264
|
+
"start_date": start_str,
|
|
265
|
+
"end_date": end_str,
|
|
266
|
+
"duration_days": (end - start).days,
|
|
267
|
+
},
|
|
268
|
+
"feed_data": {
|
|
269
|
+
"messageId": 1,
|
|
269
270
|
"sku": sku,
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
271
|
+
"operationType": "PARTIAL_UPDATE",
|
|
272
|
+
"productType": "PET_TOY",
|
|
273
|
+
"attributes": {
|
|
274
|
+
"list_price": [{"currency": "USD", "value": current_price}],
|
|
275
|
+
"sale_price": [
|
|
276
|
+
{
|
|
277
|
+
"currency": "USD",
|
|
278
|
+
"value": new_sale_price,
|
|
279
|
+
"effective_date": start_str,
|
|
280
|
+
"end_date": end_str,
|
|
281
|
+
}
|
|
282
|
+
],
|
|
280
283
|
},
|
|
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
284
|
},
|
|
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
285
|
}
|
|
316
286
|
|
|
317
|
-
# Remove None values
|
|
318
|
-
if coupon["coupon_specification"]["requirements"]["minimum_purchase"] is None:
|
|
319
|
-
del coupon["coupon_specification"]["requirements"]["minimum_purchase"]
|
|
320
|
-
|
|
321
287
|
# Output
|
|
322
|
-
output_json = json.dumps(
|
|
288
|
+
output_json = json.dumps(feed, indent=2)
|
|
323
289
|
|
|
324
290
|
if output:
|
|
325
291
|
output.write(output_json)
|
|
326
|
-
click.echo(f"✓
|
|
292
|
+
click.echo(f"✓ Sale price data saved to {output.name}")
|
|
327
293
|
|
|
328
294
|
click.echo(output_json)
|
|
329
295
|
|
|
330
296
|
# Summary
|
|
331
297
|
click.echo("\n" + "=" * 50)
|
|
332
|
-
click.echo("SUMMARY")
|
|
298
|
+
click.echo("SALE PRICE SUMMARY")
|
|
333
299
|
click.echo("=" * 50)
|
|
334
300
|
click.echo(f"SKU: {sku}")
|
|
335
301
|
click.echo(f"Discount: {discount_display}")
|
|
336
|
-
click.echo(f"Price: ${current_price} → ${
|
|
337
|
-
click.echo(f"Budget: ${budget}")
|
|
302
|
+
click.echo(f"Price: ${current_price} → ${new_sale_price}")
|
|
338
303
|
click.echo(f"Duration: {start_str} to {end_str}")
|
|
339
|
-
click.echo(
|
|
340
|
-
click.echo("
|
|
341
|
-
click.echo("
|
|
304
|
+
click.echo("\n⚠️ SP-API does not support direct sale price creation.")
|
|
305
|
+
click.echo(" Submit the feed_data above via the Feeds API")
|
|
306
|
+
click.echo(" or update manually in Seller Central.")
|
|
342
307
|
|
|
343
308
|
except Exception as e:
|
|
344
309
|
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
|