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.
Files changed (36) hide show
  1. api_logic_server_cli/api_logic_server.py +3 -1
  2. api_logic_server_cli/prototypes/base/.github/.copilot-instructions.md +114 -52
  3. api_logic_server_cli/prototypes/base/docs/training/testing.md +95 -9
  4. api_logic_server_cli/prototypes/base/test/api_logic_server_behave/behave_logic_report.py +19 -6
  5. api_logic_server_cli/prototypes/basic_demo/.github/.copilot-instructions.md +744 -0
  6. api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +17 -1
  7. api_logic_server_cli/prototypes/basic_demo/readme.md +13 -5
  8. api_logic_server_cli/prototypes/basic_demo/tutor.md +1436 -0
  9. api_logic_server_cli/prototypes/manager/.github/.copilot-instructions.md +50 -23
  10. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/.github/.copilot-instructions.md +3 -0
  11. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/customizations/logic/declare_logic.py +17 -1
  12. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/docs/training/testing.md +95 -9
  13. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/iteration/logic/declare_logic.py +17 -1
  14. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/logic/declare_logic.py +38 -1
  15. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/features/order_processing.feature +59 -50
  16. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/features/steps/order_processing_steps.py +395 -248
  17. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/behave.log +66 -62
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/logs/scenario_logic_logs/Transaction_Processing.log +26 -17
  28. 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
  29. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/reports/Behave Logic Report.md +361 -146
  30. api_logic_server_cli/prototypes/manager/system/ApiLogicServer-Internal-Dev/copilot-dev-context.md +270 -2
  31. {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/METADATA +25 -16
  32. {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/RECORD +36 -30
  33. {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/WHEEL +0 -0
  34. {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/entry_points.txt +0 -0
  35. {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/licenses/LICENSE +0 -0
  36. {apilogicserver-15.2.3.dist-info → apilogicserver-15.2.10.dist-info}/top_level.txt +0 -0
@@ -1,27 +1,79 @@
1
1
  """
2
- Step implementations for Order Processing tests
3
- Following Phase 2 (Custom API) + Phase 1 (CRUD) patterns
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
- """Return auth headers - empty since SECURITY_ENABLED=False"""
15
- return {}
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 test data (Rule #0: Always create fresh with timestamps)
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
- """Create fresh customer with timestamp for uniqueness (Rule #0)"""
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, # Will be replaced by sum of orders
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
- # Set primary customer for single-customer scenarios
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
- # Order Setup Steps - SPECIFIC patterns MUST come BEFORE general (Rule #0.5)
59
- # ============================================================================
109
+ # ==============================================================================
110
+ # GIVEN Steps - Order Setup (ORDERED: Multi-item BEFORE Single-item - Rule #0.5)
111
+ # ==============================================================================
60
112
 
61
- @given('Order exists for "{customer_name}" with {qty1:d} {product1:S} and {qty2:d} {product2:S}')
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
- """Create order with multiple items (Phase 2)"""
64
- scenario_name = f'Setup Order - {qty1} {product1} + {qty2} {product2}'
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
- customer_info = context.customer_map.get(customer_name)
123
+ # Get customer from map
124
+ customer_info = context.customer_map[customer_name]
125
+ customer_id = customer_info['id']
68
126
 
69
- add_order_uri = f'{BASE_URL}/api/ServicesEndPoint/OrderB2B'
70
- add_order_args = {
71
- "meta": {
72
- "method": "OrderB2B",
73
- "args": {
74
- "order": {
75
- "Account": customer_info['unique_name'],
76
- "Notes": "Multi-item test order",
77
- "Items": [
78
- {"Name": product1, "QuantityOrdered": qty1},
79
- {"Name": product2, "QuantityOrdered": qty2}
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
- r = requests.post(url=add_order_uri, json=add_order_args, headers=get_headers())
87
- context.order_created = (r.status_code == 200)
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
- if context.order_created:
90
- # Get order and items
91
- customer_id = customer_info['id']
92
- orders_uri = f'{BASE_URL}/api/Order/?filter[customer_id]={customer_id}'
93
- r_orders = requests.get(url=orders_uri, headers=get_headers())
94
- orders = r_orders.json()['data']
95
-
96
- if orders:
97
- latest_order = orders[-1]
98
- context.order_id = int(latest_order['id'])
99
-
100
- # Get items
101
- items_uri = f'{BASE_URL}/api/Item/?filter[order_id]={context.order_id}'
102
- r_items = requests.get(url=items_uri, headers=get_headers())
103
- items = r_items.json()['data']
104
- if items:
105
- context.item_ids = [int(item['id']) for item in items]
106
- context.item_id = context.item_ids[0] # First item
107
- else:
108
- context.item_id = None
109
- context.item_ids = []
110
- else:
111
- context.order_id = None
112
- context.item_id = None
113
- context.item_ids = []
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
- @given('Order exists for "{customer_name}" with {quantity:d} {product_name}')
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
- """Create order using OrderB2B API (Phase 2) - GENERAL pattern"""
119
- scenario_name = f'Setup Order - {quantity} {product_name}'
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
- customer_info = context.customer_map.get(customer_name)
123
- if not customer_info:
124
- raise ValueError(f"Customer {customer_name} not found in context")
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 API
303
+ "method": "OrderB2B", # CRITICAL: Required for custom APIs
130
304
  "args": {
131
305
  "order": {
132
- "Account": customer_info['unique_name'],
133
- "Notes": "Test order",
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
- # Custom API returns direct dict, not JSON:API format
150
- response_data = r.json()
151
-
152
- # Get order ID by querying back
153
- customer_id = customer_info['id']
154
- orders_uri = f'{BASE_URL}/api/Order/?filter[customer_id]={customer_id}'
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 first item
330
+ # Get the item
163
331
  items_uri = f'{BASE_URL}/api/Item/?filter[order_id]={context.order_id}'
164
- r_items = requests.get(url=items_uri, headers=get_headers())
165
- items = r_items.json()['data']
166
- if items:
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
- @given('Shipped order exists for "{customer_name}" with {quantity:d} {product_name}')
174
- def step_impl(context, customer_name, quantity, product_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 - Tests carbon neutral discount (10% off when qty >= 10)
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
- customer_info = context.customer_map.get(customer_name)
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": customer_info['unique_name'],
219
- "Notes": "Carbon neutral order",
360
+ "Account": account_name,
361
+ "Notes": f"Multi-item order - {scenario_name}",
220
362
  "Items": [
221
363
  {
222
- "Name": product_name,
223
- "QuantityOrdered": quantity
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
- # Get order and item IDs
236
- customer_id = customer_info['id']
237
- orders_uri = f'{BASE_URL}/api/Order/?filter[customer_id]={customer_id}'
238
- r_orders = requests.get(url=orders_uri, headers=get_headers())
239
- orders = r_orders.json()['data']
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
- r_items = requests.get(url=items_uri, headers=get_headers())
247
- items = r_items.json()['data']
248
- if items:
249
- context.item_id = int(items[0]['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 - Tests OrderB2B integration, item calculations, and customer balance
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
- customer_info = context.customer_map.get(customer_name)
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": customer_info['unique_name'],
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
- # Get order and item IDs
289
- customer_id = customer_info['id']
290
- orders_uri = f'{BASE_URL}/api/Order/?filter[customer_id]={customer_id}'
291
- r_orders = requests.get(url=orders_uri, headers=get_headers())
292
- orders = r_orders.json()['data']
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
- r_items = requests.get(url=items_uri, headers=get_headers())
300
- items = r_items.json()['data']
301
- if items:
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 - Tests formula recalculation and cascading sum updates
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 quantity: {r.text}"
479
+ assert r.status_code == 200, f"Failed to update item: {r.text}"
329
480
 
330
481
 
331
- @when('Order customer changed to "{new_customer_name}"')
332
- def step_impl(context, new_customer_name):
482
+ @when('Item product changed to "{product_name}"')
483
+ def step_impl(context, product_name):
333
484
  """
334
- Phase 1: UPDATE FK - Tests adjustment of BOTH old and new parent customer balances
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
- new_customer_info = context.customer_map.get(new_customer_name)
492
+ product_id = get_product_id_by_name(product_name)
340
493
 
341
- patch_uri = f'{BASE_URL}/api/Order/{context.order_id}/'
494
+ patch_uri = f'{BASE_URL}/api/Item/{context.item_id}/'
342
495
  patch_data = {
343
496
  "data": {
344
- "type": "Order",
345
- "id": context.order_id,
497
+ "type": "Item",
498
+ "id": context.item_id,
346
499
  "attributes": {
347
- "customer_id": new_customer_info['id'] # Rule #2: Direct FK
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 change customer: {r.text}"
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 - Tests aggregate down and cascade to customer balance
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
- delete_uri = f'{BASE_URL}/api/Item/{context.item_id}/'
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 is shipped')
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 - Tests WHERE clause exclusion (shipped orders don't count in balance)
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
- import datetime
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
- "date_shipped": str(datetime.date.today())
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 ship order: {r.text}"
553
+ assert r.status_code == 200, f"Failed to update order: {r.text}"
391
554
 
392
555
 
393
- @when('Order is unshipped')
556
+ @when('Order is shipped')
394
557
  def step_impl(context):
395
558
  """
396
- Phase 1: UPDATE - Tests WHERE clause inclusion (unshipped orders count in balance again)
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": None
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 unship order: {r.text}"
578
+ assert r.status_code == 200, f"Failed to ship order: {r.text}"
414
579
 
415
580
 
416
- @when('Item product changed to "{new_product_name}"')
417
- def step_impl(context, new_product_name):
581
+ @when('Order is unshipped')
582
+ def step_impl(context):
418
583
  """
419
- Phase 1: UPDATE FK - Tests product lookup and automatic unit_price re-copy from new product
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
- # Get new product ID
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": "Item",
436
- "id": context.item_id,
594
+ "type": "Order",
595
+ "id": context.order_id,
437
596
  "attributes": {
438
- "product_id": new_product_id # Rule #2: Direct FK
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 change product: {r.text}"
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 (primary customer)"""
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: Use customer map)"""
469
- scenario_name = f'Verify {customer_name} Balance'
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 total"""
486
- scenario_name = 'Verify Order Total'
487
- test_utils.prt(f'Checking order amount_total = {expected}', scenario_name)
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
- scenario_name = 'Verify Item Amount'
506
- test_utils.prt(f'Checking item amount = {expected}', scenario_name)
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 unit price (tests copy rule)"""
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 creation succeeded"""
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 creation should fail')
671
+ @then('Order should be rejected')
544
672
  def step_impl(context):
545
- """Verify order creation failed (negative test)"""
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 content"""
552
- # Error already verified by order_created = False
553
- pass
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'])