ApiLogicServer 15.2.3__py3-none-any.whl → 15.2.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api_logic_server_cli/api_logic_server.py +3 -1
- api_logic_server_cli/prototypes/base/.github/.copilot-instructions.md +114 -52
- api_logic_server_cli/prototypes/base/docs/training/testing.md +95 -9
- api_logic_server_cli/prototypes/base/test/api_logic_server_behave/behave_logic_report.py +19 -6
- api_logic_server_cli/prototypes/basic_demo/.github/.copilot-instructions.md +744 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +17 -1
- api_logic_server_cli/prototypes/basic_demo/readme.md +13 -5
- api_logic_server_cli/prototypes/basic_demo/tutor.md +1436 -0
- api_logic_server_cli/prototypes/manager/.github/.copilot-instructions.md +50 -23
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/.github/.copilot-instructions.md +3 -0
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/customizations/logic/declare_logic.py +17 -1
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/docs/training/testing.md +95 -9
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/iteration/logic/declare_logic.py +17 -1
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/logic/declare_logic.py +38 -1
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/features/order_processing.feature +59 -50
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/features/steps/order_processing_steps.py +395 -248
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/behave.log +66 -62
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Carbon_Neutral_Discount_A.log +51 -41
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Change_Order_Customer.log +29 -0
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Change_Product_in_Item.log +35 -0
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Delete_Item_Reduces_Order.log +39 -19
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Exceed_Credit_Limit_Rejec.log +36 -45
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Good_Order_Placed_via_B2B.log +50 -40
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Item_Quantity_Change.log +33 -0
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Multi-Item_Order_via_B2B_.log +67 -0
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Ship_Order_Excludes_from_.log +24 -14
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Transaction_Processing.log +26 -17
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Unship_Order_Includes_in_.log +24 -14
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/reports/Behave Logic Report.md +361 -146
- api_logic_server_cli/prototypes/manager/system/ApiLogicServer-Internal-Dev/copilot-dev-context.md +270 -2
- {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/METADATA +25 -16
- {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/RECORD +36 -30
- {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/WHEEL +0 -0
- {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/entry_points.txt +0 -0
- {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/top_level.txt +0 -0
|
@@ -1,27 +1,79 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
Order Processing Test Steps - Following ALL Critical Rules from testing.md
|
|
3
|
+
|
|
4
|
+
CRITICAL: Step ordering follows Rule #0.5 (most specific → most general)
|
|
5
|
+
- Multi-item patterns BEFORE single-item patterns
|
|
6
|
+
- Carbon neutral patterns BEFORE general patterns
|
|
4
7
|
"""
|
|
8
|
+
|
|
5
9
|
from behave import *
|
|
6
10
|
import requests
|
|
7
11
|
import test_utils
|
|
8
12
|
import time
|
|
9
13
|
from decimal import Decimal
|
|
14
|
+
import os
|
|
15
|
+
from dotenv import load_dotenv
|
|
16
|
+
from pathlib import Path
|
|
10
17
|
|
|
11
18
|
BASE_URL = 'http://localhost:5656'
|
|
12
19
|
|
|
20
|
+
# Load config to check SECURITY_ENABLED
|
|
21
|
+
config_path = Path(__file__).parent.parent.parent.parent.parent / 'config' / 'default.env'
|
|
22
|
+
load_dotenv(config_path)
|
|
23
|
+
|
|
24
|
+
# Cache for auth token (obtained once per test session)
|
|
25
|
+
_auth_token = None
|
|
26
|
+
|
|
27
|
+
def get_auth_token():
|
|
28
|
+
"""Login and get JWT token if security is enabled"""
|
|
29
|
+
global _auth_token
|
|
30
|
+
|
|
31
|
+
if _auth_token is not None:
|
|
32
|
+
return _auth_token
|
|
33
|
+
|
|
34
|
+
# Login with default admin credentials
|
|
35
|
+
login_url = f'{BASE_URL}/api/auth/login'
|
|
36
|
+
login_data = {
|
|
37
|
+
'username': 'admin',
|
|
38
|
+
'password': 'p'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
response = requests.post(login_url, json=login_data)
|
|
43
|
+
if response.status_code == 200:
|
|
44
|
+
_auth_token = response.json().get('access_token')
|
|
45
|
+
return _auth_token
|
|
46
|
+
else:
|
|
47
|
+
raise Exception(f"Login failed: {response.status_code} - {response.text}")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
raise Exception(f"Failed to obtain auth token: {e}")
|
|
50
|
+
|
|
13
51
|
def get_headers():
|
|
14
|
-
"""
|
|
15
|
-
|
|
52
|
+
"""Get headers including auth token if security is enabled"""
|
|
53
|
+
security_enabled = os.getenv('SECURITY_ENABLED', 'false').lower() not in ['false', 'no']
|
|
54
|
+
|
|
55
|
+
headers = {'Content-Type': 'application/json'}
|
|
56
|
+
|
|
57
|
+
if security_enabled:
|
|
58
|
+
token = get_auth_token()
|
|
59
|
+
if token:
|
|
60
|
+
headers['Authorization'] = f'Bearer {token}'
|
|
61
|
+
|
|
62
|
+
return headers
|
|
16
63
|
|
|
17
64
|
|
|
18
|
-
#
|
|
19
|
-
# GIVEN Steps - Setup
|
|
20
|
-
#
|
|
65
|
+
# ==============================================================================
|
|
66
|
+
# GIVEN Steps - Customer Setup (Rule #0: Always create fresh data)
|
|
67
|
+
# ==============================================================================
|
|
21
68
|
|
|
22
69
|
@given('Customer "{customer_name}" with balance {balance:d} and credit limit {limit:d}')
|
|
23
70
|
def step_impl(context, customer_name, balance, limit):
|
|
24
|
-
"""
|
|
71
|
+
"""
|
|
72
|
+
Phase 1: CREATE Customer with unique timestamp name (Rule #0)
|
|
73
|
+
|
|
74
|
+
CRITICAL: Always create fresh data, never reuse existing customers
|
|
75
|
+
"""
|
|
76
|
+
# Create unique name with timestamp for test repeatability
|
|
25
77
|
unique_name = f"{customer_name} {int(time.time() * 1000)}"
|
|
26
78
|
|
|
27
79
|
post_uri = f'{BASE_URL}/api/Customer/'
|
|
@@ -30,7 +82,7 @@ def step_impl(context, customer_name, balance, limit):
|
|
|
30
82
|
"type": "Customer",
|
|
31
83
|
"attributes": {
|
|
32
84
|
"name": unique_name,
|
|
33
|
-
"balance": balance,
|
|
85
|
+
"balance": balance,
|
|
34
86
|
"credit_limit": limit
|
|
35
87
|
}
|
|
36
88
|
}
|
|
@@ -49,88 +101,210 @@ def step_impl(context, customer_name, balance, limit):
|
|
|
49
101
|
'unique_name': unique_name
|
|
50
102
|
}
|
|
51
103
|
|
|
52
|
-
#
|
|
104
|
+
# Also set default context values for single-customer tests
|
|
53
105
|
context.customer_id = customer_id
|
|
54
106
|
context.customer_name = unique_name
|
|
55
107
|
|
|
56
108
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
109
|
+
# ==============================================================================
|
|
110
|
+
# GIVEN Steps - Order Setup (ORDERED: Multi-item BEFORE Single-item - Rule #0.5)
|
|
111
|
+
# ==============================================================================
|
|
60
112
|
|
|
61
|
-
@given('Order
|
|
113
|
+
@given('Order is created for "{customer_name}" with {qty1:d} {product1} and {qty2:d} {product2}')
|
|
62
114
|
def step_impl(context, customer_name, qty1, product1, qty2, product2):
|
|
63
|
-
"""
|
|
64
|
-
|
|
115
|
+
"""
|
|
116
|
+
Phase 1: CREATE Order with 2 items using CRUD API
|
|
117
|
+
|
|
118
|
+
CRITICAL: This pattern MUST come BEFORE single-item pattern (Rule #0.5)
|
|
119
|
+
"""
|
|
120
|
+
scenario_name = context.scenario.name
|
|
65
121
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
66
122
|
|
|
67
|
-
|
|
123
|
+
# Get customer from map
|
|
124
|
+
customer_info = context.customer_map[customer_name]
|
|
125
|
+
customer_id = customer_info['id']
|
|
68
126
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
# Create Order
|
|
128
|
+
order_uri = f'{BASE_URL}/api/Order/'
|
|
129
|
+
order_data = {
|
|
130
|
+
"data": {
|
|
131
|
+
"type": "Order",
|
|
132
|
+
"attributes": {
|
|
133
|
+
"customer_id": customer_id,
|
|
134
|
+
"notes": f"Test order - {scenario_name}"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
r = requests.post(url=order_uri, json=order_data, headers=get_headers())
|
|
139
|
+
assert r.status_code == 201, f"Failed to create order: {r.text}"
|
|
140
|
+
order_id = int(r.json()['data']['id'])
|
|
141
|
+
context.order_id = order_id
|
|
142
|
+
|
|
143
|
+
# Create Item 1
|
|
144
|
+
product1_id = get_product_id_by_name(product1)
|
|
145
|
+
item1_uri = f'{BASE_URL}/api/Item/'
|
|
146
|
+
item1_data = {
|
|
147
|
+
"data": {
|
|
148
|
+
"type": "Item",
|
|
149
|
+
"attributes": {
|
|
150
|
+
"order_id": order_id,
|
|
151
|
+
"product_id": product1_id,
|
|
152
|
+
"quantity": qty1
|
|
82
153
|
}
|
|
83
154
|
}
|
|
84
155
|
}
|
|
156
|
+
r = requests.post(url=item1_uri, json=item1_data, headers=get_headers())
|
|
157
|
+
assert r.status_code == 201, f"Failed to create item 1: {r.text}"
|
|
158
|
+
item1_id = int(r.json()['data']['id'])
|
|
159
|
+
|
|
160
|
+
# Create Item 2
|
|
161
|
+
product2_id = get_product_id_by_name(product2)
|
|
162
|
+
item2_uri = f'{BASE_URL}/api/Item/'
|
|
163
|
+
item2_data = {
|
|
164
|
+
"data": {
|
|
165
|
+
"type": "Item",
|
|
166
|
+
"attributes": {
|
|
167
|
+
"order_id": order_id,
|
|
168
|
+
"product_id": product2_id,
|
|
169
|
+
"quantity": qty2
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
r = requests.post(url=item2_uri, json=item2_data, headers=get_headers())
|
|
174
|
+
assert r.status_code == 201, f"Failed to create item 2: {r.text}"
|
|
175
|
+
item2_id = int(r.json()['data']['id'])
|
|
85
176
|
|
|
86
|
-
|
|
87
|
-
context.
|
|
177
|
+
# Store both item IDs for later use
|
|
178
|
+
context.item_ids = [item1_id, item2_id]
|
|
179
|
+
context.item_id = item1_id # Default to first item
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@given('Order is created for "{customer_name}" with {quantity:d} {product_name}')
|
|
183
|
+
def step_impl(context, customer_name, quantity, product_name):
|
|
184
|
+
"""
|
|
185
|
+
Phase 1: CREATE Order with single item using CRUD API
|
|
88
186
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
187
|
+
CRITICAL: This pattern comes AFTER multi-item pattern (Rule #0.5)
|
|
188
|
+
"""
|
|
189
|
+
scenario_name = context.scenario.name
|
|
190
|
+
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
191
|
+
|
|
192
|
+
# Get customer from map
|
|
193
|
+
customer_info = context.customer_map[customer_name]
|
|
194
|
+
customer_id = customer_info['id']
|
|
195
|
+
|
|
196
|
+
# Create Order
|
|
197
|
+
order_uri = f'{BASE_URL}/api/Order/'
|
|
198
|
+
order_data = {
|
|
199
|
+
"data": {
|
|
200
|
+
"type": "Order",
|
|
201
|
+
"attributes": {
|
|
202
|
+
"customer_id": customer_id,
|
|
203
|
+
"notes": f"Test order - {scenario_name}"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
r = requests.post(url=order_uri, json=order_data, headers=get_headers())
|
|
208
|
+
assert r.status_code == 201, f"Failed to create order: {r.text}"
|
|
209
|
+
order_id = int(r.json()['data']['id'])
|
|
210
|
+
context.order_id = order_id
|
|
211
|
+
|
|
212
|
+
# Create Item
|
|
213
|
+
product_id = get_product_id_by_name(product_name)
|
|
214
|
+
item_uri = f'{BASE_URL}/api/Item/'
|
|
215
|
+
item_data = {
|
|
216
|
+
"data": {
|
|
217
|
+
"type": "Item",
|
|
218
|
+
"attributes": {
|
|
219
|
+
"order_id": order_id,
|
|
220
|
+
"product_id": product_id,
|
|
221
|
+
"quantity": quantity
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
r = requests.post(url=item_uri, json=item_data, headers=get_headers())
|
|
226
|
+
assert r.status_code == 201, f"Failed to create item: {r.text}"
|
|
227
|
+
item_id = int(r.json()['data']['id'])
|
|
228
|
+
context.item_id = item_id
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@given('Shipped order is created for "{customer_name}" with {quantity:d} {product_name}')
|
|
232
|
+
def step_impl(context, customer_name, quantity, product_name):
|
|
233
|
+
"""
|
|
234
|
+
Phase 1: CREATE Order and immediately ship it
|
|
235
|
+
|
|
236
|
+
Tests WHERE clause exclusion from balance
|
|
237
|
+
"""
|
|
238
|
+
scenario_name = context.scenario.name
|
|
239
|
+
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
240
|
+
|
|
241
|
+
# Get customer from map
|
|
242
|
+
customer_info = context.customer_map[customer_name]
|
|
243
|
+
customer_id = customer_info['id']
|
|
244
|
+
|
|
245
|
+
# Create Order with date_shipped set
|
|
246
|
+
order_uri = f'{BASE_URL}/api/Order/'
|
|
247
|
+
order_data = {
|
|
248
|
+
"data": {
|
|
249
|
+
"type": "Order",
|
|
250
|
+
"attributes": {
|
|
251
|
+
"customer_id": customer_id,
|
|
252
|
+
"notes": f"Test shipped order - {scenario_name}",
|
|
253
|
+
"date_shipped": "2025-10-22"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
r = requests.post(url=order_uri, json=order_data, headers=get_headers())
|
|
258
|
+
assert r.status_code == 201, f"Failed to create order: {r.text}"
|
|
259
|
+
order_id = int(r.json()['data']['id'])
|
|
260
|
+
context.order_id = order_id
|
|
261
|
+
|
|
262
|
+
# Create Item
|
|
263
|
+
product_id = get_product_id_by_name(product_name)
|
|
264
|
+
item_uri = f'{BASE_URL}/api/Item/'
|
|
265
|
+
item_data = {
|
|
266
|
+
"data": {
|
|
267
|
+
"type": "Item",
|
|
268
|
+
"attributes": {
|
|
269
|
+
"order_id": order_id,
|
|
270
|
+
"product_id": product_id,
|
|
271
|
+
"quantity": quantity
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
r = requests.post(url=item_uri, json=item_data, headers=get_headers())
|
|
276
|
+
assert r.status_code == 201, f"Failed to create item: {r.text}"
|
|
277
|
+
item_id = int(r.json()['data']['id'])
|
|
278
|
+
context.item_id = item_id
|
|
279
|
+
|
|
114
280
|
|
|
281
|
+
# ==============================================================================
|
|
282
|
+
# WHEN Steps - Order Creation (ORDERED: Specific BEFORE General - Rule #0.5)
|
|
283
|
+
# ==============================================================================
|
|
115
284
|
|
|
116
|
-
@
|
|
285
|
+
@when('B2B order placed for "{customer_name}" with {quantity:d} carbon neutral {product_name}')
|
|
117
286
|
def step_impl(context, customer_name, quantity, product_name):
|
|
118
|
-
"""
|
|
119
|
-
|
|
287
|
+
"""
|
|
288
|
+
Phase 2: CREATE using OrderB2B API - Carbon neutral product
|
|
289
|
+
|
|
290
|
+
CRITICAL: This pattern MUST come BEFORE general pattern (Rule #0.5)
|
|
291
|
+
Tests carbon neutral discount logic (10% off when qty >= 10)
|
|
292
|
+
"""
|
|
293
|
+
scenario_name = context.scenario.name
|
|
120
294
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
121
295
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
296
|
+
# Get customer unique name
|
|
297
|
+
customer_info = context.customer_map[customer_name]
|
|
298
|
+
account_name = customer_info['unique_name']
|
|
125
299
|
|
|
126
300
|
add_order_uri = f'{BASE_URL}/api/ServicesEndPoint/OrderB2B'
|
|
127
301
|
add_order_args = {
|
|
128
302
|
"meta": {
|
|
129
|
-
"method": "OrderB2B", # CRITICAL: Required for custom
|
|
303
|
+
"method": "OrderB2B", # CRITICAL: Required for custom APIs
|
|
130
304
|
"args": {
|
|
131
305
|
"order": {
|
|
132
|
-
"Account":
|
|
133
|
-
"Notes": "
|
|
306
|
+
"Account": account_name,
|
|
307
|
+
"Notes": f"Carbon neutral order - {scenario_name}",
|
|
134
308
|
"Items": [
|
|
135
309
|
{
|
|
136
310
|
"Name": product_name,
|
|
@@ -146,81 +320,53 @@ def step_impl(context, customer_name, quantity, product_name):
|
|
|
146
320
|
context.order_created = (r.status_code == 200)
|
|
147
321
|
|
|
148
322
|
if context.order_created:
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
r_orders = requests.get(url=orders_uri, headers=get_headers())
|
|
156
|
-
orders = r_orders.json()['data']
|
|
157
|
-
|
|
158
|
-
if orders:
|
|
159
|
-
latest_order = orders[-1] # Get most recent
|
|
160
|
-
context.order_id = int(latest_order['id'])
|
|
323
|
+
# Find the created order (most recent for this customer)
|
|
324
|
+
orders_uri = f'{BASE_URL}/api/Order/?filter[customer_id]={customer_info["id"]}&sort=-id'
|
|
325
|
+
r = requests.get(url=orders_uri, headers=get_headers())
|
|
326
|
+
if r.json()['data']:
|
|
327
|
+
order_data = r.json()['data'][0]
|
|
328
|
+
context.order_id = int(order_data['id'])
|
|
161
329
|
|
|
162
|
-
# Get
|
|
330
|
+
# Get the item
|
|
163
331
|
items_uri = f'{BASE_URL}/api/Item/?filter[order_id]={context.order_id}'
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
context.item_id = int(items[0]['id'])
|
|
332
|
+
r = requests.get(url=items_uri, headers=get_headers())
|
|
333
|
+
if r.json()['data']:
|
|
334
|
+
context.item_id = int(r.json()['data'][0]['id'])
|
|
168
335
|
else:
|
|
169
336
|
context.order_id = None
|
|
170
337
|
context.item_id = None
|
|
171
338
|
|
|
172
339
|
|
|
173
|
-
@
|
|
174
|
-
def step_impl(context, customer_name,
|
|
175
|
-
"""Create and ship order"""
|
|
176
|
-
# First create order
|
|
177
|
-
context.execute_steps(f'''
|
|
178
|
-
Given Order exists for "{customer_name}" with {quantity} {product_name}
|
|
179
|
-
''')
|
|
180
|
-
|
|
181
|
-
# Then ship it
|
|
182
|
-
import datetime
|
|
183
|
-
patch_uri = f'{BASE_URL}/api/Order/{context.order_id}/'
|
|
184
|
-
patch_data = {
|
|
185
|
-
"data": {
|
|
186
|
-
"type": "Order",
|
|
187
|
-
"id": context.order_id,
|
|
188
|
-
"attributes": {
|
|
189
|
-
"date_shipped": str(datetime.date.today())
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
r = requests.patch(url=patch_uri, json=patch_data, headers=get_headers())
|
|
195
|
-
assert r.status_code == 200, f"Failed to ship order: {r.text}"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
# ============================================================================
|
|
199
|
-
# WHEN Steps - Actions (Rule #0.5: Specific patterns BEFORE general ones!)
|
|
200
|
-
# ============================================================================
|
|
201
|
-
|
|
202
|
-
@when('B2B order placed for "{customer_name}" with {quantity:d} carbon neutral {product_name}')
|
|
203
|
-
def step_impl(context, customer_name, quantity, product_name):
|
|
340
|
+
@when('B2B order placed for "{customer_name}" with {qty1:d} {product1} and {qty2:d} {product2}')
|
|
341
|
+
def step_impl(context, customer_name, qty1, product1, qty2, product2):
|
|
204
342
|
"""
|
|
205
|
-
Phase 2: CREATE using OrderB2B API -
|
|
343
|
+
Phase 2: CREATE using OrderB2B API - Multi-item order
|
|
344
|
+
|
|
345
|
+
CRITICAL: This pattern MUST come BEFORE single-item pattern (Rule #0.5)
|
|
206
346
|
"""
|
|
207
347
|
scenario_name = context.scenario.name
|
|
208
348
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
209
349
|
|
|
210
|
-
|
|
350
|
+
# Get customer unique name
|
|
351
|
+
customer_info = context.customer_map[customer_name]
|
|
352
|
+
account_name = customer_info['unique_name']
|
|
211
353
|
|
|
212
354
|
add_order_uri = f'{BASE_URL}/api/ServicesEndPoint/OrderB2B'
|
|
213
355
|
add_order_args = {
|
|
214
356
|
"meta": {
|
|
215
|
-
"method": "OrderB2B",
|
|
357
|
+
"method": "OrderB2B", # CRITICAL: Required for custom APIs
|
|
216
358
|
"args": {
|
|
217
359
|
"order": {
|
|
218
|
-
"Account":
|
|
219
|
-
"Notes": "
|
|
360
|
+
"Account": account_name,
|
|
361
|
+
"Notes": f"Multi-item order - {scenario_name}",
|
|
220
362
|
"Items": [
|
|
221
363
|
{
|
|
222
|
-
"Name":
|
|
223
|
-
"QuantityOrdered":
|
|
364
|
+
"Name": product1,
|
|
365
|
+
"QuantityOrdered": qty1
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"Name": product2,
|
|
369
|
+
"QuantityOrdered": qty2
|
|
224
370
|
}
|
|
225
371
|
]
|
|
226
372
|
}
|
|
@@ -232,21 +378,19 @@ def step_impl(context, customer_name, quantity, product_name):
|
|
|
232
378
|
context.order_created = (r.status_code == 200)
|
|
233
379
|
|
|
234
380
|
if context.order_created:
|
|
235
|
-
#
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if orders:
|
|
242
|
-
latest_order = orders[-1]
|
|
243
|
-
context.order_id = int(latest_order['id'])
|
|
381
|
+
# Find the created order (most recent for this customer)
|
|
382
|
+
orders_uri = f'{BASE_URL}/api/Order/?filter[customer_id]={customer_info["id"]}&sort=-id'
|
|
383
|
+
r = requests.get(url=orders_uri, headers=get_headers())
|
|
384
|
+
if r.json()['data']:
|
|
385
|
+
order_data = r.json()['data'][0]
|
|
386
|
+
context.order_id = int(order_data['id'])
|
|
244
387
|
|
|
388
|
+
# Get the items
|
|
245
389
|
items_uri = f'{BASE_URL}/api/Item/?filter[order_id]={context.order_id}'
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
context.item_id =
|
|
390
|
+
r = requests.get(url=items_uri, headers=get_headers())
|
|
391
|
+
if r.json()['data']:
|
|
392
|
+
context.item_ids = [int(item['id']) for item in r.json()['data']]
|
|
393
|
+
context.item_id = context.item_ids[0] # Default to first
|
|
250
394
|
else:
|
|
251
395
|
context.order_id = None
|
|
252
396
|
context.item_id = None
|
|
@@ -255,21 +399,25 @@ def step_impl(context, customer_name, quantity, product_name):
|
|
|
255
399
|
@when('B2B order placed for "{customer_name}" with {quantity:d} {product_name}')
|
|
256
400
|
def step_impl(context, customer_name, quantity, product_name):
|
|
257
401
|
"""
|
|
258
|
-
Phase 2: CREATE using OrderB2B API -
|
|
402
|
+
Phase 2: CREATE using OrderB2B API - Single item order
|
|
403
|
+
|
|
404
|
+
CRITICAL: This pattern comes AFTER multi-item and carbon neutral (Rule #0.5)
|
|
259
405
|
"""
|
|
260
406
|
scenario_name = context.scenario.name
|
|
261
407
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
262
408
|
|
|
263
|
-
|
|
409
|
+
# Get customer unique name
|
|
410
|
+
customer_info = context.customer_map[customer_name]
|
|
411
|
+
account_name = customer_info['unique_name']
|
|
264
412
|
|
|
265
413
|
add_order_uri = f'{BASE_URL}/api/ServicesEndPoint/OrderB2B'
|
|
266
414
|
add_order_args = {
|
|
267
415
|
"meta": {
|
|
268
|
-
"method": "OrderB2B",
|
|
416
|
+
"method": "OrderB2B", # CRITICAL: Required for custom APIs
|
|
269
417
|
"args": {
|
|
270
418
|
"order": {
|
|
271
|
-
"Account":
|
|
272
|
-
"Notes": "Test order",
|
|
419
|
+
"Account": account_name,
|
|
420
|
+
"Notes": f"Test order - {scenario_name}",
|
|
273
421
|
"Items": [
|
|
274
422
|
{
|
|
275
423
|
"Name": product_name,
|
|
@@ -285,30 +433,33 @@ def step_impl(context, customer_name, quantity, product_name):
|
|
|
285
433
|
context.order_created = (r.status_code == 200)
|
|
286
434
|
|
|
287
435
|
if context.order_created:
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if orders:
|
|
295
|
-
latest_order = orders[-1]
|
|
296
|
-
context.order_id = int(latest_order['id'])
|
|
436
|
+
# Find the created order (most recent for this customer)
|
|
437
|
+
orders_uri = f'{BASE_URL}/api/Order/?filter[customer_id]={customer_info["id"]}&sort=-id'
|
|
438
|
+
r = requests.get(url=orders_uri, headers=get_headers())
|
|
439
|
+
if r.json()['data']:
|
|
440
|
+
order_data = r.json()['data'][0]
|
|
441
|
+
context.order_id = int(order_data['id'])
|
|
297
442
|
|
|
443
|
+
# Get the item
|
|
298
444
|
items_uri = f'{BASE_URL}/api/Item/?filter[order_id]={context.order_id}'
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
context.item_id = int(items[0]['id'])
|
|
445
|
+
r = requests.get(url=items_uri, headers=get_headers())
|
|
446
|
+
if r.json()['data']:
|
|
447
|
+
context.item_id = int(r.json()['data'][0]['id'])
|
|
303
448
|
else:
|
|
304
449
|
context.order_id = None
|
|
305
450
|
context.item_id = None
|
|
306
451
|
|
|
307
452
|
|
|
453
|
+
# ==============================================================================
|
|
454
|
+
# WHEN Steps - Updates and Deletes (Phase 1 - Granular Testing)
|
|
455
|
+
# ==============================================================================
|
|
456
|
+
|
|
308
457
|
@when('Item quantity changed to {qty:d}')
|
|
309
458
|
def step_impl(context, qty):
|
|
310
459
|
"""
|
|
311
|
-
Phase 1: UPDATE using CRUD
|
|
460
|
+
Phase 1: UPDATE using CRUD API
|
|
461
|
+
|
|
462
|
+
Tests quantity change triggers amount recalculation
|
|
312
463
|
"""
|
|
313
464
|
scenario_name = context.scenario.name
|
|
314
465
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
@@ -325,75 +476,89 @@ def step_impl(context, qty):
|
|
|
325
476
|
}
|
|
326
477
|
|
|
327
478
|
r = requests.patch(url=patch_uri, json=patch_data, headers=get_headers())
|
|
328
|
-
assert r.status_code == 200, f"Failed to update
|
|
479
|
+
assert r.status_code == 200, f"Failed to update item: {r.text}"
|
|
329
480
|
|
|
330
481
|
|
|
331
|
-
@when('
|
|
332
|
-
def step_impl(context,
|
|
482
|
+
@when('Item product changed to "{product_name}"')
|
|
483
|
+
def step_impl(context, product_name):
|
|
333
484
|
"""
|
|
334
|
-
Phase 1: UPDATE
|
|
485
|
+
Phase 1: UPDATE using CRUD API
|
|
486
|
+
|
|
487
|
+
Tests FK change triggers unit_price copy from new product
|
|
335
488
|
"""
|
|
336
489
|
scenario_name = context.scenario.name
|
|
337
490
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
338
491
|
|
|
339
|
-
|
|
492
|
+
product_id = get_product_id_by_name(product_name)
|
|
340
493
|
|
|
341
|
-
patch_uri = f'{BASE_URL}/api/
|
|
494
|
+
patch_uri = f'{BASE_URL}/api/Item/{context.item_id}/'
|
|
342
495
|
patch_data = {
|
|
343
496
|
"data": {
|
|
344
|
-
"type": "
|
|
345
|
-
"id": context.
|
|
497
|
+
"type": "Item",
|
|
498
|
+
"id": context.item_id,
|
|
346
499
|
"attributes": {
|
|
347
|
-
"
|
|
500
|
+
"product_id": product_id # Direct FK (Rule #2)
|
|
348
501
|
}
|
|
349
502
|
}
|
|
350
503
|
}
|
|
351
504
|
|
|
352
505
|
r = requests.patch(url=patch_uri, json=patch_data, headers=get_headers())
|
|
353
|
-
assert r.status_code == 200, f"Failed to
|
|
506
|
+
assert r.status_code == 200, f"Failed to update item: {r.text}"
|
|
354
507
|
|
|
355
508
|
|
|
356
|
-
@when('First item deleted')
|
|
509
|
+
@when('First item is deleted')
|
|
357
510
|
def step_impl(context):
|
|
358
511
|
"""
|
|
359
|
-
Phase 1: DELETE
|
|
512
|
+
Phase 1: DELETE using CRUD API
|
|
513
|
+
|
|
514
|
+
Tests deletion triggers aggregate recalculation
|
|
360
515
|
"""
|
|
361
516
|
scenario_name = context.scenario.name
|
|
362
517
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
363
518
|
|
|
364
|
-
|
|
519
|
+
# Delete first item from multi-item order
|
|
520
|
+
item_id = context.item_ids[0]
|
|
521
|
+
|
|
522
|
+
delete_uri = f'{BASE_URL}/api/Item/{item_id}/'
|
|
365
523
|
r = requests.delete(url=delete_uri, headers=get_headers())
|
|
366
524
|
assert r.status_code == 204, f"Failed to delete item: {r.text}"
|
|
367
525
|
|
|
368
526
|
|
|
369
|
-
@when('Order
|
|
370
|
-
def step_impl(context):
|
|
527
|
+
@when('Order customer changed to "{new_customer_name}"')
|
|
528
|
+
def step_impl(context, new_customer_name):
|
|
371
529
|
"""
|
|
372
|
-
Phase 1: UPDATE
|
|
530
|
+
Phase 1: UPDATE using CRUD API
|
|
531
|
+
|
|
532
|
+
Tests FK change adjusts BOTH old and new customer balances
|
|
373
533
|
"""
|
|
374
534
|
scenario_name = context.scenario.name
|
|
375
535
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
376
536
|
|
|
377
|
-
|
|
537
|
+
# Get new customer ID
|
|
538
|
+
new_customer_info = context.customer_map[new_customer_name]
|
|
539
|
+
new_customer_id = new_customer_info['id']
|
|
540
|
+
|
|
378
541
|
patch_uri = f'{BASE_URL}/api/Order/{context.order_id}/'
|
|
379
542
|
patch_data = {
|
|
380
543
|
"data": {
|
|
381
544
|
"type": "Order",
|
|
382
545
|
"id": context.order_id,
|
|
383
546
|
"attributes": {
|
|
384
|
-
"
|
|
547
|
+
"customer_id": new_customer_id # Direct FK (Rule #2)
|
|
385
548
|
}
|
|
386
549
|
}
|
|
387
550
|
}
|
|
388
551
|
|
|
389
552
|
r = requests.patch(url=patch_uri, json=patch_data, headers=get_headers())
|
|
390
|
-
assert r.status_code == 200, f"Failed to
|
|
553
|
+
assert r.status_code == 200, f"Failed to update order: {r.text}"
|
|
391
554
|
|
|
392
555
|
|
|
393
|
-
@when('Order is
|
|
556
|
+
@when('Order is shipped')
|
|
394
557
|
def step_impl(context):
|
|
395
558
|
"""
|
|
396
|
-
Phase 1: UPDATE
|
|
559
|
+
Phase 1: UPDATE using CRUD API
|
|
560
|
+
|
|
561
|
+
Tests WHERE clause exclusion (shipped orders don't count in balance)
|
|
397
562
|
"""
|
|
398
563
|
scenario_name = context.scenario.name
|
|
399
564
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
@@ -404,150 +569,132 @@ def step_impl(context):
|
|
|
404
569
|
"type": "Order",
|
|
405
570
|
"id": context.order_id,
|
|
406
571
|
"attributes": {
|
|
407
|
-
"date_shipped":
|
|
572
|
+
"date_shipped": "2025-10-22"
|
|
408
573
|
}
|
|
409
574
|
}
|
|
410
575
|
}
|
|
411
576
|
|
|
412
577
|
r = requests.patch(url=patch_uri, json=patch_data, headers=get_headers())
|
|
413
|
-
assert r.status_code == 200, f"Failed to
|
|
578
|
+
assert r.status_code == 200, f"Failed to ship order: {r.text}"
|
|
414
579
|
|
|
415
580
|
|
|
416
|
-
@when('
|
|
417
|
-
def step_impl(context
|
|
581
|
+
@when('Order is unshipped')
|
|
582
|
+
def step_impl(context):
|
|
418
583
|
"""
|
|
419
|
-
Phase 1: UPDATE
|
|
584
|
+
Phase 1: UPDATE using CRUD API
|
|
585
|
+
|
|
586
|
+
Tests WHERE clause inclusion (unshipped orders count in balance again)
|
|
420
587
|
"""
|
|
421
588
|
scenario_name = context.scenario.name
|
|
422
589
|
test_utils.prt(f'\n{scenario_name}\n', scenario_name)
|
|
423
590
|
|
|
424
|
-
|
|
425
|
-
products_uri = f'{BASE_URL}/api/Product/?filter[name]={new_product_name}'
|
|
426
|
-
r_product = requests.get(url=products_uri, headers=get_headers())
|
|
427
|
-
products = r_product.json()['data']
|
|
428
|
-
assert products, f"Product {new_product_name} not found"
|
|
429
|
-
|
|
430
|
-
new_product_id = int(products[0]['id'])
|
|
431
|
-
|
|
432
|
-
patch_uri = f'{BASE_URL}/api/Item/{context.item_id}/'
|
|
591
|
+
patch_uri = f'{BASE_URL}/api/Order/{context.order_id}/'
|
|
433
592
|
patch_data = {
|
|
434
593
|
"data": {
|
|
435
|
-
"type": "
|
|
436
|
-
"id": context.
|
|
594
|
+
"type": "Order",
|
|
595
|
+
"id": context.order_id,
|
|
437
596
|
"attributes": {
|
|
438
|
-
"
|
|
597
|
+
"date_shipped": None
|
|
439
598
|
}
|
|
440
599
|
}
|
|
441
600
|
}
|
|
442
601
|
|
|
443
602
|
r = requests.patch(url=patch_uri, json=patch_data, headers=get_headers())
|
|
444
|
-
assert r.status_code == 200, f"Failed to
|
|
603
|
+
assert r.status_code == 200, f"Failed to unship order: {r.text}"
|
|
445
604
|
|
|
446
605
|
|
|
447
|
-
#
|
|
606
|
+
# ==============================================================================
|
|
448
607
|
# THEN Steps - Assertions
|
|
449
|
-
#
|
|
608
|
+
# ==============================================================================
|
|
450
609
|
|
|
451
610
|
@then('Customer balance should be {expected:d}')
|
|
452
611
|
def step_impl(context, expected):
|
|
453
|
-
"""Verify customer balance (
|
|
454
|
-
scenario_name = 'Verify Customer Balance'
|
|
455
|
-
test_utils.prt(f'Checking customer balance = {expected}', scenario_name)
|
|
456
|
-
|
|
612
|
+
"""Verify customer balance (default customer)"""
|
|
457
613
|
customer_uri = f'{BASE_URL}/api/Customer/{context.customer_id}/'
|
|
458
614
|
r = requests.get(url=customer_uri, headers=get_headers())
|
|
459
|
-
assert r.status_code == 200, f"Failed to get customer: {r.text}"
|
|
460
|
-
|
|
461
615
|
actual = float(r.json()['data']['attributes']['balance'] or 0)
|
|
462
|
-
assert abs(actual - expected) < 0.01,
|
|
463
|
-
f"Expected balance {expected}, got {actual}"
|
|
616
|
+
assert abs(actual - expected) < 0.01, f"Expected balance {expected}, got {actual}"
|
|
464
617
|
|
|
465
618
|
|
|
466
619
|
@then('Customer "{customer_name}" balance should be {expected:d}')
|
|
467
620
|
def step_impl(context, customer_name, expected):
|
|
468
|
-
"""Verify specific customer balance (Rule #8
|
|
469
|
-
|
|
470
|
-
test_utils.prt(f'Checking {customer_name} balance = {expected}', scenario_name)
|
|
471
|
-
|
|
472
|
-
customer_info = context.customer_map.get(customer_name)
|
|
473
|
-
|
|
621
|
+
"""Verify specific customer balance (for multi-customer tests - Rule #8)"""
|
|
622
|
+
customer_info = context.customer_map[customer_name]
|
|
474
623
|
customer_uri = f'{BASE_URL}/api/Customer/{customer_info["id"]}/'
|
|
475
624
|
r = requests.get(url=customer_uri, headers=get_headers())
|
|
476
|
-
assert r.status_code == 200, f"Failed to get customer: {r.text}"
|
|
477
|
-
|
|
478
625
|
actual = float(r.json()['data']['attributes']['balance'] or 0)
|
|
479
|
-
assert abs(actual - expected) < 0.01,
|
|
480
|
-
f"Expected {customer_name} balance {expected}, got {actual}"
|
|
626
|
+
assert abs(actual - expected) < 0.01, f"Expected {customer_name} balance {expected}, got {actual}"
|
|
481
627
|
|
|
482
628
|
|
|
483
629
|
@then('Order amount_total should be {expected:d}')
|
|
484
630
|
def step_impl(context, expected):
|
|
485
|
-
"""Verify order
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if not hasattr(context, 'order_id') or context.order_id is None:
|
|
490
|
-
assert not context.order_created, "Order should have failed"
|
|
631
|
+
"""Verify order amount_total"""
|
|
632
|
+
if context.order_id is None:
|
|
633
|
+
assert not context.order_created, "Order should not have been created"
|
|
491
634
|
return
|
|
492
|
-
|
|
635
|
+
|
|
493
636
|
order_uri = f'{BASE_URL}/api/Order/{context.order_id}/'
|
|
494
637
|
r = requests.get(url=order_uri, headers=get_headers())
|
|
495
|
-
assert r.status_code == 200, f"Failed to get order: {r.text}"
|
|
496
|
-
|
|
497
638
|
actual = float(r.json()['data']['attributes']['amount_total'] or 0)
|
|
498
|
-
assert abs(actual - expected) < 0.01,
|
|
499
|
-
f"Expected amount_total {expected}, got {actual}"
|
|
639
|
+
assert abs(actual - expected) < 0.01, f"Expected amount_total {expected}, got {actual}"
|
|
500
640
|
|
|
501
641
|
|
|
502
642
|
@then('Item amount should be {expected:d}')
|
|
503
643
|
def step_impl(context, expected):
|
|
504
644
|
"""Verify item amount"""
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if not hasattr(context, 'item_id') or context.item_id is None:
|
|
509
|
-
assert not context.order_created, "Order should have failed"
|
|
645
|
+
if context.item_id is None:
|
|
646
|
+
assert not context.order_created, "Item should not have been created"
|
|
510
647
|
return
|
|
511
|
-
|
|
648
|
+
|
|
512
649
|
item_uri = f'{BASE_URL}/api/Item/{context.item_id}/'
|
|
513
650
|
r = requests.get(url=item_uri, headers=get_headers())
|
|
514
|
-
assert r.status_code == 200, f"Failed to get item: {r.text}"
|
|
515
|
-
|
|
516
651
|
actual = float(r.json()['data']['attributes']['amount'] or 0)
|
|
517
|
-
assert abs(actual - expected) < 0.01,
|
|
518
|
-
f"Expected amount {expected}, got {actual}"
|
|
652
|
+
assert abs(actual - expected) < 0.01, f"Expected amount {expected}, got {actual}"
|
|
519
653
|
|
|
520
654
|
|
|
521
655
|
@then('Item unit_price should be {expected:d}')
|
|
522
656
|
def step_impl(context, expected):
|
|
523
|
-
"""Verify item
|
|
524
|
-
scenario_name = 'Verify Unit Price Copy'
|
|
525
|
-
test_utils.prt(f'Checking item unit_price = {expected}', scenario_name)
|
|
526
|
-
|
|
657
|
+
"""Verify item unit_price (tests copy rule)"""
|
|
527
658
|
item_uri = f'{BASE_URL}/api/Item/{context.item_id}/'
|
|
528
659
|
r = requests.get(url=item_uri, headers=get_headers())
|
|
529
|
-
assert r.status_code == 200, f"Failed to get item: {r.text}"
|
|
530
|
-
|
|
531
660
|
actual = float(r.json()['data']['attributes']['unit_price'] or 0)
|
|
532
|
-
assert abs(actual - expected) < 0.01,
|
|
533
|
-
f"Expected unit_price {expected}, got {actual}"
|
|
661
|
+
assert abs(actual - expected) < 0.01, f"Expected unit_price {expected}, got {actual}"
|
|
534
662
|
|
|
535
663
|
|
|
536
664
|
@then('Order created successfully')
|
|
537
665
|
def step_impl(context):
|
|
538
|
-
"""Verify order
|
|
666
|
+
"""Verify order was created"""
|
|
539
667
|
assert context.order_created, "Order creation failed"
|
|
540
668
|
assert context.order_id is not None, "Order ID not set"
|
|
541
669
|
|
|
542
670
|
|
|
543
|
-
@then('Order
|
|
671
|
+
@then('Order should be rejected')
|
|
544
672
|
def step_impl(context):
|
|
545
|
-
"""Verify order
|
|
546
|
-
assert not context.order_created, "Order should have been rejected"
|
|
673
|
+
"""Verify order was rejected (constraint violation)"""
|
|
674
|
+
assert not context.order_created, "Order should have been rejected but was created"
|
|
547
675
|
|
|
548
676
|
|
|
549
677
|
@then('Error message should contain "{text}"')
|
|
550
678
|
def step_impl(context, text):
|
|
551
|
-
"""Verify error message
|
|
552
|
-
#
|
|
553
|
-
|
|
679
|
+
"""Verify error message contains expected text"""
|
|
680
|
+
# For constraint violations, we expect the order not to be created
|
|
681
|
+
assert not context.order_created, f"Expected constraint violation, but order was created"
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# ==============================================================================
|
|
685
|
+
# Helper Functions
|
|
686
|
+
# ==============================================================================
|
|
687
|
+
|
|
688
|
+
def get_product_id_by_name(product_name):
|
|
689
|
+
"""
|
|
690
|
+
Lookup product ID by name
|
|
691
|
+
|
|
692
|
+
Uses exact match on product name from database
|
|
693
|
+
"""
|
|
694
|
+
products_uri = f'{BASE_URL}/api/Product/?filter[name]={product_name}'
|
|
695
|
+
r = requests.get(url=products_uri, headers=get_headers())
|
|
696
|
+
|
|
697
|
+
if not r.json()['data']:
|
|
698
|
+
raise ValueError(f"Product '{product_name}' not found in database")
|
|
699
|
+
|
|
700
|
+
return int(r.json()['data'][0]['id'])
|