ApiLogicServer 15.0.60__py3-none-any.whl → 15.0.65__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 +10 -5
- api_logic_server_cli/api_logic_server_info.yaml +2 -2
- api_logic_server_cli/cli.py +2 -2
- api_logic_server_cli/create_from_model/meta_model.py +1 -1
- api_logic_server_cli/genai/{genai_admin_app.py → genai_react_app.py} +10 -2
- api_logic_server_cli/genai/genai_svcs.py +2 -2
- api_logic_server_cli/prototypes/base/.github/.copilot-instructions.md +297 -1
- api_logic_server_cli/prototypes/base/docs/training/logic_bank_api.prompt +27 -0
- api_logic_server_cli/prototypes/base/logic/logic_discovery/use_case.py +5 -1
- api_logic_server_cli/prototypes/base/test/readme_test.md +395 -0
- api_logic_server_cli/prototypes/basic_demo/docs/models-not-code.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/docs/runtime engines.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/logic/procedural/credit_service.py +204 -0
- api_logic_server_cli/prototypes/basic_demo/logic/procedural/declarative-vs-procedural-comparison.png +0 -0
- api_logic_server_cli/prototypes/manager/.github/.copilot-instructions.md +1 -12
- api_logic_server_cli/prototypes/manager/system/Manager_workspace.code-workspace +2 -2
- api_logic_server_cli/prototypes/nw/test/readme_test_nw.md +224 -0
- {apilogicserver-15.0.60.dist-info → apilogicserver-15.0.65.dist-info}/METADATA +1 -1
- {apilogicserver-15.0.60.dist-info → apilogicserver-15.0.65.dist-info}/RECORD +23 -18
- api_logic_server_cli/prototypes/nw/test/readme_test.md +0 -13
- {apilogicserver-15.0.60.dist-info → apilogicserver-15.0.65.dist-info}/WHEEL +0 -0
- {apilogicserver-15.0.60.dist-info → apilogicserver-15.0.65.dist-info}/entry_points.txt +0 -0
- {apilogicserver-15.0.60.dist-info → apilogicserver-15.0.65.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-15.0.60.dist-info → apilogicserver-15.0.65.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# API Logic Server Testing Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This directory contains comprehensive tests for API Logic Server projects, designed to validate business logic rules, API endpoints, security, and data integrity. The testing framework emphasizes **business logic validation** through declarative rules rather than just basic CRUD operations.
|
|
6
|
+
|
|
7
|
+
## 🎯 What Gets Tested
|
|
8
|
+
|
|
9
|
+
- **Business Logic Rules**: Declarative rules for sums, formulas, constraints, and events
|
|
10
|
+
- **API Endpoints**: REST API functionality and JSON:API compliance
|
|
11
|
+
- **Security**: Authentication, authorization, and role-based access control
|
|
12
|
+
- **Data Integrity**: Database constraints, cascading updates, and transactions
|
|
13
|
+
- **Integration**: Logic engine, database, and API layer integration
|
|
14
|
+
|
|
15
|
+
## 📁 Test Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
test/
|
|
19
|
+
├── readme_test.md # This guide
|
|
20
|
+
├── api_logic_server_behave/ # Primary BDD testing framework
|
|
21
|
+
│ ├── behave_run.py # Main test runner
|
|
22
|
+
│ ├── behave_logic_report.py # Report generator
|
|
23
|
+
│ ├── features/ # Gherkin feature files
|
|
24
|
+
│ │ ├── *.feature # Business logic test scenarios
|
|
25
|
+
│ │ └── steps/ # Python step implementations
|
|
26
|
+
│ ├── logs/ # Test execution logs
|
|
27
|
+
│ └── reports/ # Generated test reports
|
|
28
|
+
└── basic/ # Simple server tests
|
|
29
|
+
├── server_test.py # Basic API functionality tests
|
|
30
|
+
└── results/ # Basic test results
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 🚀 Running Tests
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
1. **Start API Logic Server**: `python api_logic_server_run.py` or press F5
|
|
37
|
+
2. **Install behave**: `pip install behave` (if not already installed)
|
|
38
|
+
3. **Database State**: Tests restore data automatically, but if tests fail mid-execution, restore your database from backup
|
|
39
|
+
|
|
40
|
+
### Primary Testing Method: Behave (BDD)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# From project root
|
|
44
|
+
cd test/api_logic_server_behave
|
|
45
|
+
python behave_run.py
|
|
46
|
+
|
|
47
|
+
# Or in VS Code
|
|
48
|
+
# Use "Behave Run" configuration (F5)
|
|
49
|
+
# In Debug Console, select "Behave Run" from dropdown (not server log)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Alternative: Basic Server Tests
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# From project root
|
|
56
|
+
cd test/basic
|
|
57
|
+
python server_test.py go
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Note**: Basic tests require `SECURITY_ENABLED = False` in `config/config.py`
|
|
61
|
+
|
|
62
|
+
## 📝 Common Test Scenarios
|
|
63
|
+
|
|
64
|
+
API Logic Server projects typically test these business patterns:
|
|
65
|
+
|
|
66
|
+
### Core Business Logic
|
|
67
|
+
- **State Management**: Entity status changes and their cascading effects
|
|
68
|
+
- **Credit/Limit Checking**: Validation against business constraints
|
|
69
|
+
- **Balance/Total Calculations**: Automatic sum and formula derivations
|
|
70
|
+
- **Inventory/Resource Management**: Allocation and availability tracking
|
|
71
|
+
- **Notifications**: Email alerts and external system integration
|
|
72
|
+
- **Constraint Validation**: Business rule enforcement
|
|
73
|
+
|
|
74
|
+
*Example: In Northwind, order ready/not-ready states affect customer balances through automatic sum rules.*
|
|
75
|
+
|
|
76
|
+
### Entity Management
|
|
77
|
+
- **Audit Trails**: Automatic logging of critical changes
|
|
78
|
+
- **Derived Attributes**: Calculated fields and transformations
|
|
79
|
+
- **Business Rules**: Validation and constraint enforcement
|
|
80
|
+
|
|
81
|
+
*Example: Employee salary changes create audit records and validate minimum raise amounts.*
|
|
82
|
+
|
|
83
|
+
### Security Testing
|
|
84
|
+
- **Authentication**: Login/logout functionality
|
|
85
|
+
- **Role-Based Access**: Permission validation
|
|
86
|
+
- **Data Filtering**: Row-level security based on user roles
|
|
87
|
+
|
|
88
|
+
## 🧪 Test Data Strategy
|
|
89
|
+
|
|
90
|
+
### Testing Business Logic Derivations
|
|
91
|
+
|
|
92
|
+
**Best Practice for Derivation Testing:**
|
|
93
|
+
|
|
94
|
+
When testing business logic derivations (sums, formulas, constraints), follow this pattern:
|
|
95
|
+
|
|
96
|
+
1. **Read Initial State**: Capture the starting values before the transaction
|
|
97
|
+
2. **Execute Transaction**: Perform the business operation (create order, update quantity, etc.)
|
|
98
|
+
3. **Read Final State**: Capture the resulting values after business logic execution
|
|
99
|
+
4. **Compare Changes**: Verify the differences match expected derivation behavior
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
@when('Customer places order for ${amount:d}')
|
|
103
|
+
def step_impl(context, amount):
|
|
104
|
+
# 1. Read initial state
|
|
105
|
+
customer_response = requests.get(f"{BASE_URL}/api/Customer/1")
|
|
106
|
+
context.initial_balance = customer_response.json()['data']['attributes']['balance']
|
|
107
|
+
|
|
108
|
+
# 2. Execute transaction
|
|
109
|
+
order_data = {"customer_id": 1, "amount_total": amount}
|
|
110
|
+
context.response = requests.post(f"{BASE_URL}/api/Order", json=order_data)
|
|
111
|
+
|
|
112
|
+
@then('Customer balance increases by ${expected_increase:d}')
|
|
113
|
+
def step_impl(context, expected_increase):
|
|
114
|
+
# 3. Read final state
|
|
115
|
+
customer_response = requests.get(f"{BASE_URL}/api/Customer/1")
|
|
116
|
+
final_balance = customer_response.json()['data']['attributes']['balance']
|
|
117
|
+
|
|
118
|
+
# 4. Compare changes
|
|
119
|
+
actual_increase = final_balance - context.initial_balance
|
|
120
|
+
assert actual_increase == expected_increase, f"Balance changed by {actual_increase}, expected {expected_increase}"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Why This Approach Works:**
|
|
124
|
+
- ✅ **Independent of Initial Data**: Tests work regardless of starting database state
|
|
125
|
+
- ✅ **Focuses on Business Logic**: Verifies the rule behavior, not absolute values
|
|
126
|
+
- ✅ **Robust Against Multiple Runs**: Tests remain valid after repeated execution
|
|
127
|
+
- ✅ **Clear Intent**: Shows exactly what the business rule should accomplish
|
|
128
|
+
|
|
129
|
+
### Identifying Test Data
|
|
130
|
+
When creating tests, identify stable test entities in your database:
|
|
131
|
+
|
|
132
|
+
1. **Key Business Entities**: Main domain objects (customers, orders, products, etc.)
|
|
133
|
+
2. **Known State**: Entities with predictable initial values
|
|
134
|
+
3. **Relationships**: Connected entities that demonstrate rule cascading
|
|
135
|
+
4. **Edge Cases**: Boundary conditions for constraints and validations
|
|
136
|
+
|
|
137
|
+
### Test Data Documentation
|
|
138
|
+
Document your test data in comments or separate files:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
"""
|
|
142
|
+
Test Data Used:
|
|
143
|
+
- Primary Customer: [CustomerID] with [known balance/status]
|
|
144
|
+
- Test Order: [OrderID] with [known amount]
|
|
145
|
+
- Test Employee: [EmployeeID] with [known salary]
|
|
146
|
+
- Constraint Limits: [specific values that trigger validations]
|
|
147
|
+
"""
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 🔧 For Developers
|
|
151
|
+
|
|
152
|
+
### Adding New Tests
|
|
153
|
+
|
|
154
|
+
1. **Identify Business Scenario**: What business rule or process needs testing?
|
|
155
|
+
2. **Create Feature File**: Add `.feature` file in `features/` using Gherkin syntax
|
|
156
|
+
3. **Implement Steps**: Create corresponding `.py` file in `features/steps/`
|
|
157
|
+
4. **Use Test Utilities**: Import `test_utils.py` for common functions
|
|
158
|
+
5. **Follow Patterns**: Reference existing tests for authentication, logging, and assertions
|
|
159
|
+
|
|
160
|
+
### Generic Feature Structure
|
|
161
|
+
|
|
162
|
+
```gherkin
|
|
163
|
+
Feature: [Business Process Name]
|
|
164
|
+
|
|
165
|
+
Scenario: [Specific Business Rule Test]
|
|
166
|
+
Given [Initial business state]
|
|
167
|
+
When [Business action is performed]
|
|
168
|
+
Then [Expected business outcome occurs]
|
|
169
|
+
And [Additional validations]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Generic Step Implementation Pattern
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
# features/steps/[feature_name].py
|
|
176
|
+
from behave import *
|
|
177
|
+
import test_utils
|
|
178
|
+
import requests
|
|
179
|
+
import json
|
|
180
|
+
from dotmap import DotMap
|
|
181
|
+
|
|
182
|
+
@given('[Initial business state]')
|
|
183
|
+
def step_impl(context):
|
|
184
|
+
# Get initial state of your key business entities
|
|
185
|
+
# Store in context for later comparison
|
|
186
|
+
context.initial_state = get_entity_state(entity_id)
|
|
187
|
+
|
|
188
|
+
@when('[Business action is performed]')
|
|
189
|
+
def step_impl(context):
|
|
190
|
+
# Perform the business operation via API
|
|
191
|
+
header = test_utils.login() # Get auth header if needed
|
|
192
|
+
|
|
193
|
+
# Make API call (POST, PATCH, DELETE, etc.)
|
|
194
|
+
api_url = f'http://localhost:5656/api/[YourEntity]/[id]/'
|
|
195
|
+
response = requests.patch(url=api_url, json=your_data, headers=header)
|
|
196
|
+
|
|
197
|
+
# Store response for validation
|
|
198
|
+
context.response = response
|
|
199
|
+
|
|
200
|
+
@then('[Expected business outcome occurs]')
|
|
201
|
+
def step_impl(context):
|
|
202
|
+
# Verify the business rule executed correctly
|
|
203
|
+
# Check entity state changes, calculated fields, etc.
|
|
204
|
+
final_state = get_entity_state(entity_id)
|
|
205
|
+
assert final_state.calculated_field == expected_value
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## 🤖 For GitHub Copilot
|
|
209
|
+
|
|
210
|
+
When generating tests for API Logic Server projects:
|
|
211
|
+
|
|
212
|
+
### Key Patterns to Follow
|
|
213
|
+
|
|
214
|
+
1. **Focus on Business Logic**: Test declarative rules, not just CRUD operations
|
|
215
|
+
2. **Use Behave Framework**: Create `.feature` files with Gherkin syntax
|
|
216
|
+
3. **Test Rule Execution**: Verify LogicBank rules fire and produce correct results
|
|
217
|
+
4. **Test Constraints**: Verify business rule violations are properly caught
|
|
218
|
+
5. **Check Cascading Updates**: Ensure changes propagate through rule chains
|
|
219
|
+
6. **Include Authentication**: Use proper auth headers for API calls
|
|
220
|
+
|
|
221
|
+
### Common Test Utilities
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
import test_utils
|
|
225
|
+
|
|
226
|
+
# Authentication (handles both SQL and Keycloak)
|
|
227
|
+
header = test_utils.login(user='username') # Returns auth header dict
|
|
228
|
+
|
|
229
|
+
# Logging (appears in console and scenario log files)
|
|
230
|
+
test_utils.prt("Test message", "scenario_name")
|
|
231
|
+
|
|
232
|
+
# Typical API call patterns
|
|
233
|
+
r = requests.get(url=api_url, headers=header)
|
|
234
|
+
r = requests.post(url=api_url, json=data, headers=header)
|
|
235
|
+
r = requests.patch(url=api_url, json=data, headers=header)
|
|
236
|
+
r = requests.delete(url=api_url, headers=header)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Business Logic Testing Patterns
|
|
240
|
+
|
|
241
|
+
#### Testing Sum Rules
|
|
242
|
+
```python
|
|
243
|
+
# Pattern: Test that derived sums update automatically
|
|
244
|
+
# Example: Customer.Balance = sum(Order.AmountTotal where not shipped)
|
|
245
|
+
|
|
246
|
+
@given('Entity with calculated sum field')
|
|
247
|
+
def step_impl(context):
|
|
248
|
+
context.parent_before = get_parent_entity(parent_id)
|
|
249
|
+
|
|
250
|
+
@when('Child entity is modified')
|
|
251
|
+
def step_impl(context):
|
|
252
|
+
# Create/update child entity
|
|
253
|
+
header = test_utils.login()
|
|
254
|
+
child_data = {"amount": 100, "parent_id": parent_id}
|
|
255
|
+
r = requests.post(url=child_api_url, json=child_data, headers=header)
|
|
256
|
+
|
|
257
|
+
@then('Parent sum field updates automatically')
|
|
258
|
+
def step_impl(context):
|
|
259
|
+
parent_after = get_parent_entity(parent_id)
|
|
260
|
+
expected_sum = context.parent_before.sum_field + 100
|
|
261
|
+
assert parent_after.sum_field == expected_sum
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### Testing Constraint Rules
|
|
265
|
+
```python
|
|
266
|
+
# Pattern: Test that business constraints are enforced
|
|
267
|
+
# Example: Customer.Balance <= Customer.CreditLimit
|
|
268
|
+
|
|
269
|
+
@when('Action violates business constraint')
|
|
270
|
+
def step_impl(context):
|
|
271
|
+
header = test_utils.login()
|
|
272
|
+
# Data that should violate constraint
|
|
273
|
+
violating_data = {"amount": 99999} # Exceeds credit limit
|
|
274
|
+
r = requests.patch(url=api_url, json=violating_data, headers=header)
|
|
275
|
+
context.response = r
|
|
276
|
+
|
|
277
|
+
@then('Constraint violation is detected')
|
|
278
|
+
def step_impl(context):
|
|
279
|
+
assert context.response.status_code >= 400 # Should fail
|
|
280
|
+
error_message = context.response.json()
|
|
281
|
+
assert "constraint" in error_message.get("message", "").lower()
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### Testing Formula Rules
|
|
285
|
+
```python
|
|
286
|
+
# Pattern: Test that formulas calculate correctly
|
|
287
|
+
# Example: OrderDetail.Amount = Quantity * UnitPrice
|
|
288
|
+
|
|
289
|
+
@when('Formula inputs are changed')
|
|
290
|
+
def step_impl(context):
|
|
291
|
+
header = test_utils.login()
|
|
292
|
+
update_data = {"quantity": 5, "unit_price": 10.50}
|
|
293
|
+
r = requests.patch(url=api_url, json=update_data, headers=header)
|
|
294
|
+
|
|
295
|
+
@then('Formula result updates automatically')
|
|
296
|
+
def step_impl(context):
|
|
297
|
+
updated_entity = get_entity(entity_id)
|
|
298
|
+
expected_amount = 5 * 10.50
|
|
299
|
+
assert updated_entity.amount == expected_amount
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Security Testing Patterns
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
# Test role-based access
|
|
306
|
+
@given('User with specific role')
|
|
307
|
+
def step_impl(context):
|
|
308
|
+
context.auth_header = test_utils.login(user='role_specific_user')
|
|
309
|
+
|
|
310
|
+
@when('User accesses restricted resource')
|
|
311
|
+
def step_impl(context):
|
|
312
|
+
r = requests.get(url=restricted_api_url, headers=context.auth_header)
|
|
313
|
+
context.response = r
|
|
314
|
+
|
|
315
|
+
@then('Access is properly controlled')
|
|
316
|
+
def step_impl(context):
|
|
317
|
+
# Should succeed for authorized users, fail for others
|
|
318
|
+
if user_has_permission:
|
|
319
|
+
assert context.response.status_code == 200
|
|
320
|
+
else:
|
|
321
|
+
assert context.response.status_code in [401, 403]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Project-Specific Considerations
|
|
325
|
+
|
|
326
|
+
When creating tests for a specific project:
|
|
327
|
+
|
|
328
|
+
1. **Identify Key Business Rules**: What are the main business logic rules in `logic/declare_logic.py`?
|
|
329
|
+
2. **Map Test Scenarios**: Create scenarios that exercise each major rule type
|
|
330
|
+
3. **Use Domain Language**: Write scenarios in business terms, not technical terms
|
|
331
|
+
4. **Test Rule Interactions**: Verify that rules work together correctly
|
|
332
|
+
5. **Include Edge Cases**: Test boundary conditions and error cases
|
|
333
|
+
|
|
334
|
+
### API Endpoint Patterns
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
# Standard API endpoint patterns for any project
|
|
338
|
+
base_url = "http://localhost:5656/api"
|
|
339
|
+
|
|
340
|
+
# Get entity with relationships
|
|
341
|
+
get_url = f"{base_url}/{EntityName}/{entity_id}/?include={RelatedEntity}List"
|
|
342
|
+
|
|
343
|
+
# Create new entity
|
|
344
|
+
post_url = f"{base_url}/{EntityName}/"
|
|
345
|
+
post_data = {"data": {"attributes": {...}, "type": "EntityName"}}
|
|
346
|
+
|
|
347
|
+
# Update existing entity
|
|
348
|
+
patch_url = f"{base_url}/{EntityName}/{entity_id}/"
|
|
349
|
+
patch_data = {"data": {"attributes": {...}, "type": "EntityName", "id": entity_id}}
|
|
350
|
+
|
|
351
|
+
# Delete entity
|
|
352
|
+
delete_url = f"{base_url}/{EntityName}/{entity_id}/"
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## 📊 Test Reports and Logging
|
|
356
|
+
|
|
357
|
+
- **Logic Reports**: Generated in `reports/` showing which rules executed
|
|
358
|
+
- **Behave Output**: Standard BDD test results with pass/fail status
|
|
359
|
+
- **Logic Logs**: Detailed rule execution in `logs/scenario_logic_logs/`
|
|
360
|
+
- **Server Logs**: API request/response details and errors
|
|
361
|
+
|
|
362
|
+
## 🔍 Debugging Tests
|
|
363
|
+
|
|
364
|
+
### Common Issues and Solutions
|
|
365
|
+
|
|
366
|
+
1. **Authentication Failures**: Check `Config.SECURITY_ENABLED` and login credentials
|
|
367
|
+
2. **Database State**: If tests fail, restore database to clean state
|
|
368
|
+
3. **Rule Not Firing**: Verify rule syntax in `logic/declare_logic.py`
|
|
369
|
+
4. **API Errors**: Check server console and `logs/scenario_logic_logs/`
|
|
370
|
+
|
|
371
|
+
### Debug Steps
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
# Add debug logging to your test steps
|
|
375
|
+
test_utils.prt(f"Before: {entity_before}", scenario_name)
|
|
376
|
+
test_utils.prt(f"API Response: {response.text}", scenario_name)
|
|
377
|
+
test_utils.prt(f"After: {entity_after}", scenario_name)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## ⚠️ Important Notes
|
|
381
|
+
|
|
382
|
+
- **Data Restoration**: Tests automatically restore data, but partial failures may require manual database restore
|
|
383
|
+
- **Security Mode**: Behave tests work with security enabled; basic tests require security disabled
|
|
384
|
+
- **Rule Engine Focus**: Tests specifically validate LogicBank rule execution, not just API responses
|
|
385
|
+
- **Performance Testing**: Tests can include logic optimization verification (rule pruning, etc.)
|
|
386
|
+
- **Concurrent Testing**: Be aware of database locking if running multiple test suites simultaneously
|
|
387
|
+
|
|
388
|
+
## 🎯 Best Practices
|
|
389
|
+
|
|
390
|
+
1. **Test Business Logic First**: Focus on rules and constraints before API mechanics
|
|
391
|
+
2. **Use Meaningful Test Data**: Choose entities and values that clearly demonstrate rule behavior
|
|
392
|
+
3. **Document Test Scenarios**: Explain the business purpose of each test
|
|
393
|
+
4. **Keep Tests Independent**: Each scenario should work regardless of other test execution
|
|
394
|
+
5. **Verify Rule Execution**: Don't just test API responses, verify that business logic actually ran
|
|
395
|
+
6. **Test Error Cases**: Include scenarios that should fail due to business constraints
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from sqlalchemy import func
|
|
2
|
+
from sqlalchemy.orm import Session
|
|
3
|
+
from src.models.northwind import Customer, Order, OrderDetail, Product
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
|
|
7
|
+
class CreditService:
|
|
8
|
+
"""Service class for credit checking business logic"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, session: Session):
|
|
11
|
+
self.session = session
|
|
12
|
+
|
|
13
|
+
def calculate_item_amount(self, quantity: int, unit_price: Decimal) -> Decimal:
|
|
14
|
+
"""
|
|
15
|
+
Calculate item amount: quantity * unit_price
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
quantity: Number of units
|
|
19
|
+
unit_price: Price per unit
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Total amount for the item
|
|
23
|
+
"""
|
|
24
|
+
if quantity is None or unit_price is None:
|
|
25
|
+
return Decimal('0.00')
|
|
26
|
+
return Decimal(str(quantity)) * Decimal(str(unit_price))
|
|
27
|
+
|
|
28
|
+
def calculate_order_amount_total(self, order_id: int) -> Decimal:
|
|
29
|
+
"""
|
|
30
|
+
Calculate order amount_total as sum of all item amounts in the order
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
order_id: Order ID to calculate total for
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Total amount for the order
|
|
37
|
+
"""
|
|
38
|
+
order_details = self.session.query(OrderDetail, Product)\
|
|
39
|
+
.join(Product, OrderDetail.ProductId == Product.Id)\
|
|
40
|
+
.filter(OrderDetail.OrderId == order_id)\
|
|
41
|
+
.all()
|
|
42
|
+
|
|
43
|
+
total = Decimal('0.00')
|
|
44
|
+
for order_detail, product in order_details:
|
|
45
|
+
# Use product's current unit price if order detail doesn't have one
|
|
46
|
+
unit_price = order_detail.UnitPrice or product.UnitPrice
|
|
47
|
+
quantity = order_detail.Quantity or 0
|
|
48
|
+
|
|
49
|
+
item_amount = self.calculate_item_amount(quantity, unit_price)
|
|
50
|
+
|
|
51
|
+
# Apply discount if any
|
|
52
|
+
if order_detail.Discount:
|
|
53
|
+
discount_factor = Decimal('1.00') - (Decimal(str(order_detail.Discount)) / Decimal('100'))
|
|
54
|
+
item_amount *= discount_factor
|
|
55
|
+
|
|
56
|
+
total += item_amount
|
|
57
|
+
|
|
58
|
+
return total
|
|
59
|
+
|
|
60
|
+
def calculate_customer_balance(self, customer_id: str) -> Decimal:
|
|
61
|
+
"""
|
|
62
|
+
Calculate customer balance as sum of Order amount_total where date_shipped is null
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
customer_id: Customer ID to calculate balance for
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Current outstanding balance for the customer
|
|
69
|
+
"""
|
|
70
|
+
# Get all unshipped orders for the customer
|
|
71
|
+
unshipped_orders = self.session.query(Order)\
|
|
72
|
+
.filter(Order.CustomerId == customer_id)\
|
|
73
|
+
.filter(Order.ShippedDate.is_(None))\
|
|
74
|
+
.all()
|
|
75
|
+
|
|
76
|
+
total_balance = Decimal('0.00')
|
|
77
|
+
|
|
78
|
+
for order in unshipped_orders:
|
|
79
|
+
# Calculate the order total if not already calculated
|
|
80
|
+
if order.AmountTotal is not None:
|
|
81
|
+
order_total = Decimal(str(order.AmountTotal))
|
|
82
|
+
else:
|
|
83
|
+
order_total = self.calculate_order_amount_total(order.Id)
|
|
84
|
+
# Update the order with calculated total
|
|
85
|
+
order.AmountTotal = float(order_total)
|
|
86
|
+
|
|
87
|
+
total_balance += order_total
|
|
88
|
+
|
|
89
|
+
# Commit any updates to order totals
|
|
90
|
+
self.session.commit()
|
|
91
|
+
|
|
92
|
+
return total_balance
|
|
93
|
+
|
|
94
|
+
def check_credit_limit(self, customer_id: str) -> Dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Check if customer's balance is less than credit limit
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
customer_id: Customer ID to check credit for
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dictionary containing credit check results
|
|
103
|
+
"""
|
|
104
|
+
customer = self.session.query(Customer).filter(Customer.Id == customer_id).first()
|
|
105
|
+
|
|
106
|
+
if not customer:
|
|
107
|
+
return {
|
|
108
|
+
'success': False,
|
|
109
|
+
'error': 'Customer not found',
|
|
110
|
+
'customer_id': customer_id
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Calculate current balance
|
|
114
|
+
current_balance = self.calculate_customer_balance(customer_id)
|
|
115
|
+
|
|
116
|
+
# Get credit limit
|
|
117
|
+
credit_limit = Decimal(str(customer.CreditLimit)) if customer.CreditLimit else Decimal('0.00')
|
|
118
|
+
|
|
119
|
+
# Check if balance is within credit limit
|
|
120
|
+
credit_available = credit_limit - current_balance
|
|
121
|
+
within_limit = current_balance <= credit_limit
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
'success': True,
|
|
125
|
+
'customer_id': customer_id,
|
|
126
|
+
'customer_name': customer.CompanyName,
|
|
127
|
+
'current_balance': float(current_balance),
|
|
128
|
+
'credit_limit': float(credit_limit),
|
|
129
|
+
'credit_available': float(credit_available),
|
|
130
|
+
'within_credit_limit': within_limit,
|
|
131
|
+
'balance_percentage': float((current_balance / credit_limit * 100) if credit_limit > 0 else 0),
|
|
132
|
+
'unshipped_order_count': self.session.query(Order)
|
|
133
|
+
.filter(Order.CustomerId == customer_id)
|
|
134
|
+
.filter(Order.ShippedDate.is_(None))
|
|
135
|
+
.count()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def get_credit_status_summary(self, customer_id: str) -> Dict[str, Any]:
|
|
139
|
+
"""
|
|
140
|
+
Get comprehensive credit status including order details
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
customer_id: Customer ID to get status for
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Detailed credit status information
|
|
147
|
+
"""
|
|
148
|
+
credit_check = self.check_credit_limit(customer_id)
|
|
149
|
+
|
|
150
|
+
if not credit_check['success']:
|
|
151
|
+
return credit_check
|
|
152
|
+
|
|
153
|
+
# Get unshipped orders with details
|
|
154
|
+
unshipped_orders = self.session.query(Order)\
|
|
155
|
+
.filter(Order.CustomerId == customer_id)\
|
|
156
|
+
.filter(Order.ShippedDate.is_(None))\
|
|
157
|
+
.order_by(Order.OrderDate.desc())\
|
|
158
|
+
.all()
|
|
159
|
+
|
|
160
|
+
orders_details = []
|
|
161
|
+
for order in unshipped_orders:
|
|
162
|
+
order_total = self.calculate_order_amount_total(order.Id)
|
|
163
|
+
orders_details.append({
|
|
164
|
+
'order_id': order.Id,
|
|
165
|
+
'order_date': order.OrderDate,
|
|
166
|
+
'required_date': order.RequiredDate,
|
|
167
|
+
'amount_total': float(order_total),
|
|
168
|
+
'freight': float(order.Freight) if order.Freight else 0,
|
|
169
|
+
'ship_name': order.ShipName,
|
|
170
|
+
'order_detail_count': order.OrderDetailCount or 0
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
credit_check['unshipped_orders'] = orders_details
|
|
174
|
+
return credit_check
|
|
175
|
+
|
|
176
|
+
def update_order_amounts(self, order_id: Optional[int] = None) -> Dict[str, Any]:
|
|
177
|
+
"""
|
|
178
|
+
Update order amount totals based on current order details and product prices
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
order_id: Specific order to update, or None to update all orders
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Update results
|
|
185
|
+
"""
|
|
186
|
+
if order_id:
|
|
187
|
+
orders = self.session.query(Order).filter(Order.Id == order_id).all()
|
|
188
|
+
else:
|
|
189
|
+
orders = self.session.query(Order).all()
|
|
190
|
+
|
|
191
|
+
updated_count = 0
|
|
192
|
+
for order in orders:
|
|
193
|
+
new_total = self.calculate_order_amount_total(order.Id)
|
|
194
|
+
if order.AmountTotal != float(new_total):
|
|
195
|
+
order.AmountTotal = float(new_total)
|
|
196
|
+
updated_count += 1
|
|
197
|
+
|
|
198
|
+
self.session.commit()
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
'success': True,
|
|
202
|
+
'updated_orders': updated_count,
|
|
203
|
+
'total_orders_processed': len(orders)
|
|
204
|
+
}
|
api_logic_server_cli/prototypes/basic_demo/logic/procedural/declarative-vs-procedural-comparison.png
ADDED
|
Binary file
|
|
@@ -7,18 +7,7 @@ The Project Manager contains genai training, and provides a convenient place to
|
|
|
7
7
|
> Note: your virtual environment is automatically configured in most cases; verify it with `genai-logic` - it it's not there, activate the `venv` in this project.
|
|
8
8
|
|
|
9
9
|
## Quick Start for New Users
|
|
10
|
-
|
|
11
|
-
echo "=== Repository Sizes ==="
|
|
12
|
-
du -sh .git/
|
|
13
|
-
du -sh .
|
|
14
|
-
|
|
15
|
-
# Find largest files in Git history
|
|
16
|
-
echo "=== Largest Files in Git History ==="
|
|
17
|
-
git rev-list --objects --all | \
|
|
18
|
-
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
|
|
19
|
-
awk '/^blob/ {print substr($0,6)}' | \
|
|
20
|
-
sort --numeric-sort --key=2 | \
|
|
21
|
-
tail -10
|
|
10
|
+
|
|
22
11
|
**ALWAYS start new users with the basic_demo** - it's the best introduction to the system.
|
|
23
12
|
|
|
24
13
|
It's a small sqlite database, and provides a readme that explores the project, and how to customize it:
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"workbench.editorAssociations": {
|
|
10
10
|
"*.md": "vscode.markdown.preview.editor"
|
|
11
11
|
},
|
|
12
|
-
"workbench.colorTheme": "
|
|
13
|
-
"workbench.preferredLightColorTheme": "
|
|
12
|
+
"workbench.colorTheme": "Word (Light)",
|
|
13
|
+
"workbench.preferredLightColorTheme": "Word (Light)"
|
|
14
14
|
}
|
|
15
15
|
}
|