Compare commits
353 Commits
6af9458ad4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 21e4ac5124 | |||
| 3ade1b9354 | |||
| b5bb9415f6 | |||
| bb3d6f0012 | |||
| c92fe1261b | |||
| ca152cd544 | |||
| 914967edcc | |||
| 64fe58c171 | |||
| 3044490a3e | |||
| adc36246b8 | |||
| dd9dc04328 | |||
| 4a60d75a13 | |||
| e98eddc168 | |||
| 8cd09f3f89 | |||
| 4c1608f78a | |||
| 24219e4d9a | |||
| fde58bea06 | |||
| 52b78ce346 | |||
| f804ff8442 | |||
| d9abb275a5 | |||
| 4b56eb7ab1 | |||
| 27ac7f3e28 | |||
| dfd42c1b10 | |||
| 297b8a8d5a | |||
| 91fb4b0757 | |||
| f4386e97ee | |||
| e8c9fc7e7d | |||
| d591200df8 | |||
| 83af32eb88 | |||
| 2a49e3d30f | |||
| 6e40e16017 | |||
| dd09bcaeec | |||
| 013eafd775 | |||
| 07cd66a0e3 | |||
| 73d453d78a | |||
| d4e9fed719 | |||
| 3e93f64c6b | |||
| 377d2d3ae8 | |||
| b51f9e8e30 | |||
| d380437594 | |||
| cff0af31be | |||
| e492e5f71c | |||
| 9a5b7dd061 | |||
| b3051b423a | |||
| bc951a36d9 | |||
| 2e043260eb | |||
| 1828ac85eb | |||
| 50a4fc38a7 | |||
| 30f3dae5a3 | |||
| 4c750f0268 | |||
| 59b0d8977a | |||
| 2bc03ed97c | |||
| 91963f3b87 | |||
| 3ae0b579d3 | |||
| 972ee1e5d0 | |||
| 70f2803dd3 | |||
| a247622d23 | |||
| 50d50fcbd0 | |||
| b306a5e8f4 | |||
| 28b08580c8 | |||
| 754bfca87d | |||
| 1decb4572c | |||
| d685341b04 | |||
| 0c6d8409c7 | |||
| f81851445e | |||
| 4748368809 | |||
| f310363f7c | |||
| 95f0eac079 | |||
| 11dcfdad73 | |||
| 01f7add8dd | |||
| 0d1007282a | |||
| 2a15c14ee8 | |||
| bc5e227d81 | |||
| 8a70259445 | |||
| 823935c016 | |||
| dab5560de8 | |||
| 157b4c6ec3 | |||
| 211c46ebbc | |||
| d81e9a3fa4 | |||
| fd0de714a4 | |||
| c6b155520c | |||
| 66b77e747d | |||
| 71b5eb1758 | |||
| b4f01210d9 | |||
| 9bceeaac9c | |||
| 332960de30 | |||
| 0455e63a2e | |||
| aaed1b2d01 | |||
| 9dee534b2f | |||
| beef3ce76b | |||
| 884a694718 | |||
| 4cafbe9610 | |||
| 19923ed26b | |||
| 46f8d227b8 | |||
| 95e4956216 | |||
| 77e520bbce | |||
| 518bace534 | |||
| fcde2d68fc | |||
| 5a33f68743 | |||
| 040cbd1962 | |||
| b679c9687d | |||
| 314360a394 | |||
| 44a0c38016 | |||
| da9e1ab293 | |||
| 5de297a804 | |||
| 4429674100 | |||
| 316ec42566 | |||
| 894832c62b | |||
| 1d90bfe044 | |||
| ce0caa5685 | |||
| 33f823aba0 | |||
| edd55cd2fd | |||
| f3344b2859 | |||
| 1107de989b | |||
| a423bcf03e | |||
| 661547f6cf | |||
| 3015a490f9 | |||
| 5b4ed79f87 | |||
| 52a5f941fe | |||
| 6161d69ba2 | |||
| f41f72b86f | |||
| 644bf158cd | |||
| f89c0382f0 | |||
| 11b8e31a29 | |||
| 0ddef13124 | |||
| 60bed05d3f | |||
| 40da2d6b11 | |||
| d96e0ea1b4 | |||
| 7d652716bb | |||
| b6047f5b7d | |||
| 366d4b9765 | |||
| 540205402f | |||
| 07fab01f6a | |||
| 6c07f6cbb2 | |||
| bc7431943a | |||
| adec17cd02 | |||
| a28d5d1de5 | |||
| 502473eee4 | |||
| 183f55c7b3 | |||
| 169a774b9c | |||
| ebbe6d62b8 | |||
| c2c0e3c740 | |||
| 4a1f71a312 | |||
| 5dd5e01dc6 | |||
| 694a1cd1a5 | |||
| 826ef2ddd2 | |||
| a1cc05cd3d | |||
| 19d267587b | |||
| 9a13aee8ed | |||
| 9c39a9703f | |||
| 395707951e | |||
| 34bf961309 | |||
| 44acf5e442 | |||
| b3224ba13d | |||
| 93b7279c3a | |||
| 29d942322d | |||
| 8c8975239a | |||
| f766a72480 | |||
| 618376aa39 | |||
| efca9734d2 | |||
| 6acd783754 | |||
| 8cf5da6914 | |||
| eee33d6a1b | |||
| aefca3115e | |||
| 319900623a | |||
| a77a8a3a98 | |||
| f141cc4e6a | |||
| 2287f4597d | |||
| 8136739233 | |||
| 2ca313c3c7 | |||
| 27802e47c2 | |||
| 14d5ff97f3 | |||
| b9b8ffadcb | |||
| 31ced5f759 | |||
| 802cc6b137 | |||
| 45260b6b82 | |||
| fa758b7e31 | |||
| a099bfdc48 | |||
| cb9a829684 | |||
| c4e9e4e646 | |||
| 8c449d7baa | |||
| 820ab1aaa4 | |||
| 2268f32f51 | |||
| b68d542258 | |||
| a7392de9f6 | |||
| 3c7e4458af | |||
| 8b147f53c6 | |||
| 784bcb9d23 | |||
| b8aa484653 | |||
| 05c53e1865 | |||
| 6dec1e3ca6 | |||
| f631283286 | |||
| f631322b4e | |||
| e61e02fb39 | |||
| b5b73559b5 | |||
| 28dca65a06 | |||
| adbecd360b | |||
| ef9ea29643 | |||
| f8a2394da5 | |||
| 4d07418f44 | |||
| bf64f82613 | |||
| 9684747d08 | |||
| 2078ce35b2 | |||
| 22ae63b414 | |||
| 78ee05f50e | |||
| 6d6eba75bf | |||
| a709adaee8 | |||
| 8d5c8a52e6 | |||
| d8f0cf16c7 | |||
| 93a2d9baff | |||
| 35d1559162 | |||
| ce822af883 | |||
| 4ebd419987 | |||
| 2b29867093 | |||
| 30c4593e0f | |||
| 8c0967e215 | |||
| 86e85a98b8 | |||
| e3a52f6536 | |||
| 4aa6f76e46 | |||
| f95db7c0b1 | |||
| 2b55e7458b | |||
| c82210795f | |||
| cb3bc3c118 | |||
| 962862ccc1 | |||
| 3053bc5d92 | |||
| 79a88b0a36 | |||
| e7f8e61717 | |||
| d480b59df4 | |||
| ce5b54f27b | |||
| 6a82d7c12d | |||
| f1e7baaa6c | |||
| 6b46a78e72 | |||
| d648c921b7 | |||
| 3df75e2e78 | |||
| 92a434530f | |||
| 01146d5c97 | |||
| d0d5aadaf7 | |||
| 56afb9192b | |||
| a4519035df | |||
| c9b2ecbdff | |||
| 1194731f33 | |||
| 12c1c3c511 | |||
| 81cf84ed28 | |||
| a6e6d9be8e | |||
| ec888f2e94 | |||
| 53dfe018c2 | |||
| 3de69e55a1 | |||
| cfce6c0ca4 | |||
| 2833ff1476 | |||
| f47c680cb8 | |||
| 32e4aa6564 | |||
| 6c78827c7f | |||
| 0389294b1a | |||
| cd935988c4 | |||
| 05d31a7fc5 | |||
| 272b62fbd3 | |||
| 32acc76b49 | |||
| d36783a7f1 | |||
| 2fc157d7b2 | |||
| 506171503d | |||
| be248222bc | |||
| 716a4e3d15 | |||
| 467b1510f4 | |||
| 5c8fbd21c7 | |||
| 1f3042547b | |||
| d7a383f3d7 | |||
| b77952bf89 | |||
| ff852f1ab3 | |||
| 42b894094a | |||
| b9ac252a9f | |||
| 51e512ec08 | |||
| f517a7ccd7 | |||
| c47a394a7b | |||
| 1eef69f300 | |||
| 1dcb0e6c33 | |||
| ef21d47533 | |||
| 6c5969e4e1 | |||
| 6a739bf670 | |||
| ffa12f0255 | |||
| 93731b7173 | |||
| e5dbd7ef1a | |||
| 167bb50f4f | |||
| 182610283d | |||
| e23788cb7d | |||
| 573b0180ad | |||
| d9fc52d47a | |||
| a8b29750a5 | |||
| 2c710ad416 | |||
| 682213fdee | |||
| 3d1586f025 | |||
| 64082ca877 | |||
| 67260e9322 | |||
| 44568893fd | |||
| 10fdf91dfa | |||
| 8ee8c398ce | |||
| 3a7cf29386 | |||
| eaab47f2f8 | |||
| 6458ab13d7 | |||
| 0b701fb847 | |||
| f67510b706 | |||
| 8c715cfde3 | |||
| 4bce16fb73 | |||
| 1cb659e3a5 | |||
| 3ec58c1524 | |||
| b382090771 | |||
| 5474fc5301 | |||
| cd596b85b3 | |||
| eedc463207 | |||
| 677e5211f9 | |||
| 10aa75aa69 | |||
| aad18c27ab | |||
| b0db8133a0 | |||
| 1b8a40f1ff | |||
| f84c5d903e | |||
| ef7187b508 | |||
| 488d5a6f0e | |||
| 3c2b559282 | |||
| 62e418c473 | |||
| 688896d856 | |||
| cf08e1a6c8 | |||
| ba130d4171 | |||
| e9253fbd84 | |||
| 34ee7bb7ad | |||
| 481deaa67d | |||
| 11f1909f68 | |||
| 9154eec871 | |||
| b0a40200c1 | |||
| 8dcc4145aa | |||
| 77b76afb3f | |||
| 8968e7d9cd | |||
| 531487f5c9 | |||
| 9c27fa02b0 | |||
| 7c43d6f4a2 | |||
| 9173448645 | |||
| 874e254c11 | |||
| 8abcea154b | |||
| c3bb496a98 | |||
| 779de02f97 | |||
| af3f04a23f | |||
| c58ceb9872 | |||
| 363eb74d22 | |||
| 011a4df2d4 | |||
| 79c985ee39 | |||
| 3a264c0a39 | |||
| 1b24269ef1 | |||
| 9c4f6064b2 | |||
| f20266167d | |||
| e3428cc4aa | |||
| 7852d09dcc | |||
| 0acfa75c8e | |||
| b265d0db51 | |||
| bf5bb69409 | |||
| d9060ed6ea |
@@ -24,7 +24,9 @@ api_endpoint_rules:
|
||||
SCHEMA LOCATION: All response schemas must be defined in models/schema/*.py,
|
||||
never inline in endpoint files. This ensures schemas are reusable and discoverable.
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/**/*.py"
|
||||
- "app/modules/*/routes/api/**/*.py"
|
||||
anti_patterns:
|
||||
- "return dict"
|
||||
- "-> dict"
|
||||
@@ -82,7 +84,9 @@ api_endpoint_rules:
|
||||
# In app/api/v1/admin/my_feature.py
|
||||
from models.schema.my_feature import MyRequest
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/**/*.py"
|
||||
- "app/modules/*/routes/api/**/*.py"
|
||||
anti_patterns:
|
||||
- "from pydantic import"
|
||||
- "from pydantic.main import"
|
||||
@@ -118,7 +122,9 @@ api_endpoint_rules:
|
||||
- db.query() - complex queries are business logic
|
||||
- db.delete() - deleting entities is business logic
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/**/*.py"
|
||||
- "app/modules/*/routes/api/**/*.py"
|
||||
anti_patterns:
|
||||
- "db.add("
|
||||
- "db.delete("
|
||||
@@ -139,7 +145,7 @@ api_endpoint_rules:
|
||||
- Dependencies (app/api/deps.py) - authentication/authorization validation
|
||||
- Services (app/services/) - business logic validation
|
||||
|
||||
The global exception handler catches all WizamartException subclasses and
|
||||
The global exception handler catches all OrionException subclasses and
|
||||
converts them to appropriate HTTP responses.
|
||||
|
||||
WRONG (endpoint raises exception):
|
||||
@@ -155,7 +161,9 @@ api_endpoint_rules:
|
||||
# Dependency guarantees token_vendor_id is present
|
||||
return order_service.get_orders(db, current_user.token_vendor_id)
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/**/*.py"
|
||||
- "app/modules/*/routes/api/**/*.py"
|
||||
anti_patterns:
|
||||
- "raise HTTPException"
|
||||
- "raise InvalidTokenException"
|
||||
@@ -184,7 +192,9 @@ api_endpoint_rules:
|
||||
def stripe_webhook(request: Request):
|
||||
...
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/**/*.py"
|
||||
- "app/modules/*/routes/api/**/*.py"
|
||||
required_if_not_public:
|
||||
- "Depends(get_current_"
|
||||
auto_exclude_files:
|
||||
@@ -197,11 +207,14 @@ api_endpoint_rules:
|
||||
name: "Multi-tenant endpoints must scope queries to vendor_id"
|
||||
severity: "error"
|
||||
description: |
|
||||
All queries in vendor/shop contexts must filter by vendor_id.
|
||||
All queries in vendor/storefront contexts must filter by vendor_id.
|
||||
Use request.state.vendor_id from middleware.
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/vendor/**/*.py"
|
||||
file_pattern: "app/api/v1/storefront/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/vendor/**/*.py"
|
||||
- "app/modules/*/routes/api/store*.py"
|
||||
- "app/api/v1/storefront/**/*.py"
|
||||
- "app/modules/*/routes/api/storefront*.py"
|
||||
discouraged_patterns:
|
||||
- "db.query(.*).all()"
|
||||
|
||||
@@ -248,7 +261,9 @@ api_endpoint_rules:
|
||||
- from models.database.*
|
||||
- from app.modules.*.models.*
|
||||
pattern:
|
||||
file_pattern: "app/api/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/**/*.py"
|
||||
- "app/modules/*/routes/api/**/*.py"
|
||||
anti_patterns:
|
||||
- "from models\\.database\\."
|
||||
- "from app\\.modules\\.[a-z_]+\\.models\\."
|
||||
|
||||
@@ -9,7 +9,9 @@ auth_rules:
|
||||
description: |
|
||||
Authentication must use JWT tokens in Authorization: Bearer header
|
||||
pattern:
|
||||
file_pattern: "app/api/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/**/*.py"
|
||||
- "app/modules/*/routes/api/**/*.py"
|
||||
enforcement: "middleware"
|
||||
|
||||
- id: "AUTH-002"
|
||||
@@ -18,7 +20,9 @@ auth_rules:
|
||||
description: |
|
||||
Use Depends(get_current_admin/vendor/customer) for role checks
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/**/*.py"
|
||||
- "app/modules/*/routes/api/**/*.py"
|
||||
required: "Depends\\(get_current_"
|
||||
|
||||
- id: "AUTH-003"
|
||||
@@ -36,10 +40,10 @@ auth_rules:
|
||||
description: |
|
||||
Two vendor context patterns exist - use the appropriate one:
|
||||
|
||||
1. SHOP ENDPOINTS (public, no authentication required):
|
||||
1. STOREFRONT ENDPOINTS (public, no authentication required):
|
||||
- Use: vendor: Vendor = Depends(require_vendor_context())
|
||||
- Vendor is detected from URL/subdomain/domain
|
||||
- File pattern: app/api/v1/storefront/**/*.py
|
||||
- File pattern: app/api/v1/storefront/**/*.py, app/modules/*/routes/api/storefront*.py
|
||||
- Mark as public with: # public
|
||||
|
||||
2. VENDOR API ENDPOINTS (authenticated):
|
||||
@@ -49,15 +53,19 @@ auth_rules:
|
||||
- File pattern: app/api/v1/vendor/**/*.py
|
||||
|
||||
DEPRECATED for vendor APIs:
|
||||
- require_vendor_context() - only for shop endpoints
|
||||
- require_vendor_context() - only for storefront endpoints
|
||||
- getattr(request.state, "vendor", None) without permission dependency
|
||||
|
||||
See: docs/backend/vendor-in-token-architecture.md
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/vendor/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/vendor/**/*.py"
|
||||
- "app/modules/*/routes/api/store*.py"
|
||||
anti_patterns:
|
||||
- "require_vendor_context\\(\\)"
|
||||
file_pattern: "app/api/v1/storefront/**/*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/storefront/**/*.py"
|
||||
- "app/modules/*/routes/api/storefront*.py"
|
||||
required_patterns:
|
||||
- "require_vendor_context\\(\\)|# public"
|
||||
|
||||
@@ -149,7 +157,9 @@ multi_tenancy_rules:
|
||||
description: |
|
||||
In vendor/shop contexts, all database queries must filter by vendor_id
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
context: "vendor_shop"
|
||||
required_pattern: ".filter\\(.*vendor_id.*\\)"
|
||||
|
||||
@@ -159,5 +169,7 @@ multi_tenancy_rules:
|
||||
description: |
|
||||
Queries must never access data from other vendors
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
enforcement: "database_query_level"
|
||||
|
||||
@@ -10,7 +10,9 @@ exception_rules:
|
||||
Create domain-specific exceptions in app/exceptions/ for better
|
||||
error handling and clarity.
|
||||
pattern:
|
||||
file_pattern: "app/exceptions/**/*.py"
|
||||
file_pattern:
|
||||
- "app/exceptions/**/*.py"
|
||||
- "app/modules/*/exceptions.py"
|
||||
encouraged_structure: |
|
||||
class VendorError(Exception):
|
||||
"""Base exception for vendor-related errors"""
|
||||
@@ -34,21 +36,25 @@ exception_rules:
|
||||
description: |
|
||||
When catching exceptions, log them with context and stack trace.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
encouraged_patterns:
|
||||
- "logger.error"
|
||||
- "exc_info=True"
|
||||
|
||||
- id: "EXC-004"
|
||||
name: "Domain exceptions must inherit from WizamartException"
|
||||
name: "Domain exceptions must inherit from OrionException"
|
||||
severity: "error"
|
||||
description: |
|
||||
All custom domain exceptions must inherit from WizamartException (or its
|
||||
All custom domain exceptions must inherit from OrionException (or its
|
||||
subclasses like ResourceNotFoundException, ValidationException, etc.).
|
||||
This ensures the global exception handler catches and converts them properly.
|
||||
pattern:
|
||||
file_pattern: "app/exceptions/**/*.py"
|
||||
required_base_class: "WizamartException"
|
||||
file_pattern:
|
||||
- "app/exceptions/**/*.py"
|
||||
- "app/modules/*/exceptions.py"
|
||||
required_base_class: "OrionException"
|
||||
example_good: |
|
||||
class VendorNotFoundException(ResourceNotFoundException):
|
||||
def __init__(self, vendor_code: str):
|
||||
@@ -59,7 +65,7 @@ exception_rules:
|
||||
severity: "error"
|
||||
description: |
|
||||
The global exception handler must be set up in app initialization to
|
||||
catch WizamartException and convert to HTTP responses.
|
||||
catch OrionException and convert to HTTP responses.
|
||||
pattern:
|
||||
file_pattern: "app/main.py"
|
||||
required_patterns:
|
||||
|
||||
@@ -157,7 +157,7 @@ javascript_rules:
|
||||
- Page URLs (not API calls) like window.location.href = `/vendor/${vendorCode}/...`
|
||||
|
||||
Why this matters:
|
||||
- Including vendorCode causes 404 errors ("/vendor/wizamart/orders" not found)
|
||||
- Including vendorCode causes 404 errors ("/vendor/orion/orders" not found)
|
||||
- The JWT token already identifies the vendor
|
||||
- Consistent with the API design pattern
|
||||
pattern:
|
||||
@@ -238,6 +238,50 @@ javascript_rules:
|
||||
exceptions:
|
||||
- "utils.js"
|
||||
|
||||
- id: "JS-015"
|
||||
name: "Use confirm_modal macros, not native confirm()"
|
||||
severity: "error"
|
||||
description: |
|
||||
All confirmation dialogs must use the project's confirm_modal or
|
||||
confirm_modal_dynamic Jinja2 macros from shared/macros/modals.html.
|
||||
Never use the native browser confirm() dialog.
|
||||
|
||||
The modal macros provide:
|
||||
- Consistent styled dialogs matching the admin/store theme
|
||||
- Dark mode support
|
||||
- Variant colors (danger=red, warning=yellow, info=blue)
|
||||
- Icon support
|
||||
- Double-confirm pattern for destructive operations
|
||||
|
||||
WRONG (native browser dialog):
|
||||
if (!confirm('Are you sure you want to delete this?')) return;
|
||||
if (!confirm(I18n.t('confirmations.delete'))) return;
|
||||
|
||||
RIGHT (state variable + modal macro):
|
||||
// In JS: add state variable and remove confirm() guard
|
||||
showDeleteModal: false,
|
||||
async deleteItem() {
|
||||
// No confirm() guard — modal already confirmed
|
||||
await apiClient.delete('/admin/items/' + this.item.id);
|
||||
}
|
||||
|
||||
// In template: button sets state, macro shows modal
|
||||
<button @click="showDeleteModal = true">Delete</button>
|
||||
{{ confirm_modal('deleteModal', 'Delete Item', 'Are you sure?',
|
||||
'deleteItem()', 'showDeleteModal', 'Delete', 'Cancel', 'danger') }}
|
||||
|
||||
For dynamic messages (containing JS expressions):
|
||||
{{ confirm_modal_dynamic('deleteModal', 'Delete Item',
|
||||
"'Delete ' + item.name + '?'",
|
||||
'deleteItem()', 'showDeleteModal', 'Delete', 'Cancel', 'danger') }}
|
||||
pattern:
|
||||
file_pattern: "static/**/js/**/*.js"
|
||||
anti_patterns:
|
||||
- "confirm\\("
|
||||
exceptions:
|
||||
- "utils.js"
|
||||
- "vendor/"
|
||||
|
||||
- id: "JS-010"
|
||||
name: "Use PlatformSettings for pagination rows per page"
|
||||
severity: "error"
|
||||
|
||||
@@ -111,11 +111,9 @@ language_rules:
|
||||
function languageSelector(currentLang, enabledLanguages) { ... }
|
||||
window.languageSelector = languageSelector;
|
||||
pattern:
|
||||
file_pattern: "static/shop/js/shop-layout.js"
|
||||
required_patterns:
|
||||
- "function languageSelector"
|
||||
- "window.languageSelector"
|
||||
file_pattern: "static/vendor/js/init-alpine.js"
|
||||
file_patterns:
|
||||
- "static/shop/js/shop-layout.js"
|
||||
- "static/vendor/js/init-alpine.js"
|
||||
required_patterns:
|
||||
- "function languageSelector"
|
||||
- "window.languageSelector"
|
||||
@@ -247,3 +245,26 @@ language_rules:
|
||||
pattern:
|
||||
file_pattern: "static/locales/*.json"
|
||||
check: "valid_json"
|
||||
|
||||
- id: "LANG-011"
|
||||
name: "Use $t() not I18n.t() in HTML templates"
|
||||
severity: "error"
|
||||
description: |
|
||||
In HTML templates, never use I18n.t() directly. It evaluates once
|
||||
and does NOT re-evaluate when translations finish loading async.
|
||||
|
||||
WRONG (non-reactive, shows raw key then updates):
|
||||
<span x-text="I18n.t('module.key')"></span>
|
||||
|
||||
RIGHT (reactive, updates when translations load):
|
||||
<span x-text="$t('module.key')"></span>
|
||||
|
||||
BEST (server-side, zero flash):
|
||||
<span>{{ _('module.key') }}</span>
|
||||
|
||||
Note: I18n.t() is fine in .js files where it's called inside
|
||||
async callbacks after I18n.init() has completed.
|
||||
pattern:
|
||||
file_pattern: "**/*.html"
|
||||
anti_patterns:
|
||||
- "I18n.t("
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Architecture Rules - Model Rules
|
||||
# Rules for models/database/*.py and models/schema/*.py files
|
||||
# Rules for models/database/*.py, models/schema/*.py, app/modules/*/models/**/*.py, and app/modules/*/schemas/**/*.py files
|
||||
|
||||
model_rules:
|
||||
|
||||
@@ -10,7 +10,9 @@ model_rules:
|
||||
All database models must inherit from SQLAlchemy Base and use proper
|
||||
column definitions with types and constraints.
|
||||
pattern:
|
||||
file_pattern: "models/database/**/*.py"
|
||||
file_pattern:
|
||||
- "models/database/**/*.py"
|
||||
- "app/modules/*/models/**/*.py"
|
||||
required_patterns:
|
||||
- "class.*\\(Base\\):"
|
||||
|
||||
@@ -21,7 +23,10 @@ model_rules:
|
||||
Never mix SQLAlchemy and Pydantic in the same model.
|
||||
SQLAlchemy = database schema, Pydantic = API validation/serialization.
|
||||
pattern:
|
||||
file_pattern: "models/**/*.py"
|
||||
file_pattern:
|
||||
- "models/**/*.py"
|
||||
- "app/modules/*/models/**/*.py"
|
||||
- "app/modules/*/schemas/**/*.py"
|
||||
anti_patterns:
|
||||
- "class.*\\(Base, BaseModel\\):"
|
||||
|
||||
@@ -31,7 +36,9 @@ model_rules:
|
||||
description: |
|
||||
Pydantic response models must enable from_attributes to work with SQLAlchemy models.
|
||||
pattern:
|
||||
file_pattern: "models/schema/**/*.py"
|
||||
file_pattern:
|
||||
- "models/schema/**/*.py"
|
||||
- "app/modules/*/schemas/**/*.py"
|
||||
required_in_response_models:
|
||||
- "from_attributes = True"
|
||||
|
||||
@@ -51,5 +58,7 @@ model_rules:
|
||||
Junction/join tables use both entity names in plural:
|
||||
- Good: vendor_users, order_items, product_translations
|
||||
pattern:
|
||||
file_pattern: "models/database/**/*.py"
|
||||
file_pattern:
|
||||
- "models/database/**/*.py"
|
||||
- "app/modules/*/models/**/*.py"
|
||||
check: "table_naming_plural"
|
||||
|
||||
@@ -141,7 +141,7 @@ module_rules:
|
||||
en.json
|
||||
de.json
|
||||
fr.json
|
||||
lu.json
|
||||
lb.json
|
||||
|
||||
Translation keys are namespaced as {module}.key_name
|
||||
pattern:
|
||||
@@ -154,16 +154,16 @@ module_rules:
|
||||
severity: "warning"
|
||||
description: |
|
||||
Self-contained modules should have an exceptions.py file defining
|
||||
module-specific exceptions that inherit from WizamartException.
|
||||
module-specific exceptions that inherit from OrionException.
|
||||
|
||||
Structure:
|
||||
app/modules/{module}/exceptions.py
|
||||
|
||||
Example:
|
||||
# app/modules/analytics/exceptions.py
|
||||
from app.exceptions import WizamartException
|
||||
from app.exceptions import OrionException
|
||||
|
||||
class AnalyticsException(WizamartException):
|
||||
class AnalyticsException(OrionException):
|
||||
"""Base exception for analytics module."""
|
||||
pass
|
||||
|
||||
@@ -269,14 +269,14 @@ module_rules:
|
||||
Module locales/ directory should have translation files for
|
||||
all supported languages to ensure consistent i18n.
|
||||
|
||||
Supported languages: en, de, fr, lu
|
||||
Supported languages: en, de, fr, lb
|
||||
|
||||
Structure:
|
||||
app/modules/<code>/locales/
|
||||
├── en.json
|
||||
├── de.json
|
||||
├── fr.json
|
||||
└── lu.json
|
||||
└── lb.json
|
||||
|
||||
Missing translations will fall back to English, but it's
|
||||
better to have all languages covered.
|
||||
@@ -286,7 +286,7 @@ module_rules:
|
||||
- "en.json"
|
||||
- "de.json"
|
||||
- "fr.json"
|
||||
- "lu.json"
|
||||
- "lb.json"
|
||||
|
||||
- id: "MOD-007"
|
||||
name: "Module definition must match directory structure"
|
||||
@@ -692,8 +692,9 @@ module_rules:
|
||||
name: "Modules with routers should use get_*_with_routers pattern"
|
||||
severity: "info"
|
||||
description: |
|
||||
Modules that define routers (admin_router, vendor_router, etc.)
|
||||
should follow the lazy import pattern with a dedicated function:
|
||||
Modules that define routers should follow the lazy import pattern
|
||||
with a dedicated function. Route files use `router` as the variable
|
||||
name; consumer code distinguishes via `admin_router`/`store_router`.
|
||||
|
||||
def get_{module}_module_with_routers() -> ModuleDefinition:
|
||||
|
||||
@@ -704,12 +705,12 @@ module_rules:
|
||||
|
||||
WRONG:
|
||||
# Direct router assignment at module level
|
||||
module.admin_router = admin_router
|
||||
module.admin_router = router
|
||||
|
||||
RIGHT:
|
||||
def _get_admin_router():
|
||||
from app.modules.orders.routes.admin import admin_router
|
||||
return admin_router
|
||||
from app.modules.orders.routes.api.admin import router
|
||||
return router
|
||||
|
||||
def get_orders_module_with_routers() -> ModuleDefinition:
|
||||
orders_module.admin_router = _get_admin_router()
|
||||
@@ -761,3 +762,96 @@ module_rules:
|
||||
file_pattern: "main.py"
|
||||
validates:
|
||||
- "module_locales mount BEFORE module_static mount"
|
||||
|
||||
# =========================================================================
|
||||
# Cross-Module Boundary Rules
|
||||
# =========================================================================
|
||||
|
||||
- id: "MOD-025"
|
||||
name: "Modules must NOT import models from other modules"
|
||||
severity: "error"
|
||||
description: |
|
||||
Modules must access data from other modules through their SERVICE layer,
|
||||
never by importing and querying their models directly.
|
||||
|
||||
This is the "services over models" principle: if module A needs data
|
||||
from module B, it MUST call module B's service methods.
|
||||
|
||||
WRONG (direct model import):
|
||||
# app/modules/orders/services/order_service.py
|
||||
from app.modules.catalog.models import Product # FORBIDDEN
|
||||
|
||||
class OrderService:
|
||||
def get_order_details(self, db, order_id):
|
||||
product = db.query(Product).filter_by(id=pid).first()
|
||||
|
||||
RIGHT (service call):
|
||||
# app/modules/orders/services/order_service.py
|
||||
from app.modules.catalog.services import product_service
|
||||
|
||||
class OrderService:
|
||||
def get_order_details(self, db, order_id):
|
||||
product = product_service.get_product_by_id(db, pid)
|
||||
|
||||
ALSO RIGHT (provider protocol for core→optional):
|
||||
# app/modules/core/services/stats_aggregator.py
|
||||
from app.modules.contracts.metrics import MetricsProviderProtocol
|
||||
# Discover providers through registry, no direct imports
|
||||
|
||||
EXCEPTIONS:
|
||||
- Test fixtures may create models from other modules for setup
|
||||
- TYPE_CHECKING imports for type hints are allowed
|
||||
- Tenancy models (User, Store, Merchant, Platform) may be imported
|
||||
as type hints in route signatures where FastAPI requires it,
|
||||
but queries must go through tenancy services
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Encapsulation: Modules own their data access patterns
|
||||
- Refactoring: Module B can change its schema without breaking A
|
||||
- Testability: Mock services, not database queries
|
||||
- Consistency: Clear API boundaries between modules
|
||||
- Decoupling: Modules can evolve independently
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/services/**/*.py"
|
||||
anti_patterns:
|
||||
- "from app\\.modules\\.(?!<own_module>)\\.models import"
|
||||
exceptions:
|
||||
- "TYPE_CHECKING"
|
||||
- "tests/"
|
||||
|
||||
- id: "MOD-026"
|
||||
name: "Cross-module data access must use service methods"
|
||||
severity: "error"
|
||||
description: |
|
||||
When a module needs data from another module, it must use that
|
||||
module's public service API. Each module should expose service
|
||||
methods for common data access patterns.
|
||||
|
||||
Service methods a module should expose:
|
||||
- get_{entity}_by_id(db, id) -> Entity or None
|
||||
- list_{entities}(db, filters) -> list[Entity]
|
||||
- get_{entity}_count(db, filters) -> int
|
||||
- search_{entities}(db, query, filters) -> list[Entity]
|
||||
|
||||
WRONG (direct query across module boundary):
|
||||
# In orders module
|
||||
count = db.query(func.count(Product.id)).scalar()
|
||||
|
||||
RIGHT (call catalog service):
|
||||
# In orders module
|
||||
count = product_service.get_product_count(db, store_id=store_id)
|
||||
|
||||
This applies to:
|
||||
- Simple lookups (get by ID)
|
||||
- List/search queries
|
||||
- Aggregation queries (count, sum)
|
||||
- Join queries (should be decomposed into service calls)
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Single source of truth for data access logic
|
||||
- Easier to add caching, validation, or access control
|
||||
- Clear contract between modules
|
||||
- Simpler testing with service mocks
|
||||
pattern:
|
||||
file_pattern: "app/modules/*/services/**/*.py"
|
||||
check: "cross_module_service_usage"
|
||||
|
||||
@@ -23,7 +23,9 @@ money_handling_rules:
|
||||
|
||||
Column naming convention: Use `_cents` suffix for all monetary columns.
|
||||
pattern:
|
||||
file_pattern: "models/database/**/*.py"
|
||||
file_pattern:
|
||||
- "models/database/**/*.py"
|
||||
- "app/modules/*/models/**/*.py"
|
||||
required_patterns:
|
||||
- "_cents = Column(Integer"
|
||||
anti_patterns:
|
||||
@@ -79,7 +81,9 @@ money_handling_rules:
|
||||
|
||||
Or use model validators to convert before response serialization.
|
||||
pattern:
|
||||
file_pattern: "models/schema/**/*.py"
|
||||
file_pattern:
|
||||
- "models/schema/**/*.py"
|
||||
- "app/modules/*/schemas/**/*.py"
|
||||
check: "money_response_format"
|
||||
|
||||
- id: "MON-004"
|
||||
@@ -124,7 +128,9 @@ money_handling_rules:
|
||||
tax = subtotal * 0.17 # Floating point!
|
||||
total = subtotal + tax
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
check: "money_arithmetic"
|
||||
|
||||
- id: "MON-006"
|
||||
|
||||
@@ -15,6 +15,10 @@ naming_rules:
|
||||
- "__init__.py"
|
||||
- "auth.py"
|
||||
- "health.py"
|
||||
- "store.py"
|
||||
- "admin.py"
|
||||
- "platform.py"
|
||||
- "storefront.py"
|
||||
|
||||
- id: "NAM-002"
|
||||
name: "Service files use SINGULAR + 'service' suffix"
|
||||
@@ -22,8 +26,17 @@ naming_rules:
|
||||
description: |
|
||||
Service files should use singular name + _service (vendor_service.py)
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
check: "service_naming"
|
||||
exceptions:
|
||||
- "*_features.py"
|
||||
- "*_metrics.py"
|
||||
- "*_widgets.py"
|
||||
- "*_aggregator.py"
|
||||
- "*_provider.py"
|
||||
- "*_presets.py"
|
||||
|
||||
- id: "NAM-003"
|
||||
name: "Model files use SINGULAR names"
|
||||
@@ -31,14 +44,16 @@ naming_rules:
|
||||
description: |
|
||||
Both database and schema model files use singular names (product.py)
|
||||
pattern:
|
||||
file_pattern: "models/**/*.py"
|
||||
file_pattern:
|
||||
- "models/**/*.py"
|
||||
- "app/modules/*/models/**/*.py"
|
||||
check: "singular_naming"
|
||||
|
||||
- id: "NAM-004"
|
||||
name: "Use consistent terminology: vendor not shop"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Use 'vendor' consistently, not 'shop' (except for shop frontend)
|
||||
Use 'vendor' consistently, not 'shop' (except for storefront)
|
||||
pattern:
|
||||
file_pattern: "app/**/*.py"
|
||||
discouraged_terms:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Architecture Rules - Service Layer Rules
|
||||
# Rules for app/services/**/*.py files
|
||||
# Rules for app/services/**/*.py and app/modules/*/services/**/*.py files
|
||||
|
||||
service_layer_rules:
|
||||
|
||||
@@ -10,7 +10,9 @@ service_layer_rules:
|
||||
Services are business logic layer - they should NOT know about HTTP.
|
||||
Raise domain-specific exceptions instead (ValueError, custom exceptions).
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
anti_patterns:
|
||||
- "raise HTTPException"
|
||||
- "from fastapi import HTTPException"
|
||||
@@ -22,7 +24,9 @@ service_layer_rules:
|
||||
Services should raise meaningful domain exceptions, not generic Exception.
|
||||
Create custom exception classes for business rule violations.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
discouraged_patterns:
|
||||
- "raise Exception\\("
|
||||
|
||||
@@ -33,7 +37,9 @@ service_layer_rules:
|
||||
Service methods should receive database session as a parameter for testability
|
||||
and transaction control. Never create session inside service.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
required_in_method_signature:
|
||||
- "db: Session"
|
||||
anti_patterns:
|
||||
@@ -47,7 +53,9 @@ service_layer_rules:
|
||||
Service methods should accept Pydantic models for complex inputs
|
||||
to ensure type safety and validation.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
encouraged_patterns:
|
||||
- "BaseModel"
|
||||
|
||||
@@ -57,7 +65,9 @@ service_layer_rules:
|
||||
description: |
|
||||
All database queries must be scoped to vendor_id to prevent cross-tenant data access.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
check: "vendor_scoping"
|
||||
|
||||
- id: "SVC-006"
|
||||
@@ -74,11 +84,22 @@ service_layer_rules:
|
||||
|
||||
The endpoint should call db.commit() after all service operations succeed.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
anti_patterns:
|
||||
- "db.commit()"
|
||||
exceptions:
|
||||
- "log_service.py"
|
||||
- "card_service.py"
|
||||
- "wallet_service.py"
|
||||
- "program_service.py"
|
||||
- "points_service.py"
|
||||
- "apple_wallet_service.py"
|
||||
- "pin_service.py"
|
||||
- "stamp_service.py"
|
||||
- "google_wallet_service.py"
|
||||
- "theme_presets.py"
|
||||
|
||||
- id: "SVC-007"
|
||||
name: "Service return types must match API response schemas"
|
||||
@@ -113,5 +134,7 @@ service_layer_rules:
|
||||
result = service.get_stats(db)
|
||||
StatsResponse(**result) # Raises if keys don't match
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
file_pattern:
|
||||
- "app/services/**/*.py"
|
||||
- "app/modules/*/services/**/*.py"
|
||||
check: "schema_compatibility"
|
||||
|
||||
@@ -55,7 +55,7 @@ rules:
|
||||
type: file_exists
|
||||
paths:
|
||||
- ".github/PULL_REQUEST_TEMPLATE.md"
|
||||
- ".gitlab/merge_request_templates/*.md"
|
||||
- "CONTRIBUTING.md"
|
||||
message: "Pull request template recommended"
|
||||
|
||||
- id: CHANGE-REV-002
|
||||
@@ -74,7 +74,6 @@ rules:
|
||||
type: file_exists
|
||||
paths:
|
||||
- ".github/CODEOWNERS"
|
||||
- "CODEOWNERS" # GitLab uses root CODEOWNERS or .gitlab/CODEOWNERS
|
||||
- "CODEOWNERS"
|
||||
message: "Consider defining code owners for critical paths"
|
||||
|
||||
@@ -91,7 +90,7 @@ rules:
|
||||
paths:
|
||||
- ".github/workflows/ci.yml"
|
||||
- ".github/workflows/test.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
message: "CI workflow for automated testing required"
|
||||
|
||||
- id: CHANGE-CI-002
|
||||
@@ -102,7 +101,7 @@ rules:
|
||||
type: pattern_recommended
|
||||
paths:
|
||||
- ".github/workflows/*.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
patterns:
|
||||
- "security|bandit|safety|snyk|trivy"
|
||||
message: "Consider security scanning in CI pipeline"
|
||||
@@ -115,7 +114,7 @@ rules:
|
||||
type: pattern_required
|
||||
paths:
|
||||
- ".github/workflows/*.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
patterns:
|
||||
- "ruff|flake8|pylint|mypy|lint"
|
||||
message: "Code quality checks required in CI"
|
||||
@@ -146,7 +145,7 @@ rules:
|
||||
paths:
|
||||
- ".github/workflows/release.yml"
|
||||
- ".github/workflows/deploy.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
- "Dockerfile"
|
||||
message: "Automated deployment process recommended"
|
||||
|
||||
@@ -199,7 +198,7 @@ rules:
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
- ".github/workflows/*.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
patterns:
|
||||
- "tag|version|:v"
|
||||
message: "Container image versioning recommended"
|
||||
|
||||
@@ -122,10 +122,9 @@ rules:
|
||||
type: file_exists
|
||||
paths:
|
||||
- ".github/PULL_REQUEST_TEMPLATE.md"
|
||||
- ".gitlab/merge_request_templates/*.md"
|
||||
- "CONTRIBUTING.md"
|
||||
- ".github/workflows/*.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
message: "Code review process must be documented/enforced"
|
||||
|
||||
- id: COMP-POL-002
|
||||
@@ -138,8 +137,7 @@ rules:
|
||||
- ".github/CODEOWNERS"
|
||||
- "CODEOWNERS"
|
||||
- ".github/workflows/*.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
message: "Document change approval requirements"
|
||||
|
||||
- id: COMP-POL-003
|
||||
@@ -166,7 +164,7 @@ rules:
|
||||
type: file_exists
|
||||
paths:
|
||||
- ".github/workflows/ci.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
- "pytest.ini"
|
||||
- "pyproject.toml"
|
||||
patterns:
|
||||
@@ -181,7 +179,7 @@ rules:
|
||||
type: file_exists
|
||||
paths:
|
||||
- ".github/workflows/*.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
patterns:
|
||||
- "deploy|release"
|
||||
message: "Deployment process must be automated and logged"
|
||||
|
||||
@@ -94,7 +94,7 @@ rules:
|
||||
paths:
|
||||
- "SECURITY.md"
|
||||
- ".github/SECURITY.md"
|
||||
- ".gitlab/SECURITY.md"
|
||||
- ".gitea/SECURITY.md"
|
||||
message: "Security policy (SECURITY.md) required"
|
||||
|
||||
- id: DOC-SEC-002
|
||||
|
||||
@@ -57,7 +57,7 @@ rules:
|
||||
type: file_exists
|
||||
paths:
|
||||
- ".github/workflows/*.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
patterns:
|
||||
- "safety|pip-audit|snyk|dependabot"
|
||||
message: "Dependency vulnerability scanning required"
|
||||
@@ -70,7 +70,7 @@ rules:
|
||||
type: file_exists
|
||||
paths:
|
||||
- ".github/dependabot.yml"
|
||||
- ".gitlab-ci.yml" # GitLab uses built-in dependency scanning
|
||||
- ".gitea/workflows/*.yml"
|
||||
message: "Consider enabling Dependabot for security updates"
|
||||
|
||||
- id: THIRD-VULN-003
|
||||
@@ -81,7 +81,7 @@ rules:
|
||||
type: pattern_recommended
|
||||
paths:
|
||||
- ".github/workflows/*.yml"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitea/workflows/*.yml"
|
||||
patterns:
|
||||
- "trivy|grype|snyk.*container"
|
||||
message: "Consider container image vulnerability scanning"
|
||||
|
||||
21
.dockerignore
Normal file
21
.dockerignore
Normal file
@@ -0,0 +1,21 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.git
|
||||
.gitea
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
site/
|
||||
docs/
|
||||
exports/
|
||||
alembic/versions_backup/
|
||||
*.csv
|
||||
*.md
|
||||
!requirements.txt
|
||||
.pre-commit-config.yaml
|
||||
.architecture-rules/
|
||||
.performance-rules/
|
||||
.security-rules/
|
||||
mkdocs.yml
|
||||
monitoring/
|
||||
77
.env.example
77
.env.example
@@ -6,7 +6,7 @@ DEBUG=False
|
||||
# =============================================================================
|
||||
# PROJECT INFORMATION
|
||||
# =============================================================================
|
||||
PROJECT_NAME=Wizamart - Multi-Store Marketplace Platform
|
||||
PROJECT_NAME=Orion - Multi-Store Marketplace Platform
|
||||
DESCRIPTION=Multi-tenants multi-themes ecommerce application
|
||||
VERSION=2.2.0
|
||||
|
||||
@@ -14,17 +14,17 @@ VERSION=2.2.0
|
||||
# DATABASE CONFIGURATION (PostgreSQL required)
|
||||
# =============================================================================
|
||||
# Default works with: docker-compose up -d db
|
||||
DATABASE_URL=postgresql://wizamart_user:secure_password@localhost:5432/wizamart_db
|
||||
DATABASE_URL=postgresql://orion_user:secure_password@localhost:5432/orion_db
|
||||
|
||||
# For production, use your PostgreSQL connection string:
|
||||
# DATABASE_URL=postgresql://username:password@production-host:5432/wizamart_db
|
||||
# DATABASE_URL=postgresql://username:password@production-host:5432/orion_db
|
||||
|
||||
# =============================================================================
|
||||
# ADMIN INITIALIZATION
|
||||
# =============================================================================
|
||||
# These are used by init_production.py to create the platform admin
|
||||
# ⚠️ CHANGE THESE IN PRODUCTION!
|
||||
ADMIN_EMAIL=admin@wizamart.com
|
||||
ADMIN_EMAIL=admin@wizard.lu
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=change-me-in-production
|
||||
ADMIN_FIRST_NAME=Platform
|
||||
@@ -49,9 +49,9 @@ API_PORT=8000
|
||||
# Development
|
||||
DOCUMENTATION_URL=http://localhost:8001
|
||||
# Staging
|
||||
# DOCUMENTATION_URL=https://staging-docs.wizamart.com
|
||||
# DOCUMENTATION_URL=https://staging-docs.wizard.lu
|
||||
# Production
|
||||
# DOCUMENTATION_URL=https://docs.wizamart.com
|
||||
# DOCUMENTATION_URL=https://docs.wizard.lu
|
||||
|
||||
# =============================================================================
|
||||
# RATE LIMITING
|
||||
@@ -67,10 +67,15 @@ LOG_LEVEL=INFO
|
||||
LOG_FILE=logs/app.log
|
||||
|
||||
# =============================================================================
|
||||
# PLATFORM DOMAIN CONFIGURATION
|
||||
# MAIN DOMAIN CONFIGURATION
|
||||
# =============================================================================
|
||||
# Your main platform domain
|
||||
PLATFORM_DOMAIN=wizamart.com
|
||||
MAIN_DOMAIN=wizard.lu
|
||||
|
||||
# Full base URL for outbound links (emails, billing redirects, etc.)
|
||||
# Must include protocol and port if non-standard
|
||||
# Examples: http://localhost:8000, http://acme.localhost:9999, https://wizard.lu
|
||||
APP_BASE_URL=http://localhost:8000
|
||||
|
||||
# Custom domain features
|
||||
# Enable/disable custom domains
|
||||
@@ -85,7 +90,7 @@ SSL_PROVIDER=letsencrypt
|
||||
AUTO_PROVISION_SSL=False
|
||||
|
||||
# DNS verification
|
||||
DNS_VERIFICATION_PREFIX=_wizamart-verify
|
||||
DNS_VERIFICATION_PREFIX=_wizard-verify
|
||||
DNS_VERIFICATION_TTL=3600
|
||||
|
||||
# =============================================================================
|
||||
@@ -103,8 +108,8 @@ STRIPE_TRIAL_DAYS=30
|
||||
# =============================================================================
|
||||
# Provider: smtp, sendgrid, mailgun, ses
|
||||
EMAIL_PROVIDER=smtp
|
||||
EMAIL_FROM_ADDRESS=noreply@wizamart.com
|
||||
EMAIL_FROM_NAME=Wizamart
|
||||
EMAIL_FROM_ADDRESS=noreply@wizard.lu
|
||||
EMAIL_FROM_NAME=Wizard
|
||||
EMAIL_REPLY_TO=
|
||||
|
||||
# SMTP Settings (used when EMAIL_PROVIDER=smtp)
|
||||
@@ -149,6 +154,10 @@ SEED_ORDERS_PER_STORE=10
|
||||
# =============================================================================
|
||||
# CELERY / REDIS TASK QUEUE
|
||||
# =============================================================================
|
||||
# Redis password (must match docker-compose.yml --requirepass flag)
|
||||
# ⚠️ CHANGE THIS IN PRODUCTION! Generate with: openssl rand -hex 16
|
||||
REDIS_PASSWORD=changeme
|
||||
|
||||
# Redis connection URL (used for Celery broker and backend)
|
||||
# Default works with: docker-compose up -d redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
@@ -173,6 +182,14 @@ SENTRY_DSN=
|
||||
SENTRY_ENVIRONMENT=production
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
|
||||
# =============================================================================
|
||||
# MONITORING
|
||||
# =============================================================================
|
||||
ENABLE_METRICS=true
|
||||
GRAFANA_URL=https://grafana.wizard.lu
|
||||
GRAFANA_ADMIN_USER=admin
|
||||
GRAFANA_ADMIN_PASSWORD=changeme
|
||||
|
||||
# =============================================================================
|
||||
# CLOUDFLARE R2 STORAGE
|
||||
# =============================================================================
|
||||
@@ -185,13 +202,49 @@ STORAGE_BACKEND=local
|
||||
R2_ACCOUNT_ID=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET_NAME=wizamart-media
|
||||
R2_BUCKET_NAME=orion-media
|
||||
|
||||
# Public URL for R2 bucket (optional - for custom domain)
|
||||
# If not set, uses Cloudflare's default R2 public URL
|
||||
# Example: https://media.yoursite.com
|
||||
R2_PUBLIC_URL=
|
||||
|
||||
# Cloudflare R2 backup bucket (used by scripts/backup.sh --upload)
|
||||
R2_BACKUP_BUCKET=orion-backups
|
||||
|
||||
# =============================================================================
|
||||
# LOYALTY MODULE
|
||||
# =============================================================================
|
||||
# Anti-fraud defaults (all optional, shown values are defaults)
|
||||
# LOYALTY_DEFAULT_COOLDOWN_MINUTES=15
|
||||
# LOYALTY_MAX_DAILY_STAMPS=5
|
||||
# LOYALTY_PIN_MAX_FAILED_ATTEMPTS=5
|
||||
# LOYALTY_PIN_LOCKOUT_MINUTES=30
|
||||
|
||||
# Points configuration
|
||||
# LOYALTY_DEFAULT_POINTS_PER_EURO=10
|
||||
|
||||
# Google Wallet integration
|
||||
# See docs/deployment/hetzner-server-setup.md Step 25 for setup guide
|
||||
# Get Issuer ID from https://pay.google.com/business/console
|
||||
# LOYALTY_GOOGLE_ISSUER_ID=3388000000012345678
|
||||
# Production convention: ~/apps/orion/google-wallet-sa.json (app user, mode 600).
|
||||
# Path is validated at startup — file must exist and be readable, otherwise
|
||||
# the app fails fast at import time.
|
||||
# LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=~/apps/orion/google-wallet-sa.json
|
||||
# LOYALTY_GOOGLE_WALLET_ORIGINS=["https://yourdomain.com"]
|
||||
# LOYALTY_DEFAULT_LOGO_URL=https://yourdomain.com/path/to/default-logo.png
|
||||
|
||||
# Apple Wallet integration (requires Apple Developer account)
|
||||
# LOYALTY_APPLE_PASS_TYPE_ID=pass.com.example.loyalty
|
||||
# LOYALTY_APPLE_TEAM_ID=ABCD1234
|
||||
# LOYALTY_APPLE_WWDR_CERT_PATH=/path/to/wwdr.pem
|
||||
# LOYALTY_APPLE_SIGNER_CERT_PATH=/path/to/signer.pem
|
||||
# LOYALTY_APPLE_SIGNER_KEY_PATH=/path/to/signer.key
|
||||
|
||||
# QR code size in pixels (default: 300)
|
||||
# LOYALTY_QR_CODE_SIZE=300
|
||||
|
||||
# =============================================================================
|
||||
# CLOUDFLARE CDN / PROXY
|
||||
# =============================================================================
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Gitea Actions CI/CD Configuration
|
||||
# ==================================
|
||||
# Equivalent of the GitLab CI pipeline, using GitHub Actions-compatible syntax.
|
||||
# Uses GitHub Actions-compatible syntax. Requires Gitea 1.19+ with Actions enabled.
|
||||
# Requires Gitea 1.19+ with Actions enabled.
|
||||
|
||||
name: CI
|
||||
@@ -31,34 +31,34 @@ jobs:
|
||||
run: pip install uv
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen
|
||||
run: uv pip install --system -r requirements.txt -r requirements-dev.txt
|
||||
|
||||
- name: Run ruff
|
||||
run: .venv/bin/ruff check .
|
||||
run: ruff check .
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# Tests — unit only (integration tests run locally via make test)
|
||||
# ---------------------------------------------------------------------------
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 150
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_DB: wizamart_test
|
||||
POSTGRES_DB: orion_test
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U test_user -d wizamart_test"
|
||||
--health-cmd "pg_isready -U test_user -d orion_test"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
env:
|
||||
TEST_DATABASE_URL: "postgresql://test_user:test_password@localhost:5432/wizamart_test"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@localhost:5432/wizamart_test"
|
||||
TEST_DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/orion_test"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/orion_test"
|
||||
LOG_LEVEL: "WARNING"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -67,26 +67,16 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Cache pip & venv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
.venv
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('uv.lock', 'pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen
|
||||
run: uv pip install --system -r requirements.txt -r requirements-test.txt
|
||||
|
||||
- name: Run tests
|
||||
run: .venv/bin/python -m pytest tests/ -v --tb=short
|
||||
- name: Run unit tests
|
||||
run: python -m pytest -m "unit" -q --tb=short --timeout=120 --no-cov --override-ini="addopts=" -p no:cacheprovider -p no:logging --durations=20
|
||||
|
||||
architecture:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy"
|
||||
@@ -102,10 +92,19 @@ jobs:
|
||||
run: pip install uv
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen
|
||||
run: uv pip install --system -r requirements.txt
|
||||
|
||||
- name: Validate architecture
|
||||
run: .venv/bin/python scripts/validate/validate_architecture.py
|
||||
- name: Validate architecture patterns
|
||||
run: python scripts/validate/validate_all.py --architecture
|
||||
|
||||
- name: Validate security patterns
|
||||
run: python scripts/validate/validate_all.py --security
|
||||
|
||||
- name: Validate performance patterns
|
||||
run: python scripts/validate/validate_all.py --performance
|
||||
|
||||
- name: Validate audit patterns
|
||||
run: python scripts/validate/validate_all.py --audit
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Security (non-blocking)
|
||||
@@ -126,32 +125,13 @@ jobs:
|
||||
- name: Run pip-audit
|
||||
run: pip-audit --requirement requirements.txt || true
|
||||
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Run audit
|
||||
run: .venv/bin/python scripts/validate/validate_audit.py
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build (docs - only on push to master)
|
||||
# ---------------------------------------------------------------------------
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
needs: [ruff, pytest, architecture]
|
||||
needs: [ruff, pytest, validate]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -163,13 +143,25 @@ jobs:
|
||||
run: pip install uv
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen
|
||||
run: uv pip install --system -r requirements.txt -r requirements-docs.txt
|
||||
|
||||
- name: Build docs
|
||||
run: .venv/bin/mkdocs build
|
||||
run: mkdocs build
|
||||
|
||||
- name: Upload docs artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deploy (master-only, after lint + tests + validate pass)
|
||||
# ---------------------------------------------------------------------------
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
needs: [ruff, pytest, validate]
|
||||
steps:
|
||||
- name: Deploy to production
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
name: docs-site
|
||||
path: site/
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
port: 22
|
||||
command_timeout: 10m
|
||||
script: cd ${{ secrets.DEPLOY_PATH }} && bash scripts/deploy.sh
|
||||
|
||||
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
## Summary
|
||||
|
||||
<!-- Brief description of what this PR does -->
|
||||
|
||||
## Changes
|
||||
|
||||
-
|
||||
|
||||
## Test plan
|
||||
|
||||
- [ ] Unit tests pass (`python -m pytest tests/unit/`)
|
||||
- [ ] Integration tests pass (`python -m pytest tests/integration/`)
|
||||
- [ ] Architecture validation passes (`python scripts/validate/validate_all.py`)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No new warnings introduced
|
||||
- [ ] Database migrations included (if applicable)
|
||||
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "dependencies"
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -156,11 +156,10 @@ uploads/
|
||||
__pypackages__/
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
.dockerignore.local
|
||||
*.override.yml
|
||||
|
||||
# Deployment & Security
|
||||
.build-info
|
||||
deployment-local/
|
||||
*.pem
|
||||
*.key
|
||||
@@ -168,6 +167,11 @@ deployment-local/
|
||||
secrets/
|
||||
credentials/
|
||||
|
||||
# Google Cloud service account keys
|
||||
*-service-account.json
|
||||
google-wallet-sa.json
|
||||
orion-*.json
|
||||
|
||||
# Alembic
|
||||
# Note: Keep alembic/versions/ tracked for migrations
|
||||
# alembic/versions/*.pyc is already covered by __pycache__
|
||||
@@ -183,5 +187,8 @@ tailadmin-free-tailwind-dashboard-template/
|
||||
static/shared/css/tailwind.css
|
||||
|
||||
# Export files
|
||||
wizamart_letzshop_export_*.csv
|
||||
orion_letzshop_export_*.csv
|
||||
exports/
|
||||
|
||||
# Security audit (needs revamping)
|
||||
scripts/security-audit/
|
||||
|
||||
130
.gitlab-ci.yml
130
.gitlab-ci.yml
@@ -1,130 +0,0 @@
|
||||
# GitLab CI/CD Configuration
|
||||
# =========================
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
- security
|
||||
- build
|
||||
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
PYTHON_VERSION: "3.11"
|
||||
|
||||
# Cache dependencies between jobs
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
- .venv/
|
||||
|
||||
# Lint Stage
|
||||
# ----------
|
||||
|
||||
ruff:
|
||||
stage: lint
|
||||
image: python:${PYTHON_VERSION}
|
||||
before_script:
|
||||
- pip install uv
|
||||
- uv sync --frozen
|
||||
script:
|
||||
- .venv/bin/ruff check .
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
# Test Stage
|
||||
# ----------
|
||||
|
||||
pytest:
|
||||
stage: test
|
||||
image: python:${PYTHON_VERSION}
|
||||
services:
|
||||
- name: postgres:15
|
||||
alias: postgres
|
||||
variables:
|
||||
# PostgreSQL service configuration
|
||||
POSTGRES_DB: wizamart_test
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
# Application database URL for tests
|
||||
TEST_DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/wizamart_test"
|
||||
# Skip database validation during import (tests use TEST_DATABASE_URL)
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/wizamart_test"
|
||||
before_script:
|
||||
- pip install uv
|
||||
- uv sync --frozen
|
||||
# Wait for PostgreSQL to be ready
|
||||
- apt-get update && apt-get install -y postgresql-client
|
||||
- for i in $(seq 1 30); do pg_isready -h postgres -U test_user && break || sleep 1; done
|
||||
script:
|
||||
- .venv/bin/python -m pytest tests/ -v --tb=short
|
||||
coverage: '/TOTAL.*\s+(\d+%)/'
|
||||
artifacts:
|
||||
reports:
|
||||
junit: report.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
architecture:
|
||||
stage: test
|
||||
image: python:${PYTHON_VERSION}
|
||||
variables:
|
||||
# Set DATABASE_URL to satisfy validation (not actually used by validator)
|
||||
DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy"
|
||||
before_script:
|
||||
- pip install uv
|
||||
- uv sync --frozen
|
||||
script:
|
||||
- .venv/bin/python scripts/validate/validate_architecture.py
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
# Security Stage
|
||||
# --------------
|
||||
|
||||
dependency_scanning:
|
||||
stage: security
|
||||
image: python:${PYTHON_VERSION}
|
||||
before_script:
|
||||
- pip install pip-audit
|
||||
script:
|
||||
- pip-audit --requirement requirements.txt || true
|
||||
allow_failure: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
audit:
|
||||
stage: security
|
||||
image: python:${PYTHON_VERSION}
|
||||
before_script:
|
||||
- pip install uv
|
||||
- uv sync --frozen
|
||||
script:
|
||||
- .venv/bin/python scripts/validate/validate_audit.py
|
||||
allow_failure: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
# Build Stage
|
||||
# -----------
|
||||
|
||||
docs:
|
||||
stage: build
|
||||
image: python:${PYTHON_VERSION}
|
||||
before_script:
|
||||
- pip install uv
|
||||
- uv sync --frozen
|
||||
script:
|
||||
- .venv/bin/mkdocs build
|
||||
artifacts:
|
||||
paths:
|
||||
- site/
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
2
.idea/inspectionProfiles/Project_Default.xml
generated
2
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -7,4 +7,4 @@
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
</component>
|
||||
|
||||
2
.idea/inspectionProfiles/profiles_settings.xml
generated
2
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -3,4 +3,4 @@
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
</component>
|
||||
|
||||
2
.idea/modules.xml
generated
2
.idea/modules.xml
generated
@@ -5,4 +5,4 @@
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/fastapi-multitenant-ecommerce.iml" filepath="$PROJECT_DIR$/.idea/fastapi-multitenant-ecommerce.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Run manually: pre-commit run --all-files
|
||||
|
||||
repos:
|
||||
# Architecture validation
|
||||
# Code validators (architecture, security, performance, audit)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-architecture
|
||||
@@ -16,6 +16,33 @@ repos:
|
||||
additional_dependencies: [pyyaml]
|
||||
verbose: true
|
||||
|
||||
- id: validate-security
|
||||
name: Validate Security Patterns
|
||||
entry: python scripts/validate/validate_all.py --security
|
||||
language: python
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
additional_dependencies: [pyyaml]
|
||||
verbose: true
|
||||
|
||||
- id: validate-performance
|
||||
name: Validate Performance Patterns
|
||||
entry: python scripts/validate/validate_all.py --performance
|
||||
language: python
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
additional_dependencies: [pyyaml]
|
||||
verbose: true
|
||||
|
||||
- id: validate-audit
|
||||
name: Validate Audit Patterns
|
||||
entry: python scripts/validate/validate_all.py --audit
|
||||
language: python
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
additional_dependencies: [pyyaml]
|
||||
verbose: true
|
||||
|
||||
# Python code quality
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
@@ -23,21 +50,16 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
exclude: mkdocs.yml # Uses Python tags (!!python/name) unsupported by basic YAML checker
|
||||
- id: check-added-large-files
|
||||
args: ['--maxkb=1000']
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: debug-statements
|
||||
|
||||
# Python formatting (optional - uncomment if you want)
|
||||
# - repo: https://github.com/psf/black
|
||||
# rev: 23.12.1
|
||||
# hooks:
|
||||
# - id: black
|
||||
# language_version: python3
|
||||
|
||||
# Python import sorting (optional)
|
||||
# - repo: https://github.com/pycqa/isort
|
||||
# rev: 5.13.2
|
||||
# hooks:
|
||||
# - id: isort
|
||||
# Ruff - linting and import sorting (replaces black + isort)
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
@@ -116,7 +116,7 @@ return {
|
||||
|
||||
### Duplicate /shop/ Prefix
|
||||
|
||||
**Problem:** Routes like `/stores/wizamart/shop/shop/products/4`
|
||||
**Problem:** Routes like `/stores/orion/shop/shop/products/4`
|
||||
|
||||
**Root Cause:**
|
||||
```python
|
||||
@@ -136,7 +136,7 @@ All routes in `shop_pages.py` fixed.
|
||||
|
||||
### Missing /shop/ in Template Links
|
||||
|
||||
**Problem:** Links went to `/stores/wizamart/products` instead of `/shop/products`
|
||||
**Problem:** Links went to `/stores/orion/products` instead of `/shop/products`
|
||||
|
||||
**Fix:** Updated all templates:
|
||||
- `shop/base.html` - Header, footer, navigation
|
||||
@@ -290,15 +290,15 @@ Comprehensive guide covering:
|
||||
### Test URLs
|
||||
```
|
||||
Landing Pages:
|
||||
- http://localhost:8000/stores/wizamart/
|
||||
- http://localhost:8000/stores/orion/
|
||||
- http://localhost:8000/stores/fashionhub/
|
||||
- http://localhost:8000/stores/bookstore/
|
||||
|
||||
Shop Pages:
|
||||
- http://localhost:8000/stores/wizamart/shop/
|
||||
- http://localhost:8000/stores/wizamart/shop/products
|
||||
- http://localhost:8000/stores/wizamart/shop/products/1
|
||||
- http://localhost:8000/stores/wizamart/shop/cart
|
||||
- http://localhost:8000/stores/orion/shop/
|
||||
- http://localhost:8000/stores/orion/shop/products
|
||||
- http://localhost:8000/stores/orion/shop/products/1
|
||||
- http://localhost:8000/stores/orion/shop/cart
|
||||
```
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
72
Makefile
72
Makefile
@@ -1,7 +1,7 @@
|
||||
# Wizamart Multi-Tenant E-Commerce Platform Makefile
|
||||
# Orion Multi-Tenant E-Commerce Platform Makefile
|
||||
# Cross-platform compatible (Windows & Linux)
|
||||
|
||||
.PHONY: install install-dev install-docs install-all dev test test-coverage lint format check docker-build docker-up docker-down clean help tailwind-install tailwind-dev tailwind-build tailwind-watch arch-check arch-check-file arch-check-object test-db-up test-db-down test-db-reset test-db-status celery-worker celery-beat celery-dev flower celery-status celery-purge urls
|
||||
.PHONY: install install-dev install-docs install-all dev test test-coverage lint format check docker-build docker-up docker-down clean help tailwind-install tailwind-dev tailwind-build tailwind-watch arch-check arch-check-file arch-check-object test-db-up test-db-down test-db-reset test-db-status celery-worker celery-beat celery-dev flower celery-status celery-purge urls infra-check test-affected test-affected-dry
|
||||
|
||||
# Detect OS
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@@ -44,7 +44,7 @@ setup: install-all migrate-up init-prod
|
||||
# =============================================================================
|
||||
|
||||
dev:
|
||||
$(PYTHON) -m uvicorn main:app --reload --host 0.0.0.0 --port 9999
|
||||
$(PYTHON) -m uvicorn main:app --reload --host 0.0.0.0 --port $(or $(API_PORT),8000)
|
||||
|
||||
# =============================================================================
|
||||
# DATABASE MIGRATIONS
|
||||
@@ -104,22 +104,19 @@ init-prod:
|
||||
@echo "Step 0/6: Ensuring database exists (running migrations)..."
|
||||
@$(PYTHON) -m alembic upgrade heads
|
||||
@echo ""
|
||||
@echo "Step 1/6: Creating admin user and platform settings..."
|
||||
@echo "Step 1/5: Creating admin user and platform settings..."
|
||||
$(PYTHON) scripts/seed/init_production.py
|
||||
@echo ""
|
||||
@echo "Step 2/6: Initializing log settings..."
|
||||
@echo "Step 2/5: Initializing log settings..."
|
||||
$(PYTHON) scripts/seed/init_log_settings.py
|
||||
@echo ""
|
||||
@echo "Step 3/6: Creating default CMS content pages..."
|
||||
@echo "Step 3/5: Creating default CMS content pages..."
|
||||
$(PYTHON) scripts/seed/create_default_content_pages.py
|
||||
@echo ""
|
||||
@echo "Step 4/6: Creating platform pages and landing..."
|
||||
$(PYTHON) scripts/seed/create_platform_pages.py
|
||||
@echo ""
|
||||
@echo "Step 5/6: Seeding email templates..."
|
||||
@echo "Step 4/5: Seeding email templates..."
|
||||
$(PYTHON) scripts/seed/seed_email_templates.py
|
||||
@echo ""
|
||||
@echo "Step 6/6: Seeding subscription tiers..."
|
||||
@echo "Step 5/5: Seeding subscription tiers..."
|
||||
@echo " (Handled by init_production.py Step 6)"
|
||||
@echo ""
|
||||
@echo "✅ Production initialization completed"
|
||||
@@ -132,7 +129,7 @@ seed-tiers:
|
||||
|
||||
# First-time installation - Complete setup with configuration validation
|
||||
platform-install:
|
||||
@echo "🚀 WIZAMART PLATFORM INSTALLATION"
|
||||
@echo "🚀 ORION PLATFORM INSTALLATION"
|
||||
@echo "=================================="
|
||||
$(PYTHON) scripts/seed/install.py
|
||||
|
||||
@@ -176,6 +173,12 @@ db-reset:
|
||||
$(PYTHON) -m alembic upgrade head
|
||||
@echo "Initializing production data..."
|
||||
$(PYTHON) scripts/seed/init_production.py
|
||||
@echo "Initializing log settings..."
|
||||
$(PYTHON) scripts/seed/init_log_settings.py
|
||||
@echo "Creating default CMS content pages..."
|
||||
$(PYTHON) scripts/seed/create_default_content_pages.py
|
||||
@echo "Seeding email templates..."
|
||||
$(PYTHON) scripts/seed/seed_email_templates.py
|
||||
@echo "Seeding demo data..."
|
||||
ifeq ($(DETECTED_OS),Windows)
|
||||
@set SEED_MODE=reset&& set FORCE_RESET=true&& $(PYTHON) scripts/seed/seed_demo.py
|
||||
@@ -195,10 +198,6 @@ create-cms-defaults:
|
||||
$(PYTHON) scripts/seed/create_default_content_pages.py
|
||||
@echo "✅ CMS defaults created"
|
||||
|
||||
create-platform-pages:
|
||||
@echo "🏠 Creating platform pages and landing..."
|
||||
$(PYTHON) scripts/seed/create_platform_pages.py
|
||||
@echo "✅ Platform pages created"
|
||||
|
||||
init-logging:
|
||||
@echo "📝 Initializing log settings..."
|
||||
@@ -235,7 +234,7 @@ test-db-status:
|
||||
# =============================================================================
|
||||
|
||||
# Test database URL
|
||||
TEST_DB_URL := postgresql://test_user:test_password@localhost:5433/wizamart_test
|
||||
TEST_DB_URL := postgresql://test_user:test_password@localhost:5433/orion_test
|
||||
|
||||
# Build pytest marker expression from module= and frontend= params
|
||||
MARKER_EXPR :=
|
||||
@@ -250,24 +249,21 @@ ifdef frontend
|
||||
endif
|
||||
endif
|
||||
|
||||
# All testpaths (central + module tests)
|
||||
TEST_PATHS := tests/ app/modules/tenancy/tests/ app/modules/catalog/tests/ app/modules/billing/tests/ app/modules/messaging/tests/ app/modules/orders/tests/ app/modules/customers/tests/ app/modules/marketplace/tests/ app/modules/inventory/tests/ app/modules/loyalty/tests/
|
||||
|
||||
test:
|
||||
@docker compose -f docker-compose.test.yml up -d 2>/dev/null || true
|
||||
@sleep 2
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) -m pytest $(TEST_PATHS) -v $(MARKER_EXPR)
|
||||
$(PYTHON) -m pytest -v $(MARKER_EXPR)
|
||||
|
||||
test-unit:
|
||||
@docker compose -f docker-compose.test.yml up -d 2>/dev/null || true
|
||||
@sleep 2
|
||||
ifdef module
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) -m pytest $(TEST_PATHS) -v -m "unit and $(module)"
|
||||
$(PYTHON) -m pytest -v -m "unit and $(module)"
|
||||
else
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) -m pytest $(TEST_PATHS) -v -m unit
|
||||
$(PYTHON) -m pytest -v -m unit
|
||||
endif
|
||||
|
||||
test-integration:
|
||||
@@ -275,29 +271,38 @@ test-integration:
|
||||
@sleep 2
|
||||
ifdef module
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) -m pytest $(TEST_PATHS) -v -m "integration and $(module)"
|
||||
$(PYTHON) -m pytest -v -m "integration and $(module)"
|
||||
else
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) -m pytest $(TEST_PATHS) -v -m integration
|
||||
$(PYTHON) -m pytest -v -m integration
|
||||
endif
|
||||
|
||||
test-coverage:
|
||||
@docker compose -f docker-compose.test.yml up -d 2>/dev/null || true
|
||||
@sleep 2
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) -m pytest $(TEST_PATHS) --cov=app --cov=models --cov=utils --cov=middleware --cov-report=html --cov-report=term-missing $(MARKER_EXPR)
|
||||
$(PYTHON) -m pytest --cov=app --cov=models --cov=utils --cov=middleware --cov-report=html --cov-report=term-missing $(MARKER_EXPR)
|
||||
|
||||
test-affected:
|
||||
@docker compose -f docker-compose.test.yml up -d 2>/dev/null || true
|
||||
@sleep 2
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) scripts/tests/run_affected_tests.py $(AFFECTED_ARGS)
|
||||
|
||||
test-affected-dry:
|
||||
@$(PYTHON) scripts/tests/run_affected_tests.py --dry-run $(AFFECTED_ARGS)
|
||||
|
||||
test-fast:
|
||||
@docker compose -f docker-compose.test.yml up -d 2>/dev/null || true
|
||||
@sleep 2
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) -m pytest $(TEST_PATHS) -v -m "not slow" $(MARKER_EXPR)
|
||||
$(PYTHON) -m pytest -v -m "not slow" $(MARKER_EXPR)
|
||||
|
||||
test-slow:
|
||||
@docker compose -f docker-compose.test.yml up -d 2>/dev/null || true
|
||||
@sleep 2
|
||||
TEST_DATABASE_URL="$(TEST_DB_URL)" \
|
||||
$(PYTHON) -m pytest $(TEST_PATHS) -v -m slow
|
||||
$(PYTHON) -m pytest -v -m slow
|
||||
|
||||
# =============================================================================
|
||||
# CODE QUALITY
|
||||
@@ -504,6 +509,10 @@ urls-prod:
|
||||
urls-check:
|
||||
@$(PYTHON) scripts/show_urls.py --check
|
||||
|
||||
infra-check:
|
||||
@echo "Running infrastructure verification..."
|
||||
bash scripts/verify-server.sh
|
||||
|
||||
check-env:
|
||||
@echo "Checking Python environment..."
|
||||
@echo "Detected OS: $(DETECTED_OS)"
|
||||
@@ -530,7 +539,7 @@ endif
|
||||
# =============================================================================
|
||||
|
||||
help:
|
||||
@echo "Wizamart Platform Development Commands"
|
||||
@echo "Orion Platform Development Commands"
|
||||
@echo ""
|
||||
@echo "=== SETUP ==="
|
||||
@echo " install - Install production dependencies"
|
||||
@@ -566,6 +575,8 @@ help:
|
||||
@echo " test-unit module=X - Run unit tests for module X"
|
||||
@echo " test-integration - Run integration tests only"
|
||||
@echo " test-coverage - Run tests with coverage"
|
||||
@echo " test-affected - Run tests for modules affected by changes"
|
||||
@echo " test-affected-dry - Show affected modules without running tests"
|
||||
@echo " test-fast - Run fast tests only"
|
||||
@echo " test frontend=storefront - Run storefront tests"
|
||||
@echo ""
|
||||
@@ -609,6 +620,7 @@ help:
|
||||
@echo " urls-dev - Show development URLs only"
|
||||
@echo " urls-prod - Show production URLs only"
|
||||
@echo " urls-check - Check dev URLs with curl (server must be running)"
|
||||
@echo " infra-check - Run infrastructure verification (verify-server.sh)"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " check-env - Check Python environment and OS"
|
||||
@echo ""
|
||||
@@ -681,4 +693,4 @@ help-db:
|
||||
@echo " - Email provider settings (SMTP/SendGrid/Mailgun/SES)"
|
||||
@echo " - ADMIN_PASSWORD (strong password)"
|
||||
@echo " 2. make platform-install # Validates + initializes"
|
||||
@echo " 3. DO NOT run seed-demo in production!"
|
||||
@echo " 3. DO NOT run seed-demo in production!"
|
||||
|
||||
12
README.md
12
README.md
@@ -34,7 +34,7 @@ This FastAPI application provides a complete ecommerce backend solution designed
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
wizamart/
|
||||
orion/
|
||||
├── main.py # FastAPI application entry point
|
||||
├── app/
|
||||
│ ├── core/
|
||||
@@ -179,8 +179,8 @@ make qa
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <wizamart-repo>
|
||||
cd wizamart-repo
|
||||
git clone <orion-repo>
|
||||
cd orion-repo
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
@@ -447,7 +447,7 @@ PROD002,"Super Gadget","A fantastic gadget",19.99,EUR,GadgetInc,9876543210987,Am
|
||||
- `POST /api/v1/marketplace/import-product` - Start CSV import
|
||||
- `GET /api/v1/marketplace/import-status/{job_id}` - Check import status
|
||||
- `GET /api/v1/marketplace/import-jobs` - List import jobs
|
||||
-
|
||||
-
|
||||
### Inventory Endpoints
|
||||
- `POST /api/v1/inventory` - Set inventory quantity
|
||||
- `POST /api/v1/inventory/add` - Add to inventory
|
||||
@@ -700,7 +700,7 @@ make help
|
||||
|
||||
This will display all available commands organized by category:
|
||||
- **Setup**: Installation and environment setup
|
||||
- **Development**: Development servers and workflows
|
||||
- **Development**: Development servers and workflows
|
||||
- **Documentation**: Documentation building and deployment
|
||||
- **Testing**: Various test execution options
|
||||
- **Code Quality**: Formatting, linting, and quality checks
|
||||
@@ -734,4 +734,4 @@ This will display all available commands organized by category:
|
||||
- **Health Check**: http://localhost:8000/health
|
||||
- **Version Info**: http://localhost:8000/
|
||||
|
||||
For issues and feature requests, please create an issue in the repository.
|
||||
For issues and feature requests, please create an issue in the repository.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
If you discover a security vulnerability in this project, please report it responsibly:
|
||||
|
||||
1. **Do not** open a public issue
|
||||
2. Email the security team at: security@wizamart.com
|
||||
2. Email the security team at: security@orion.lu
|
||||
3. Include:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Terminology Guide
|
||||
|
||||
This document defines the standard terminology used throughout the Wizamart codebase.
|
||||
This document defines the standard terminology used throughout the Orion codebase.
|
||||
|
||||
## Core Multi-Tenant Entities
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ Landing pages have been created for three stores with different templates.
|
||||
|
||||
## 📍 Test URLs
|
||||
|
||||
### 1. WizaMart - Modern Template
|
||||
### 1. Orion - Modern Template
|
||||
**Landing Page:**
|
||||
- http://localhost:8000/stores/wizamart/
|
||||
- http://localhost:8000/stores/orion/
|
||||
|
||||
**Shop Page:**
|
||||
- http://localhost:8000/stores/wizamart/shop/
|
||||
- http://localhost:8000/stores/orion/shop/
|
||||
|
||||
**What to expect:**
|
||||
- Full-screen hero section with animations
|
||||
@@ -93,8 +93,8 @@ db.close()
|
||||
"
|
||||
```
|
||||
|
||||
Then visit: http://localhost:8000/stores/wizamart/
|
||||
- Should automatically redirect to: http://localhost:8000/stores/wizamart/shop/
|
||||
Then visit: http://localhost:8000/stores/orion/
|
||||
- Should automatically redirect to: http://localhost:8000/stores/orion/shop/
|
||||
|
||||
---
|
||||
|
||||
@@ -111,17 +111,17 @@ Or programmatically:
|
||||
```python
|
||||
from scripts.create_landing_page import create_landing_page
|
||||
|
||||
# Change WizaMart to default template
|
||||
create_landing_page('wizamart', template='default')
|
||||
# Change Orion to default template
|
||||
create_landing_page('orion', template='default')
|
||||
|
||||
# Change to minimal
|
||||
create_landing_page('wizamart', template='minimal')
|
||||
create_landing_page('orion', template='minimal')
|
||||
|
||||
# Change to full
|
||||
create_landing_page('wizamart', template='full')
|
||||
create_landing_page('orion', template='full')
|
||||
|
||||
# Change back to modern
|
||||
create_landing_page('wizamart', template='modern')
|
||||
create_landing_page('orion', template='modern')
|
||||
```
|
||||
|
||||
---
|
||||
@@ -130,7 +130,7 @@ create_landing_page('wizamart', template='modern')
|
||||
|
||||
| Store | Subdomain | Template | Landing Page URL |
|
||||
|--------|-----------|----------|------------------|
|
||||
| WizaMart | wizamart | **modern** | http://localhost:8000/stores/wizamart/ |
|
||||
| Orion | orion | **modern** | http://localhost:8000/stores/orion/ |
|
||||
| Fashion Hub | fashionhub | **minimal** | http://localhost:8000/stores/fashionhub/ |
|
||||
| The Book Store | bookstore | **full** | http://localhost:8000/stores/bookstore/ |
|
||||
|
||||
@@ -146,7 +146,7 @@ sqlite3 letzshop.db "SELECT id, store_id, slug, title, template, is_published FR
|
||||
|
||||
Expected output:
|
||||
```
|
||||
8|1|landing|Welcome to WizaMart|modern|1
|
||||
8|1|landing|Welcome to Orion|modern|1
|
||||
9|2|landing|Fashion Hub - Style & Elegance|minimal|1
|
||||
10|3|landing|The Book Store - Your Literary Haven|full|1
|
||||
```
|
||||
@@ -180,7 +180,7 @@ Expected output:
|
||||
|
||||
## ✅ Success Checklist
|
||||
|
||||
- [ ] WizaMart landing page loads (modern template)
|
||||
- [ ] Orion landing page loads (modern template)
|
||||
- [ ] Fashion Hub landing page loads (minimal template)
|
||||
- [ ] Book Store landing page loads (full template)
|
||||
- [ ] "Shop Now" buttons work correctly
|
||||
|
||||
@@ -120,4 +120,4 @@
|
||||
won't be supported unlike CMS pages where he can create pretty much anything - btw let s make a note that number of pages should be defined in tiers)
|
||||
3/ custom domain setup: admin should be contacted to setup. same for SSL. custom emails. (this should be readonly for now)
|
||||
4/ API keys: stripe keys should be there
|
||||
5/ sections in settings page are not displayed properly: general , localization etc take 2/3 of the screen size
|
||||
5/ sections in settings page are not displayed properly: general , localization etc take 2/3 of the screen size
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
version_path_separator = space
|
||||
version_locations = alembic/versions app/modules/billing/migrations/versions app/modules/cart/migrations/versions app/modules/catalog/migrations/versions app/modules/cms/migrations/versions app/modules/customers/migrations/versions app/modules/dev_tools/migrations/versions app/modules/inventory/migrations/versions app/modules/loyalty/migrations/versions app/modules/marketplace/migrations/versions app/modules/messaging/migrations/versions app/modules/orders/migrations/versions app/modules/tenancy/migrations/versions
|
||||
version_locations = alembic/versions app/modules/billing/migrations/versions app/modules/cart/migrations/versions app/modules/catalog/migrations/versions app/modules/cms/migrations/versions app/modules/customers/migrations/versions app/modules/dev_tools/migrations/versions app/modules/hosting/migrations/versions app/modules/inventory/migrations/versions app/modules/loyalty/migrations/versions app/modules/marketplace/migrations/versions app/modules/messaging/migrations/versions app/modules/orders/migrations/versions app/modules/prospecting/migrations/versions app/modules/tenancy/migrations/versions
|
||||
# This will be overridden by alembic\env.py using settings.database_url
|
||||
sqlalchemy.url =
|
||||
# for PROD: sqlalchemy.url = postgresql://username:password@localhost:5432/ecommerce_db
|
||||
|
||||
@@ -1 +1 @@
|
||||
Generic single-database configuration.
|
||||
Generic single-database configuration.
|
||||
|
||||
@@ -81,7 +81,6 @@ try:
|
||||
from app.modules.billing.models import ( # noqa: F401
|
||||
AddOnProduct,
|
||||
BillingHistory,
|
||||
CapacitySnapshot,
|
||||
MerchantFeatureOverride,
|
||||
MerchantSubscription,
|
||||
StoreAddOn,
|
||||
@@ -90,7 +89,7 @@ try:
|
||||
TierFeatureLimit,
|
||||
)
|
||||
|
||||
print(" ✓ Billing models (9)")
|
||||
print(" ✓ Billing models (8)")
|
||||
except ImportError as e:
|
||||
_import_errors.append(f"billing: {e}")
|
||||
print(f" ✗ Billing models failed: {e}")
|
||||
@@ -263,6 +262,19 @@ except ImportError as e:
|
||||
_import_errors.append(f"dev_tools: {e}")
|
||||
print(f" ✗ Dev Tools models failed: {e}")
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# MONITORING MODULE (1 model)
|
||||
# ----------------------------------------------------------------------------
|
||||
try:
|
||||
from app.modules.monitoring.models import ( # noqa: F401
|
||||
CapacitySnapshot,
|
||||
)
|
||||
|
||||
print(" ✓ Monitoring models (1)")
|
||||
except ImportError as e:
|
||||
_import_errors.append(f"monitoring: {e}")
|
||||
print(f" ✗ Monitoring models failed: {e}")
|
||||
|
||||
# ============================================================================
|
||||
# SUMMARY
|
||||
# ============================================================================
|
||||
|
||||
26
alembic/versions/a44f4956cfb1_merge_heads.py
Normal file
26
alembic/versions/a44f4956cfb1_merge_heads.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""merge heads
|
||||
|
||||
Revision ID: a44f4956cfb1
|
||||
Revises: z_store_domain_platform_id, tenancy_001
|
||||
Create Date: 2026-02-17 16:10:36.287976
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a44f4956cfb1'
|
||||
down_revision: Union[str, None] = ('z_store_domain_platform_id', 'tenancy_001')
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -19,9 +19,9 @@ def upgrade() -> None:
|
||||
"platforms",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, index=True),
|
||||
sa.Column("code", sa.String(50), unique=True, nullable=False, index=True, comment="Unique platform identifier (e.g., 'oms', 'loyalty', 'sites')"),
|
||||
sa.Column("name", sa.String(100), nullable=False, comment="Display name (e.g., 'Wizamart OMS')"),
|
||||
sa.Column("name", sa.String(100), nullable=False, comment="Display name (e.g., 'Orion OMS')"),
|
||||
sa.Column("description", sa.Text(), nullable=True, comment="Platform description for admin/marketing purposes"),
|
||||
sa.Column("domain", sa.String(255), unique=True, nullable=True, index=True, comment="Production domain (e.g., 'oms.lu', 'loyalty.lu')"),
|
||||
sa.Column("domain", sa.String(255), unique=True, nullable=True, index=True, comment="Production domain (e.g., 'omsflow.lu', 'rewardflow.lu')"),
|
||||
sa.Column("path_prefix", sa.String(50), unique=True, nullable=True, index=True, comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)"),
|
||||
sa.Column("logo", sa.String(500), nullable=True, comment="Logo URL for light mode"),
|
||||
sa.Column("logo_dark", sa.String(500), nullable=True, comment="Logo URL for dark mode"),
|
||||
|
||||
35
alembic/versions/remove_store_platform_is_primary.py
Normal file
35
alembic/versions/remove_store_platform_is_primary.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Remove is_primary from store_platforms
|
||||
|
||||
The platform is always deterministic from the URL context (path in dev,
|
||||
subdomain/domain in prod) and the JWT carries token_platform_id.
|
||||
The is_primary column was a fallback picker that silently returned the
|
||||
wrong platform for multi-platform stores.
|
||||
|
||||
Revision ID: remove_is_primary_001
|
||||
Revises: billing_001
|
||||
Create Date: 2026-03-09
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "remove_is_primary_001"
|
||||
down_revision = "billing_001"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.drop_index("idx_store_platform_primary", table_name="store_platforms")
|
||||
op.drop_column("store_platforms", "is_primary")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.add_column(
|
||||
"store_platforms",
|
||||
sa.Column("is_primary", sa.Boolean(), nullable=False, server_default="false"),
|
||||
)
|
||||
op.create_index(
|
||||
"idx_store_platform_primary", "store_platforms", ["store_id", "is_primary"]
|
||||
)
|
||||
118
alembic/versions/softdelete_001_add_soft_delete.py
Normal file
118
alembic/versions/softdelete_001_add_soft_delete.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Add soft delete columns (deleted_at, deleted_by_id) to business-critical tables.
|
||||
|
||||
Also converts unique constraints on users.email, users.username,
|
||||
stores.store_code, stores.subdomain to partial unique indexes
|
||||
that only apply to non-deleted rows.
|
||||
|
||||
Revision ID: softdelete_001
|
||||
Revises: remove_is_primary_001, customers_002, dev_tools_002, orders_002, tenancy_004
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "softdelete_001"
|
||||
down_revision = (
|
||||
"remove_is_primary_001",
|
||||
"customers_002",
|
||||
"dev_tools_002",
|
||||
"orders_002",
|
||||
"tenancy_004",
|
||||
)
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# Tables receiving soft-delete columns
|
||||
SOFT_DELETE_TABLES = [
|
||||
"users",
|
||||
"merchants",
|
||||
"stores",
|
||||
"customers",
|
||||
"store_users",
|
||||
"orders",
|
||||
"products",
|
||||
"loyalty_programs",
|
||||
"loyalty_cards",
|
||||
]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ======================================================================
|
||||
# Step 1: Add deleted_at and deleted_by_id to all soft-delete tables
|
||||
# ======================================================================
|
||||
for table in SOFT_DELETE_TABLES:
|
||||
op.add_column(table, sa.Column("deleted_at", sa.DateTime(), nullable=True))
|
||||
op.add_column(
|
||||
table,
|
||||
sa.Column(
|
||||
"deleted_by_id",
|
||||
sa.Integer(),
|
||||
sa.ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
op.create_index(f"ix_{table}_deleted_at", table, ["deleted_at"])
|
||||
|
||||
# ======================================================================
|
||||
# Step 2: Replace simple unique constraints with partial unique indexes
|
||||
# (only enforce uniqueness among non-deleted rows)
|
||||
# ======================================================================
|
||||
|
||||
# users.email: drop old unique index, create partial
|
||||
op.drop_index("ix_users_email", table_name="users")
|
||||
op.execute(
|
||||
'CREATE UNIQUE INDEX uq_users_email_active ON users (email) '
|
||||
'WHERE deleted_at IS NULL'
|
||||
)
|
||||
# Keep a non-unique index for lookups on all rows (including deleted)
|
||||
op.create_index("ix_users_email", "users", ["email"])
|
||||
|
||||
# users.username: drop old unique index, create partial
|
||||
op.drop_index("ix_users_username", table_name="users")
|
||||
op.execute(
|
||||
'CREATE UNIQUE INDEX uq_users_username_active ON users (username) '
|
||||
'WHERE deleted_at IS NULL'
|
||||
)
|
||||
op.create_index("ix_users_username", "users", ["username"])
|
||||
|
||||
# stores.store_code: drop old unique index, create partial
|
||||
op.drop_index("ix_stores_store_code", table_name="stores")
|
||||
op.execute(
|
||||
'CREATE UNIQUE INDEX uq_stores_store_code_active ON stores (store_code) '
|
||||
'WHERE deleted_at IS NULL'
|
||||
)
|
||||
op.create_index("ix_stores_store_code", "stores", ["store_code"])
|
||||
|
||||
# stores.subdomain: drop old unique index, create partial
|
||||
op.drop_index("ix_stores_subdomain", table_name="stores")
|
||||
op.execute(
|
||||
'CREATE UNIQUE INDEX uq_stores_subdomain_active ON stores (subdomain) '
|
||||
'WHERE deleted_at IS NULL'
|
||||
)
|
||||
op.create_index("ix_stores_subdomain", "stores", ["subdomain"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Reverse partial unique indexes back to simple unique indexes
|
||||
op.drop_index("ix_stores_subdomain", table_name="stores")
|
||||
op.execute("DROP INDEX IF EXISTS uq_stores_subdomain_active")
|
||||
op.create_index("ix_stores_subdomain", "stores", ["subdomain"], unique=True)
|
||||
|
||||
op.drop_index("ix_stores_store_code", table_name="stores")
|
||||
op.execute("DROP INDEX IF EXISTS uq_stores_store_code_active")
|
||||
op.create_index("ix_stores_store_code", "stores", ["store_code"], unique=True)
|
||||
|
||||
op.drop_index("ix_users_username", table_name="users")
|
||||
op.execute("DROP INDEX IF EXISTS uq_users_username_active")
|
||||
op.create_index("ix_users_username", "users", ["username"], unique=True)
|
||||
|
||||
op.drop_index("ix_users_email", table_name="users")
|
||||
op.execute("DROP INDEX IF EXISTS uq_users_email_active")
|
||||
op.create_index("ix_users_email", "users", ["email"], unique=True)
|
||||
|
||||
# Remove soft-delete columns from all tables
|
||||
for table in reversed(SOFT_DELETE_TABLES):
|
||||
op.drop_index(f"ix_{table}_deleted_at", table_name=table)
|
||||
op.drop_column(table, "deleted_by_id")
|
||||
op.drop_column(table, "deleted_at")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""add unique constraints for custom_subdomain and store domain per platform
|
||||
|
||||
Revision ID: z_unique_subdomain_domain
|
||||
Revises: a44f4956cfb1
|
||||
Create Date: 2026-02-26
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "z_unique_subdomain_domain"
|
||||
down_revision = ("a44f4956cfb1", "tenancy_003")
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# StorePlatform: same custom_subdomain cannot be claimed twice on the same platform
|
||||
op.create_unique_constraint(
|
||||
"uq_custom_subdomain_platform",
|
||||
"store_platforms",
|
||||
["custom_subdomain", "platform_id"],
|
||||
)
|
||||
|
||||
# StoreDomain: a store can have at most one custom domain per platform
|
||||
op.create_unique_constraint(
|
||||
"uq_store_domain_platform",
|
||||
"store_domains",
|
||||
["store_id", "platform_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("uq_store_domain_platform", "store_domains", type_="unique")
|
||||
op.drop_constraint("uq_custom_subdomain_platform", "store_platforms", type_="unique")
|
||||
@@ -5,51 +5,52 @@ Revises: y3d4e5f6g7h8
|
||||
Create Date: 2026-01-11 16:44:59.070110
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '09d84a46530f'
|
||||
down_revision: Union[str, None] = 'y3d4e5f6g7h8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "09d84a46530f"
|
||||
down_revision: str | None = "y3d4e5f6g7h8"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Add celery_task_id column to job tracking tables for Celery integration."""
|
||||
# MarketplaceImportJob
|
||||
op.add_column('marketplace_import_jobs', sa.Column('celery_task_id', sa.String(length=255), nullable=True))
|
||||
op.create_index(op.f('ix_marketplace_import_jobs_celery_task_id'), 'marketplace_import_jobs', ['celery_task_id'], unique=False)
|
||||
op.add_column("marketplace_import_jobs", sa.Column("celery_task_id", sa.String(length=255), nullable=True))
|
||||
op.create_index(op.f("ix_marketplace_import_jobs_celery_task_id"), "marketplace_import_jobs", ["celery_task_id"], unique=False)
|
||||
|
||||
# LetzshopHistoricalImportJob
|
||||
op.add_column('letzshop_historical_import_jobs', sa.Column('celery_task_id', sa.String(length=255), nullable=True))
|
||||
op.create_index(op.f('ix_letzshop_historical_import_jobs_celery_task_id'), 'letzshop_historical_import_jobs', ['celery_task_id'], unique=False)
|
||||
op.add_column("letzshop_historical_import_jobs", sa.Column("celery_task_id", sa.String(length=255), nullable=True))
|
||||
op.create_index(op.f("ix_letzshop_historical_import_jobs_celery_task_id"), "letzshop_historical_import_jobs", ["celery_task_id"], unique=False)
|
||||
|
||||
# ArchitectureScan
|
||||
op.add_column('architecture_scans', sa.Column('celery_task_id', sa.String(length=255), nullable=True))
|
||||
op.create_index(op.f('ix_architecture_scans_celery_task_id'), 'architecture_scans', ['celery_task_id'], unique=False)
|
||||
op.add_column("architecture_scans", sa.Column("celery_task_id", sa.String(length=255), nullable=True))
|
||||
op.create_index(op.f("ix_architecture_scans_celery_task_id"), "architecture_scans", ["celery_task_id"], unique=False)
|
||||
|
||||
# TestRun
|
||||
op.add_column('test_runs', sa.Column('celery_task_id', sa.String(length=255), nullable=True))
|
||||
op.create_index(op.f('ix_test_runs_celery_task_id'), 'test_runs', ['celery_task_id'], unique=False)
|
||||
op.add_column("test_runs", sa.Column("celery_task_id", sa.String(length=255), nullable=True))
|
||||
op.create_index(op.f("ix_test_runs_celery_task_id"), "test_runs", ["celery_task_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove celery_task_id column from job tracking tables."""
|
||||
# TestRun
|
||||
op.drop_index(op.f('ix_test_runs_celery_task_id'), table_name='test_runs')
|
||||
op.drop_column('test_runs', 'celery_task_id')
|
||||
op.drop_index(op.f("ix_test_runs_celery_task_id"), table_name="test_runs")
|
||||
op.drop_column("test_runs", "celery_task_id")
|
||||
|
||||
# ArchitectureScan
|
||||
op.drop_index(op.f('ix_architecture_scans_celery_task_id'), table_name='architecture_scans')
|
||||
op.drop_column('architecture_scans', 'celery_task_id')
|
||||
op.drop_index(op.f("ix_architecture_scans_celery_task_id"), table_name="architecture_scans")
|
||||
op.drop_column("architecture_scans", "celery_task_id")
|
||||
|
||||
# LetzshopHistoricalImportJob
|
||||
op.drop_index(op.f('ix_letzshop_historical_import_jobs_celery_task_id'), table_name='letzshop_historical_import_jobs')
|
||||
op.drop_column('letzshop_historical_import_jobs', 'celery_task_id')
|
||||
op.drop_index(op.f("ix_letzshop_historical_import_jobs_celery_task_id"), table_name="letzshop_historical_import_jobs")
|
||||
op.drop_column("letzshop_historical_import_jobs", "celery_task_id")
|
||||
|
||||
# MarketplaceImportJob
|
||||
op.drop_index(op.f('ix_marketplace_import_jobs_celery_task_id'), table_name='marketplace_import_jobs')
|
||||
op.drop_column('marketplace_import_jobs', 'celery_task_id')
|
||||
op.drop_index(op.f("ix_marketplace_import_jobs_celery_task_id"), table_name="marketplace_import_jobs")
|
||||
op.drop_column("marketplace_import_jobs", "celery_task_id")
|
||||
|
||||
@@ -5,64 +5,64 @@ Revises: 7a7ce92593d5
|
||||
Create Date: 2025-11-29 12:44:55.427245
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0bd9ffaaced1'
|
||||
down_revision: Union[str, None] = '7a7ce92593d5'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "0bd9ffaaced1"
|
||||
down_revision: str | None = "7a7ce92593d5"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create application_logs table
|
||||
op.create_table(
|
||||
'application_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||
sa.Column('level', sa.String(length=20), nullable=False),
|
||||
sa.Column('logger_name', sa.String(length=200), nullable=False),
|
||||
sa.Column('module', sa.String(length=200), nullable=True),
|
||||
sa.Column('function_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('line_number', sa.Integer(), nullable=True),
|
||||
sa.Column('message', sa.Text(), nullable=False),
|
||||
sa.Column('exception_type', sa.String(length=200), nullable=True),
|
||||
sa.Column('exception_message', sa.Text(), nullable=True),
|
||||
sa.Column('stack_trace', sa.Text(), nullable=True),
|
||||
sa.Column('request_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=True),
|
||||
sa.Column('context', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
"application_logs",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("timestamp", sa.DateTime(), nullable=False),
|
||||
sa.Column("level", sa.String(length=20), nullable=False),
|
||||
sa.Column("logger_name", sa.String(length=200), nullable=False),
|
||||
sa.Column("module", sa.String(length=200), nullable=True),
|
||||
sa.Column("function_name", sa.String(length=100), nullable=True),
|
||||
sa.Column("line_number", sa.Integer(), nullable=True),
|
||||
sa.Column("message", sa.Text(), nullable=False),
|
||||
sa.Column("exception_type", sa.String(length=200), nullable=True),
|
||||
sa.Column("exception_message", sa.Text(), nullable=True),
|
||||
sa.Column("stack_trace", sa.Text(), nullable=True),
|
||||
sa.Column("request_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=True),
|
||||
sa.Column("context", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
|
||||
# Create indexes for better query performance
|
||||
op.create_index(op.f('ix_application_logs_id'), 'application_logs', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_application_logs_timestamp'), 'application_logs', ['timestamp'], unique=False)
|
||||
op.create_index(op.f('ix_application_logs_level'), 'application_logs', ['level'], unique=False)
|
||||
op.create_index(op.f('ix_application_logs_logger_name'), 'application_logs', ['logger_name'], unique=False)
|
||||
op.create_index(op.f('ix_application_logs_request_id'), 'application_logs', ['request_id'], unique=False)
|
||||
op.create_index(op.f('ix_application_logs_user_id'), 'application_logs', ['user_id'], unique=False)
|
||||
op.create_index(op.f('ix_application_logs_vendor_id'), 'application_logs', ['vendor_id'], unique=False)
|
||||
op.create_index(op.f("ix_application_logs_id"), "application_logs", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_application_logs_timestamp"), "application_logs", ["timestamp"], unique=False)
|
||||
op.create_index(op.f("ix_application_logs_level"), "application_logs", ["level"], unique=False)
|
||||
op.create_index(op.f("ix_application_logs_logger_name"), "application_logs", ["logger_name"], unique=False)
|
||||
op.create_index(op.f("ix_application_logs_request_id"), "application_logs", ["request_id"], unique=False)
|
||||
op.create_index(op.f("ix_application_logs_user_id"), "application_logs", ["user_id"], unique=False)
|
||||
op.create_index(op.f("ix_application_logs_vendor_id"), "application_logs", ["vendor_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop indexes
|
||||
op.drop_index(op.f('ix_application_logs_vendor_id'), table_name='application_logs')
|
||||
op.drop_index(op.f('ix_application_logs_user_id'), table_name='application_logs')
|
||||
op.drop_index(op.f('ix_application_logs_request_id'), table_name='application_logs')
|
||||
op.drop_index(op.f('ix_application_logs_logger_name'), table_name='application_logs')
|
||||
op.drop_index(op.f('ix_application_logs_level'), table_name='application_logs')
|
||||
op.drop_index(op.f('ix_application_logs_timestamp'), table_name='application_logs')
|
||||
op.drop_index(op.f('ix_application_logs_id'), table_name='application_logs')
|
||||
op.drop_index(op.f("ix_application_logs_vendor_id"), table_name="application_logs")
|
||||
op.drop_index(op.f("ix_application_logs_user_id"), table_name="application_logs")
|
||||
op.drop_index(op.f("ix_application_logs_request_id"), table_name="application_logs")
|
||||
op.drop_index(op.f("ix_application_logs_logger_name"), table_name="application_logs")
|
||||
op.drop_index(op.f("ix_application_logs_level"), table_name="application_logs")
|
||||
op.drop_index(op.f("ix_application_logs_timestamp"), table_name="application_logs")
|
||||
op.drop_index(op.f("ix_application_logs_id"), table_name="application_logs")
|
||||
|
||||
# Drop table
|
||||
op.drop_table('application_logs')
|
||||
op.drop_table("application_logs")
|
||||
|
||||
@@ -5,363 +5,363 @@ Revises: 09d84a46530f
|
||||
Create Date: 2026-01-13 19:38:45.423378
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql, sqlite
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.dialects import sqlite
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1b398cf45e85'
|
||||
down_revision: Union[str, None] = '09d84a46530f'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "1b398cf45e85"
|
||||
down_revision: str | None = "09d84a46530f"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('letzshop_vendor_cache',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('letzshop_id', sa.String(length=50), nullable=False),
|
||||
sa.Column('slug', sa.String(length=200), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('company_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('description_en', sa.Text(), nullable=True),
|
||||
sa.Column('description_fr', sa.Text(), nullable=True),
|
||||
sa.Column('description_de', sa.Text(), nullable=True),
|
||||
sa.Column('email', sa.String(length=255), nullable=True),
|
||||
sa.Column('phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('fax', sa.String(length=50), nullable=True),
|
||||
sa.Column('website', sa.String(length=500), nullable=True),
|
||||
sa.Column('street', sa.String(length=255), nullable=True),
|
||||
sa.Column('street_number', sa.String(length=50), nullable=True),
|
||||
sa.Column('city', sa.String(length=100), nullable=True),
|
||||
sa.Column('zipcode', sa.String(length=20), nullable=True),
|
||||
sa.Column('country_iso', sa.String(length=5), nullable=True),
|
||||
sa.Column('latitude', sa.String(length=20), nullable=True),
|
||||
sa.Column('longitude', sa.String(length=20), nullable=True),
|
||||
sa.Column('categories', sqlite.JSON(), nullable=True),
|
||||
sa.Column('background_image_url', sa.String(length=500), nullable=True),
|
||||
sa.Column('social_media_links', sqlite.JSON(), nullable=True),
|
||||
sa.Column('opening_hours_en', sa.Text(), nullable=True),
|
||||
sa.Column('opening_hours_fr', sa.Text(), nullable=True),
|
||||
sa.Column('opening_hours_de', sa.Text(), nullable=True),
|
||||
sa.Column('representative_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('representative_title', sa.String(length=100), nullable=True),
|
||||
sa.Column('claimed_by_vendor_id', sa.Integer(), nullable=True),
|
||||
sa.Column('claimed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('raw_data', sqlite.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['claimed_by_vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("letzshop_vendor_cache",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("letzshop_id", sa.String(length=50), nullable=False),
|
||||
sa.Column("slug", sa.String(length=200), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=False),
|
||||
sa.Column("company_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True),
|
||||
sa.Column("description_en", sa.Text(), nullable=True),
|
||||
sa.Column("description_fr", sa.Text(), nullable=True),
|
||||
sa.Column("description_de", sa.Text(), nullable=True),
|
||||
sa.Column("email", sa.String(length=255), nullable=True),
|
||||
sa.Column("phone", sa.String(length=50), nullable=True),
|
||||
sa.Column("fax", sa.String(length=50), nullable=True),
|
||||
sa.Column("website", sa.String(length=500), nullable=True),
|
||||
sa.Column("street", sa.String(length=255), nullable=True),
|
||||
sa.Column("street_number", sa.String(length=50), nullable=True),
|
||||
sa.Column("city", sa.String(length=100), nullable=True),
|
||||
sa.Column("zipcode", sa.String(length=20), nullable=True),
|
||||
sa.Column("country_iso", sa.String(length=5), nullable=True),
|
||||
sa.Column("latitude", sa.String(length=20), nullable=True),
|
||||
sa.Column("longitude", sa.String(length=20), nullable=True),
|
||||
sa.Column("categories", sqlite.JSON(), nullable=True),
|
||||
sa.Column("background_image_url", sa.String(length=500), nullable=True),
|
||||
sa.Column("social_media_links", sqlite.JSON(), nullable=True),
|
||||
sa.Column("opening_hours_en", sa.Text(), nullable=True),
|
||||
sa.Column("opening_hours_fr", sa.Text(), nullable=True),
|
||||
sa.Column("opening_hours_de", sa.Text(), nullable=True),
|
||||
sa.Column("representative_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("representative_title", sa.String(length=100), nullable=True),
|
||||
sa.Column("claimed_by_vendor_id", sa.Integer(), nullable=True),
|
||||
sa.Column("claimed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("raw_data", sqlite.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["claimed_by_vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index('idx_vendor_cache_active', 'letzshop_vendor_cache', ['is_active'], unique=False)
|
||||
op.create_index('idx_vendor_cache_city', 'letzshop_vendor_cache', ['city'], unique=False)
|
||||
op.create_index('idx_vendor_cache_claimed', 'letzshop_vendor_cache', ['claimed_by_vendor_id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_vendor_cache_claimed_by_vendor_id'), 'letzshop_vendor_cache', ['claimed_by_vendor_id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_vendor_cache_id'), 'letzshop_vendor_cache', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_vendor_cache_letzshop_id'), 'letzshop_vendor_cache', ['letzshop_id'], unique=True)
|
||||
op.create_index(op.f('ix_letzshop_vendor_cache_slug'), 'letzshop_vendor_cache', ['slug'], unique=True)
|
||||
op.drop_constraint('architecture_rules_rule_id_key', 'architecture_rules', type_='unique')
|
||||
op.alter_column('capacity_snapshots', 'created_at',
|
||||
op.create_index("idx_vendor_cache_active", "letzshop_vendor_cache", ["is_active"], unique=False)
|
||||
op.create_index("idx_vendor_cache_city", "letzshop_vendor_cache", ["city"], unique=False)
|
||||
op.create_index("idx_vendor_cache_claimed", "letzshop_vendor_cache", ["claimed_by_vendor_id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_vendor_cache_claimed_by_vendor_id"), "letzshop_vendor_cache", ["claimed_by_vendor_id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_vendor_cache_id"), "letzshop_vendor_cache", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_vendor_cache_letzshop_id"), "letzshop_vendor_cache", ["letzshop_id"], unique=True)
|
||||
op.create_index(op.f("ix_letzshop_vendor_cache_slug"), "letzshop_vendor_cache", ["slug"], unique=True)
|
||||
op.drop_constraint("architecture_rules_rule_id_key", "architecture_rules", type_="unique")
|
||||
op.alter_column("capacity_snapshots", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('capacity_snapshots', 'updated_at',
|
||||
existing_server_default=sa.text("now()"))
|
||||
op.alter_column("capacity_snapshots", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.create_index(op.f('ix_features_id'), 'features', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_features_minimum_tier_id'), 'features', ['minimum_tier_id'], unique=False)
|
||||
op.create_index('idx_inv_tx_order', 'inventory_transactions', ['order_id'], unique=False)
|
||||
op.alter_column('invoices', 'created_at',
|
||||
existing_server_default=sa.text("now()"))
|
||||
op.create_index(op.f("ix_features_id"), "features", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_features_minimum_tier_id"), "features", ["minimum_tier_id"], unique=False)
|
||||
op.create_index("idx_inv_tx_order", "inventory_transactions", ["order_id"], unique=False)
|
||||
op.alter_column("invoices", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('invoices', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("invoices", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('letzshop_fulfillment_queue', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("letzshop_fulfillment_queue", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('letzshop_fulfillment_queue', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("letzshop_fulfillment_queue", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('letzshop_sync_logs', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("letzshop_sync_logs", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('letzshop_sync_logs', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("letzshop_sync_logs", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('media_files', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("media_files", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('media_files', 'updated_at',
|
||||
existing_server_default=sa.text("now()"))
|
||||
op.alter_column("media_files", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False)
|
||||
op.alter_column('order_item_exceptions', 'created_at',
|
||||
op.alter_column("order_item_exceptions", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('order_item_exceptions', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("order_item_exceptions", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('order_items', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("order_items", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('order_items', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("order_items", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('orders', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("orders", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('orders', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("orders", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.drop_index('ix_password_reset_tokens_customer_id', table_name='password_reset_tokens')
|
||||
op.create_index(op.f('ix_password_reset_tokens_id'), 'password_reset_tokens', ['id'], unique=False)
|
||||
op.alter_column('product_media', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.drop_index("ix_password_reset_tokens_customer_id", table_name="password_reset_tokens")
|
||||
op.create_index(op.f("ix_password_reset_tokens_id"), "password_reset_tokens", ["id"], unique=False)
|
||||
op.alter_column("product_media", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('product_media', 'updated_at',
|
||||
existing_server_default=sa.text("now()"))
|
||||
op.alter_column("product_media", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=False)
|
||||
op.alter_column('products', 'is_digital',
|
||||
op.alter_column("products", "is_digital",
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('false'))
|
||||
op.alter_column('products', 'product_type',
|
||||
existing_server_default=sa.text("false"))
|
||||
op.alter_column("products", "product_type",
|
||||
existing_type=sa.VARCHAR(length=20),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text("'physical'::character varying"))
|
||||
op.drop_index('idx_product_is_digital', table_name='products')
|
||||
op.create_index(op.f('ix_products_is_digital'), 'products', ['is_digital'], unique=False)
|
||||
op.drop_constraint('uq_vendor_email_settings_vendor_id', 'vendor_email_settings', type_='unique')
|
||||
op.drop_index('ix_vendor_email_templates_lookup', table_name='vendor_email_templates')
|
||||
op.create_index(op.f('ix_vendor_email_templates_id'), 'vendor_email_templates', ['id'], unique=False)
|
||||
op.alter_column('vendor_invoice_settings', 'created_at',
|
||||
op.drop_index("idx_product_is_digital", table_name="products")
|
||||
op.create_index(op.f("ix_products_is_digital"), "products", ["is_digital"], unique=False)
|
||||
op.drop_constraint("uq_vendor_email_settings_vendor_id", "vendor_email_settings", type_="unique")
|
||||
op.drop_index("ix_vendor_email_templates_lookup", table_name="vendor_email_templates")
|
||||
op.create_index(op.f("ix_vendor_email_templates_id"), "vendor_email_templates", ["id"], unique=False)
|
||||
op.alter_column("vendor_invoice_settings", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('vendor_invoice_settings', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("vendor_invoice_settings", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.drop_constraint('vendor_invoice_settings_vendor_id_key', 'vendor_invoice_settings', type_='unique')
|
||||
op.alter_column('vendor_letzshop_credentials', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.drop_constraint("vendor_invoice_settings_vendor_id_key", "vendor_invoice_settings", type_="unique")
|
||||
op.alter_column("vendor_letzshop_credentials", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('vendor_letzshop_credentials', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("vendor_letzshop_credentials", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.drop_constraint('vendor_letzshop_credentials_vendor_id_key', 'vendor_letzshop_credentials', type_='unique')
|
||||
op.alter_column('vendor_subscriptions', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.drop_constraint("vendor_letzshop_credentials_vendor_id_key", "vendor_letzshop_credentials", type_="unique")
|
||||
op.alter_column("vendor_subscriptions", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('vendor_subscriptions', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("vendor_subscriptions", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.drop_constraint('vendor_subscriptions_vendor_id_key', 'vendor_subscriptions', type_='unique')
|
||||
op.drop_constraint('fk_vendor_subscriptions_tier_id', 'vendor_subscriptions', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'vendor_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
|
||||
op.alter_column('vendors', 'storefront_locale',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.drop_constraint("vendor_subscriptions_vendor_id_key", "vendor_subscriptions", type_="unique")
|
||||
op.drop_constraint("fk_vendor_subscriptions_tier_id", "vendor_subscriptions", type_="foreignkey")
|
||||
op.create_foreign_key(None, "vendor_subscriptions", "subscription_tiers", ["tier_id"], ["id"])
|
||||
op.alter_column("vendors", "storefront_locale",
|
||||
existing_type=sa.VARCHAR(length=10),
|
||||
comment=None,
|
||||
existing_comment='Currency/number formatting locale (NULL = inherit from platform)',
|
||||
existing_comment="Currency/number formatting locale (NULL = inherit from platform)",
|
||||
existing_nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('vendors', 'storefront_locale',
|
||||
op.alter_column("vendors", "storefront_locale",
|
||||
existing_type=sa.VARCHAR(length=10),
|
||||
comment='Currency/number formatting locale (NULL = inherit from platform)',
|
||||
comment="Currency/number formatting locale (NULL = inherit from platform)",
|
||||
existing_nullable=True)
|
||||
op.drop_constraint(None, 'vendor_subscriptions', type_='foreignkey')
|
||||
op.create_foreign_key('fk_vendor_subscriptions_tier_id', 'vendor_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], ondelete='SET NULL')
|
||||
op.create_unique_constraint('vendor_subscriptions_vendor_id_key', 'vendor_subscriptions', ['vendor_id'])
|
||||
op.alter_column('vendor_subscriptions', 'updated_at',
|
||||
op.drop_constraint(None, "vendor_subscriptions", type_="foreignkey")
|
||||
op.create_foreign_key("fk_vendor_subscriptions_tier_id", "vendor_subscriptions", "subscription_tiers", ["tier_id"], ["id"], ondelete="SET NULL")
|
||||
op.create_unique_constraint("vendor_subscriptions_vendor_id_key", "vendor_subscriptions", ["vendor_id"])
|
||||
op.alter_column("vendor_subscriptions", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('vendor_subscriptions', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("vendor_subscriptions", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.create_unique_constraint('vendor_letzshop_credentials_vendor_id_key', 'vendor_letzshop_credentials', ['vendor_id'])
|
||||
op.alter_column('vendor_letzshop_credentials', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.create_unique_constraint("vendor_letzshop_credentials_vendor_id_key", "vendor_letzshop_credentials", ["vendor_id"])
|
||||
op.alter_column("vendor_letzshop_credentials", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('vendor_letzshop_credentials', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("vendor_letzshop_credentials", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.create_unique_constraint('vendor_invoice_settings_vendor_id_key', 'vendor_invoice_settings', ['vendor_id'])
|
||||
op.alter_column('vendor_invoice_settings', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.create_unique_constraint("vendor_invoice_settings_vendor_id_key", "vendor_invoice_settings", ["vendor_id"])
|
||||
op.alter_column("vendor_invoice_settings", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('vendor_invoice_settings', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("vendor_invoice_settings", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.drop_index(op.f('ix_vendor_email_templates_id'), table_name='vendor_email_templates')
|
||||
op.create_index('ix_vendor_email_templates_lookup', 'vendor_email_templates', ['vendor_id', 'template_code', 'language'], unique=False)
|
||||
op.create_unique_constraint('uq_vendor_email_settings_vendor_id', 'vendor_email_settings', ['vendor_id'])
|
||||
op.drop_index(op.f('ix_products_is_digital'), table_name='products')
|
||||
op.create_index('idx_product_is_digital', 'products', ['is_digital'], unique=False)
|
||||
op.alter_column('products', 'product_type',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.drop_index(op.f("ix_vendor_email_templates_id"), table_name="vendor_email_templates")
|
||||
op.create_index("ix_vendor_email_templates_lookup", "vendor_email_templates", ["vendor_id", "template_code", "language"], unique=False)
|
||||
op.create_unique_constraint("uq_vendor_email_settings_vendor_id", "vendor_email_settings", ["vendor_id"])
|
||||
op.drop_index(op.f("ix_products_is_digital"), table_name="products")
|
||||
op.create_index("idx_product_is_digital", "products", ["is_digital"], unique=False)
|
||||
op.alter_column("products", "product_type",
|
||||
existing_type=sa.VARCHAR(length=20),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text("'physical'::character varying"))
|
||||
op.alter_column('products', 'is_digital',
|
||||
op.alter_column("products", "is_digital",
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text('false'))
|
||||
op.alter_column('product_media', 'updated_at',
|
||||
existing_server_default=sa.text("false"))
|
||||
op.alter_column("product_media", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True)
|
||||
op.alter_column('product_media', 'created_at',
|
||||
op.alter_column("product_media", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.drop_index(op.f('ix_password_reset_tokens_id'), table_name='password_reset_tokens')
|
||||
op.create_index('ix_password_reset_tokens_customer_id', 'password_reset_tokens', ['customer_id'], unique=False)
|
||||
op.alter_column('orders', 'updated_at',
|
||||
existing_server_default=sa.text("now()"))
|
||||
op.drop_index(op.f("ix_password_reset_tokens_id"), table_name="password_reset_tokens")
|
||||
op.create_index("ix_password_reset_tokens_customer_id", "password_reset_tokens", ["customer_id"], unique=False)
|
||||
op.alter_column("orders", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('orders', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("orders", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('order_items', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("order_items", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('order_items', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("order_items", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('order_item_exceptions', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("order_item_exceptions", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('order_item_exceptions', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("order_item_exceptions", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('media_files', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("media_files", "updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True)
|
||||
op.alter_column('media_files', 'created_at',
|
||||
op.alter_column("media_files", "created_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('letzshop_sync_logs', 'updated_at',
|
||||
existing_server_default=sa.text("now()"))
|
||||
op.alter_column("letzshop_sync_logs", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('letzshop_sync_logs', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("letzshop_sync_logs", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('letzshop_fulfillment_queue', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("letzshop_fulfillment_queue", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('letzshop_fulfillment_queue', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("letzshop_fulfillment_queue", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('invoices', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("invoices", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.alter_column('invoices', 'created_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.alter_column("invoices", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('CURRENT_TIMESTAMP'))
|
||||
op.drop_index('idx_inv_tx_order', table_name='inventory_transactions')
|
||||
op.drop_index(op.f('ix_features_minimum_tier_id'), table_name='features')
|
||||
op.drop_index(op.f('ix_features_id'), table_name='features')
|
||||
op.alter_column('capacity_snapshots', 'updated_at',
|
||||
existing_server_default=sa.text("CURRENT_TIMESTAMP"))
|
||||
op.drop_index("idx_inv_tx_order", table_name="inventory_transactions")
|
||||
op.drop_index(op.f("ix_features_minimum_tier_id"), table_name="features")
|
||||
op.drop_index(op.f("ix_features_id"), table_name="features")
|
||||
op.alter_column("capacity_snapshots", "updated_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.alter_column('capacity_snapshots', 'created_at',
|
||||
existing_server_default=sa.text("now()"))
|
||||
op.alter_column("capacity_snapshots", "created_at",
|
||||
existing_type=sa.DateTime(),
|
||||
type_=postgresql.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text('now()'))
|
||||
op.create_unique_constraint('architecture_rules_rule_id_key', 'architecture_rules', ['rule_id'])
|
||||
op.drop_index(op.f('ix_letzshop_vendor_cache_slug'), table_name='letzshop_vendor_cache')
|
||||
op.drop_index(op.f('ix_letzshop_vendor_cache_letzshop_id'), table_name='letzshop_vendor_cache')
|
||||
op.drop_index(op.f('ix_letzshop_vendor_cache_id'), table_name='letzshop_vendor_cache')
|
||||
op.drop_index(op.f('ix_letzshop_vendor_cache_claimed_by_vendor_id'), table_name='letzshop_vendor_cache')
|
||||
op.drop_index('idx_vendor_cache_claimed', table_name='letzshop_vendor_cache')
|
||||
op.drop_index('idx_vendor_cache_city', table_name='letzshop_vendor_cache')
|
||||
op.drop_index('idx_vendor_cache_active', table_name='letzshop_vendor_cache')
|
||||
op.drop_table('letzshop_vendor_cache')
|
||||
existing_server_default=sa.text("now()"))
|
||||
op.create_unique_constraint("architecture_rules_rule_id_key", "architecture_rules", ["rule_id"])
|
||||
op.drop_index(op.f("ix_letzshop_vendor_cache_slug"), table_name="letzshop_vendor_cache")
|
||||
op.drop_index(op.f("ix_letzshop_vendor_cache_letzshop_id"), table_name="letzshop_vendor_cache")
|
||||
op.drop_index(op.f("ix_letzshop_vendor_cache_id"), table_name="letzshop_vendor_cache")
|
||||
op.drop_index(op.f("ix_letzshop_vendor_cache_claimed_by_vendor_id"), table_name="letzshop_vendor_cache")
|
||||
op.drop_index("idx_vendor_cache_claimed", table_name="letzshop_vendor_cache")
|
||||
op.drop_index("idx_vendor_cache_city", table_name="letzshop_vendor_cache")
|
||||
op.drop_index("idx_vendor_cache_active", table_name="letzshop_vendor_cache")
|
||||
op.drop_table("letzshop_vendor_cache")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
@@ -5,53 +5,55 @@ Revises: cb88bc9b5f86
|
||||
Create Date: 2025-12-19 05:40:53.463341
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# Removed: from sqlalchemy.dialects import sqlite (using sa.JSON for PostgreSQL)
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '204273a59d73'
|
||||
down_revision: Union[str, None] = 'cb88bc9b5f86'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "204273a59d73"
|
||||
down_revision: str | None = "cb88bc9b5f86"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table('letzshop_historical_import_jobs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(length=50), nullable=False),
|
||||
sa.Column('current_phase', sa.String(length=20), nullable=True),
|
||||
sa.Column('current_page', sa.Integer(), nullable=True),
|
||||
sa.Column('total_pages', sa.Integer(), nullable=True),
|
||||
sa.Column('shipments_fetched', sa.Integer(), nullable=True),
|
||||
sa.Column('orders_processed', sa.Integer(), nullable=True),
|
||||
sa.Column('orders_imported', sa.Integer(), nullable=True),
|
||||
sa.Column('orders_updated', sa.Integer(), nullable=True),
|
||||
sa.Column('orders_skipped', sa.Integer(), nullable=True),
|
||||
sa.Column('products_matched', sa.Integer(), nullable=True),
|
||||
sa.Column('products_not_found', sa.Integer(), nullable=True),
|
||||
sa.Column('confirmed_stats', sa.JSON(), nullable=True),
|
||||
sa.Column('declined_stats', sa.JSON(), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("letzshop_historical_import_jobs",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("status", sa.String(length=50), nullable=False),
|
||||
sa.Column("current_phase", sa.String(length=20), nullable=True),
|
||||
sa.Column("current_page", sa.Integer(), nullable=True),
|
||||
sa.Column("total_pages", sa.Integer(), nullable=True),
|
||||
sa.Column("shipments_fetched", sa.Integer(), nullable=True),
|
||||
sa.Column("orders_processed", sa.Integer(), nullable=True),
|
||||
sa.Column("orders_imported", sa.Integer(), nullable=True),
|
||||
sa.Column("orders_updated", sa.Integer(), nullable=True),
|
||||
sa.Column("orders_skipped", sa.Integer(), nullable=True),
|
||||
sa.Column("products_matched", sa.Integer(), nullable=True),
|
||||
sa.Column("products_not_found", sa.Integer(), nullable=True),
|
||||
sa.Column("confirmed_stats", sa.JSON(), nullable=True),
|
||||
sa.Column("declined_stats", sa.JSON(), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index('idx_historical_import_vendor', 'letzshop_historical_import_jobs', ['vendor_id', 'status'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_historical_import_jobs_id'), 'letzshop_historical_import_jobs', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_historical_import_jobs_vendor_id'), 'letzshop_historical_import_jobs', ['vendor_id'], unique=False)
|
||||
op.create_index("idx_historical_import_vendor", "letzshop_historical_import_jobs", ["vendor_id", "status"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_historical_import_jobs_id"), "letzshop_historical_import_jobs", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_historical_import_jobs_vendor_id"), "letzshop_historical_import_jobs", ["vendor_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f('ix_letzshop_historical_import_jobs_vendor_id'), table_name='letzshop_historical_import_jobs')
|
||||
op.drop_index(op.f('ix_letzshop_historical_import_jobs_id'), table_name='letzshop_historical_import_jobs')
|
||||
op.drop_index('idx_historical_import_vendor', table_name='letzshop_historical_import_jobs')
|
||||
op.drop_table('letzshop_historical_import_jobs')
|
||||
op.drop_index(op.f("ix_letzshop_historical_import_jobs_vendor_id"), table_name="letzshop_historical_import_jobs")
|
||||
op.drop_index(op.f("ix_letzshop_historical_import_jobs_id"), table_name="letzshop_historical_import_jobs")
|
||||
op.drop_index("idx_historical_import_vendor", table_name="letzshop_historical_import_jobs")
|
||||
op.drop_table("letzshop_historical_import_jobs")
|
||||
|
||||
@@ -5,23 +5,23 @@ Revises: 204273a59d73
|
||||
Create Date: 2025-12-19 08:46:23.731912
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2362c2723a93'
|
||||
down_revision: Union[str, None] = '204273a59d73'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "2362c2723a93"
|
||||
down_revision: str | None = "204273a59d73"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add order_date column to letzshop_orders table
|
||||
op.add_column('letzshop_orders', sa.Column('order_date', sa.DateTime(timezone=True), nullable=True))
|
||||
op.add_column("letzshop_orders", sa.Column("order_date", sa.DateTime(timezone=True), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('letzshop_orders', 'order_date')
|
||||
op.drop_column("letzshop_orders", "order_date")
|
||||
|
||||
@@ -5,33 +5,33 @@ Revises: 9f3a25ea4991
|
||||
Create Date: 2025-12-03 22:26:02.161087
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '28d44d503cac'
|
||||
down_revision: Union[str, None] = '9f3a25ea4991'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "28d44d503cac"
|
||||
down_revision: str | None = "9f3a25ea4991"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add nullable contact fields to vendor table
|
||||
# These allow vendor-specific branding/identity, overriding company defaults
|
||||
op.add_column('vendors', sa.Column('contact_email', sa.String(255), nullable=True))
|
||||
op.add_column('vendors', sa.Column('contact_phone', sa.String(50), nullable=True))
|
||||
op.add_column('vendors', sa.Column('website', sa.String(255), nullable=True))
|
||||
op.add_column('vendors', sa.Column('business_address', sa.Text(), nullable=True))
|
||||
op.add_column('vendors', sa.Column('tax_number', sa.String(100), nullable=True))
|
||||
op.add_column("vendors", sa.Column("contact_email", sa.String(255), nullable=True))
|
||||
op.add_column("vendors", sa.Column("contact_phone", sa.String(50), nullable=True))
|
||||
op.add_column("vendors", sa.Column("website", sa.String(255), nullable=True))
|
||||
op.add_column("vendors", sa.Column("business_address", sa.Text(), nullable=True))
|
||||
op.add_column("vendors", sa.Column("tax_number", sa.String(100), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove contact fields from vendor table
|
||||
op.drop_column('vendors', 'tax_number')
|
||||
op.drop_column('vendors', 'business_address')
|
||||
op.drop_column('vendors', 'website')
|
||||
op.drop_column('vendors', 'contact_phone')
|
||||
op.drop_column('vendors', 'contact_email')
|
||||
op.drop_column("vendors", "tax_number")
|
||||
op.drop_column("vendors", "business_address")
|
||||
op.drop_column("vendors", "website")
|
||||
op.drop_column("vendors", "contact_phone")
|
||||
op.drop_column("vendors", "contact_email")
|
||||
|
||||
@@ -5,18 +5,20 @@ Revises: e1bfb453fbe9
|
||||
Create Date: 2025-12-25 18:29:34.167773
|
||||
|
||||
"""
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# Removed: from sqlalchemy.dialects import sqlite (using sa.JSON for PostgreSQL)
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2953ed10d22c'
|
||||
down_revision: Union[str, None] = 'e1bfb453fbe9'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "2953ed10d22c"
|
||||
down_revision: str | None = "e1bfb453fbe9"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -25,146 +27,146 @@ def upgrade() -> None:
|
||||
# =========================================================================
|
||||
|
||||
# subscription_tiers - Database-driven tier definitions
|
||||
op.create_table('subscription_tiers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=30), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('price_monthly_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('price_annual_cents', sa.Integer(), nullable=True),
|
||||
sa.Column('orders_per_month', sa.Integer(), nullable=True),
|
||||
sa.Column('products_limit', sa.Integer(), nullable=True),
|
||||
sa.Column('team_members', sa.Integer(), nullable=True),
|
||||
sa.Column('order_history_months', sa.Integer(), nullable=True),
|
||||
sa.Column('features', sa.JSON(), nullable=True),
|
||||
sa.Column('stripe_product_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('stripe_price_monthly_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('stripe_price_annual_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('display_order', sa.Integer(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('is_public', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("subscription_tiers",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("code", sa.String(length=30), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("price_monthly_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("price_annual_cents", sa.Integer(), nullable=True),
|
||||
sa.Column("orders_per_month", sa.Integer(), nullable=True),
|
||||
sa.Column("products_limit", sa.Integer(), nullable=True),
|
||||
sa.Column("team_members", sa.Integer(), nullable=True),
|
||||
sa.Column("order_history_months", sa.Integer(), nullable=True),
|
||||
sa.Column("features", sa.JSON(), nullable=True),
|
||||
sa.Column("stripe_product_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("stripe_price_monthly_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("stripe_price_annual_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("display_order", sa.Integer(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_public", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_subscription_tiers_code'), 'subscription_tiers', ['code'], unique=True)
|
||||
op.create_index(op.f('ix_subscription_tiers_id'), 'subscription_tiers', ['id'], unique=False)
|
||||
op.create_index(op.f("ix_subscription_tiers_code"), "subscription_tiers", ["code"], unique=True)
|
||||
op.create_index(op.f("ix_subscription_tiers_id"), "subscription_tiers", ["id"], unique=False)
|
||||
|
||||
# addon_products - Purchasable add-ons (domains, SSL, email)
|
||||
op.create_table('addon_products',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=50), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('category', sa.String(length=50), nullable=False),
|
||||
sa.Column('price_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('billing_period', sa.String(length=20), nullable=False),
|
||||
sa.Column('quantity_unit', sa.String(length=50), nullable=True),
|
||||
sa.Column('quantity_value', sa.Integer(), nullable=True),
|
||||
sa.Column('stripe_product_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('stripe_price_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('display_order', sa.Integer(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("addon_products",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("code", sa.String(length=50), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("category", sa.String(length=50), nullable=False),
|
||||
sa.Column("price_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("billing_period", sa.String(length=20), nullable=False),
|
||||
sa.Column("quantity_unit", sa.String(length=50), nullable=True),
|
||||
sa.Column("quantity_value", sa.Integer(), nullable=True),
|
||||
sa.Column("stripe_product_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("stripe_price_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("display_order", sa.Integer(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_addon_products_category'), 'addon_products', ['category'], unique=False)
|
||||
op.create_index(op.f('ix_addon_products_code'), 'addon_products', ['code'], unique=True)
|
||||
op.create_index(op.f('ix_addon_products_id'), 'addon_products', ['id'], unique=False)
|
||||
op.create_index(op.f("ix_addon_products_category"), "addon_products", ["category"], unique=False)
|
||||
op.create_index(op.f("ix_addon_products_code"), "addon_products", ["code"], unique=True)
|
||||
op.create_index(op.f("ix_addon_products_id"), "addon_products", ["id"], unique=False)
|
||||
|
||||
# billing_history - Invoice and payment history
|
||||
op.create_table('billing_history',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('stripe_invoice_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('stripe_payment_intent_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('invoice_number', sa.String(length=50), nullable=True),
|
||||
sa.Column('invoice_date', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('due_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('subtotal_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('tax_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('total_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('amount_paid_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('currency', sa.String(length=3), nullable=False),
|
||||
sa.Column('status', sa.String(length=20), nullable=False),
|
||||
sa.Column('invoice_pdf_url', sa.String(length=500), nullable=True),
|
||||
sa.Column('hosted_invoice_url', sa.String(length=500), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('line_items', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("billing_history",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("stripe_invoice_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("stripe_payment_intent_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("invoice_number", sa.String(length=50), nullable=True),
|
||||
sa.Column("invoice_date", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("due_date", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("subtotal_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("tax_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("total_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("amount_paid_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("currency", sa.String(length=3), nullable=False),
|
||||
sa.Column("status", sa.String(length=20), nullable=False),
|
||||
sa.Column("invoice_pdf_url", sa.String(length=500), nullable=True),
|
||||
sa.Column("hosted_invoice_url", sa.String(length=500), nullable=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("line_items", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index('idx_billing_status', 'billing_history', ['vendor_id', 'status'], unique=False)
|
||||
op.create_index('idx_billing_vendor_date', 'billing_history', ['vendor_id', 'invoice_date'], unique=False)
|
||||
op.create_index(op.f('ix_billing_history_id'), 'billing_history', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_billing_history_status'), 'billing_history', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_billing_history_stripe_invoice_id'), 'billing_history', ['stripe_invoice_id'], unique=True)
|
||||
op.create_index(op.f('ix_billing_history_vendor_id'), 'billing_history', ['vendor_id'], unique=False)
|
||||
op.create_index("idx_billing_status", "billing_history", ["vendor_id", "status"], unique=False)
|
||||
op.create_index("idx_billing_vendor_date", "billing_history", ["vendor_id", "invoice_date"], unique=False)
|
||||
op.create_index(op.f("ix_billing_history_id"), "billing_history", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_billing_history_status"), "billing_history", ["status"], unique=False)
|
||||
op.create_index(op.f("ix_billing_history_stripe_invoice_id"), "billing_history", ["stripe_invoice_id"], unique=True)
|
||||
op.create_index(op.f("ix_billing_history_vendor_id"), "billing_history", ["vendor_id"], unique=False)
|
||||
|
||||
# vendor_addons - Add-ons purchased by vendor
|
||||
op.create_table('vendor_addons',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('addon_product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(length=20), nullable=False),
|
||||
sa.Column('domain_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('quantity', sa.Integer(), nullable=False),
|
||||
sa.Column('stripe_subscription_item_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('period_start', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('period_end', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('cancelled_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['addon_product_id'], ['addon_products.id'], ),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("vendor_addons",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("addon_product_id", sa.Integer(), nullable=False),
|
||||
sa.Column("status", sa.String(length=20), nullable=False),
|
||||
sa.Column("domain_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("quantity", sa.Integer(), nullable=False),
|
||||
sa.Column("stripe_subscription_item_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("period_start", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("period_end", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("cancelled_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["addon_product_id"], ["addon_products.id"], ),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index('idx_vendor_addon_product', 'vendor_addons', ['vendor_id', 'addon_product_id'], unique=False)
|
||||
op.create_index('idx_vendor_addon_status', 'vendor_addons', ['vendor_id', 'status'], unique=False)
|
||||
op.create_index(op.f('ix_vendor_addons_addon_product_id'), 'vendor_addons', ['addon_product_id'], unique=False)
|
||||
op.create_index(op.f('ix_vendor_addons_domain_name'), 'vendor_addons', ['domain_name'], unique=False)
|
||||
op.create_index(op.f('ix_vendor_addons_id'), 'vendor_addons', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_vendor_addons_status'), 'vendor_addons', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_vendor_addons_vendor_id'), 'vendor_addons', ['vendor_id'], unique=False)
|
||||
op.create_index("idx_vendor_addon_product", "vendor_addons", ["vendor_id", "addon_product_id"], unique=False)
|
||||
op.create_index("idx_vendor_addon_status", "vendor_addons", ["vendor_id", "status"], unique=False)
|
||||
op.create_index(op.f("ix_vendor_addons_addon_product_id"), "vendor_addons", ["addon_product_id"], unique=False)
|
||||
op.create_index(op.f("ix_vendor_addons_domain_name"), "vendor_addons", ["domain_name"], unique=False)
|
||||
op.create_index(op.f("ix_vendor_addons_id"), "vendor_addons", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_vendor_addons_status"), "vendor_addons", ["status"], unique=False)
|
||||
op.create_index(op.f("ix_vendor_addons_vendor_id"), "vendor_addons", ["vendor_id"], unique=False)
|
||||
|
||||
# stripe_webhook_events - Webhook idempotency tracking
|
||||
op.create_table('stripe_webhook_events',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('event_id', sa.String(length=100), nullable=False),
|
||||
sa.Column('event_type', sa.String(length=100), nullable=False),
|
||||
sa.Column('status', sa.String(length=20), nullable=False),
|
||||
sa.Column('processed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('payload_encrypted', sa.Text(), nullable=True),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=True),
|
||||
sa.Column('subscription_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['subscription_id'], ['vendor_subscriptions.id'], ),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("stripe_webhook_events",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("event_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("event_type", sa.String(length=100), nullable=False),
|
||||
sa.Column("status", sa.String(length=20), nullable=False),
|
||||
sa.Column("processed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("payload_encrypted", sa.Text(), nullable=True),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=True),
|
||||
sa.Column("subscription_id", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["subscription_id"], ["vendor_subscriptions.id"], ),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index('idx_webhook_event_type_status', 'stripe_webhook_events', ['event_type', 'status'], unique=False)
|
||||
op.create_index(op.f('ix_stripe_webhook_events_event_id'), 'stripe_webhook_events', ['event_id'], unique=True)
|
||||
op.create_index(op.f('ix_stripe_webhook_events_event_type'), 'stripe_webhook_events', ['event_type'], unique=False)
|
||||
op.create_index(op.f('ix_stripe_webhook_events_id'), 'stripe_webhook_events', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_stripe_webhook_events_status'), 'stripe_webhook_events', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_stripe_webhook_events_subscription_id'), 'stripe_webhook_events', ['subscription_id'], unique=False)
|
||||
op.create_index(op.f('ix_stripe_webhook_events_vendor_id'), 'stripe_webhook_events', ['vendor_id'], unique=False)
|
||||
op.create_index("idx_webhook_event_type_status", "stripe_webhook_events", ["event_type", "status"], unique=False)
|
||||
op.create_index(op.f("ix_stripe_webhook_events_event_id"), "stripe_webhook_events", ["event_id"], unique=True)
|
||||
op.create_index(op.f("ix_stripe_webhook_events_event_type"), "stripe_webhook_events", ["event_type"], unique=False)
|
||||
op.create_index(op.f("ix_stripe_webhook_events_id"), "stripe_webhook_events", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_stripe_webhook_events_status"), "stripe_webhook_events", ["status"], unique=False)
|
||||
op.create_index(op.f("ix_stripe_webhook_events_subscription_id"), "stripe_webhook_events", ["subscription_id"], unique=False)
|
||||
op.create_index(op.f("ix_stripe_webhook_events_vendor_id"), "stripe_webhook_events", ["vendor_id"], unique=False)
|
||||
|
||||
# =========================================================================
|
||||
# Add new columns to vendor_subscriptions
|
||||
# =========================================================================
|
||||
op.add_column('vendor_subscriptions', sa.Column('stripe_price_id', sa.String(length=100), nullable=True))
|
||||
op.add_column('vendor_subscriptions', sa.Column('stripe_payment_method_id', sa.String(length=100), nullable=True))
|
||||
op.add_column('vendor_subscriptions', sa.Column('proration_behavior', sa.String(length=50), nullable=True))
|
||||
op.add_column('vendor_subscriptions', sa.Column('scheduled_tier_change', sa.String(length=30), nullable=True))
|
||||
op.add_column('vendor_subscriptions', sa.Column('scheduled_change_at', sa.DateTime(timezone=True), nullable=True))
|
||||
op.add_column('vendor_subscriptions', sa.Column('payment_retry_count', sa.Integer(), server_default='0', nullable=False))
|
||||
op.add_column('vendor_subscriptions', sa.Column('last_payment_error', sa.Text(), nullable=True))
|
||||
op.add_column("vendor_subscriptions", sa.Column("stripe_price_id", sa.String(length=100), nullable=True))
|
||||
op.add_column("vendor_subscriptions", sa.Column("stripe_payment_method_id", sa.String(length=100), nullable=True))
|
||||
op.add_column("vendor_subscriptions", sa.Column("proration_behavior", sa.String(length=50), nullable=True))
|
||||
op.add_column("vendor_subscriptions", sa.Column("scheduled_tier_change", sa.String(length=30), nullable=True))
|
||||
op.add_column("vendor_subscriptions", sa.Column("scheduled_change_at", sa.DateTime(timezone=True), nullable=True))
|
||||
op.add_column("vendor_subscriptions", sa.Column("payment_retry_count", sa.Integer(), server_default="0", nullable=False))
|
||||
op.add_column("vendor_subscriptions", sa.Column("last_payment_error", sa.Text(), nullable=True))
|
||||
|
||||
# =========================================================================
|
||||
# Seed subscription tiers
|
||||
@@ -172,106 +174,106 @@ def upgrade() -> None:
|
||||
now = datetime.utcnow()
|
||||
|
||||
subscription_tiers = sa.table(
|
||||
'subscription_tiers',
|
||||
sa.column('code', sa.String),
|
||||
sa.column('name', sa.String),
|
||||
sa.column('description', sa.Text),
|
||||
sa.column('price_monthly_cents', sa.Integer),
|
||||
sa.column('price_annual_cents', sa.Integer),
|
||||
sa.column('orders_per_month', sa.Integer),
|
||||
sa.column('products_limit', sa.Integer),
|
||||
sa.column('team_members', sa.Integer),
|
||||
sa.column('order_history_months', sa.Integer),
|
||||
sa.column('features', sa.JSON),
|
||||
sa.column('display_order', sa.Integer),
|
||||
sa.column('is_active', sa.Boolean),
|
||||
sa.column('is_public', sa.Boolean),
|
||||
sa.column('created_at', sa.DateTime),
|
||||
sa.column('updated_at', sa.DateTime),
|
||||
"subscription_tiers",
|
||||
sa.column("code", sa.String),
|
||||
sa.column("name", sa.String),
|
||||
sa.column("description", sa.Text),
|
||||
sa.column("price_monthly_cents", sa.Integer),
|
||||
sa.column("price_annual_cents", sa.Integer),
|
||||
sa.column("orders_per_month", sa.Integer),
|
||||
sa.column("products_limit", sa.Integer),
|
||||
sa.column("team_members", sa.Integer),
|
||||
sa.column("order_history_months", sa.Integer),
|
||||
sa.column("features", sa.JSON),
|
||||
sa.column("display_order", sa.Integer),
|
||||
sa.column("is_active", sa.Boolean),
|
||||
sa.column("is_public", sa.Boolean),
|
||||
sa.column("created_at", sa.DateTime),
|
||||
sa.column("updated_at", sa.DateTime),
|
||||
)
|
||||
|
||||
op.bulk_insert(subscription_tiers, [
|
||||
{
|
||||
'code': 'essential',
|
||||
'name': 'Essential',
|
||||
'description': 'Perfect for solo vendors getting started with Letzshop',
|
||||
'price_monthly_cents': 4900,
|
||||
'price_annual_cents': 49000,
|
||||
'orders_per_month': 100,
|
||||
'products_limit': 200,
|
||||
'team_members': 1,
|
||||
'order_history_months': 6,
|
||||
'features': ['letzshop_sync', 'inventory_basic', 'invoice_lu', 'customer_view'],
|
||||
'display_order': 1,
|
||||
'is_active': True,
|
||||
'is_public': True,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"code": "essential",
|
||||
"name": "Essential",
|
||||
"description": "Perfect for solo vendors getting started with Letzshop",
|
||||
"price_monthly_cents": 4900,
|
||||
"price_annual_cents": 49000,
|
||||
"orders_per_month": 100,
|
||||
"products_limit": 200,
|
||||
"team_members": 1,
|
||||
"order_history_months": 6,
|
||||
"features": ["letzshop_sync", "inventory_basic", "invoice_lu", "customer_view"],
|
||||
"display_order": 1,
|
||||
"is_active": True,
|
||||
"is_public": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
{
|
||||
'code': 'professional',
|
||||
'name': 'Professional',
|
||||
'description': 'For active multi-channel vendors shipping EU-wide',
|
||||
'price_monthly_cents': 9900,
|
||||
'price_annual_cents': 99000,
|
||||
'orders_per_month': 500,
|
||||
'products_limit': None,
|
||||
'team_members': 3,
|
||||
'order_history_months': 24,
|
||||
'features': [
|
||||
'letzshop_sync', 'inventory_locations', 'inventory_purchase_orders',
|
||||
'invoice_lu', 'invoice_eu_vat', 'customer_view', 'customer_export'
|
||||
"code": "professional",
|
||||
"name": "Professional",
|
||||
"description": "For active multi-channel vendors shipping EU-wide",
|
||||
"price_monthly_cents": 9900,
|
||||
"price_annual_cents": 99000,
|
||||
"orders_per_month": 500,
|
||||
"products_limit": None,
|
||||
"team_members": 3,
|
||||
"order_history_months": 24,
|
||||
"features": [
|
||||
"letzshop_sync", "inventory_locations", "inventory_purchase_orders",
|
||||
"invoice_lu", "invoice_eu_vat", "customer_view", "customer_export"
|
||||
],
|
||||
'display_order': 2,
|
||||
'is_active': True,
|
||||
'is_public': True,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"display_order": 2,
|
||||
"is_active": True,
|
||||
"is_public": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
{
|
||||
'code': 'business',
|
||||
'name': 'Business',
|
||||
'description': 'For high-volume vendors with teams and data-driven operations',
|
||||
'price_monthly_cents': 19900,
|
||||
'price_annual_cents': 199000,
|
||||
'orders_per_month': 2000,
|
||||
'products_limit': None,
|
||||
'team_members': 10,
|
||||
'order_history_months': None,
|
||||
'features': [
|
||||
'letzshop_sync', 'inventory_locations', 'inventory_purchase_orders',
|
||||
'invoice_lu', 'invoice_eu_vat', 'invoice_bulk', 'customer_view',
|
||||
'customer_export', 'analytics_dashboard', 'accounting_export',
|
||||
'api_access', 'automation_rules', 'team_roles'
|
||||
"code": "business",
|
||||
"name": "Business",
|
||||
"description": "For high-volume vendors with teams and data-driven operations",
|
||||
"price_monthly_cents": 19900,
|
||||
"price_annual_cents": 199000,
|
||||
"orders_per_month": 2000,
|
||||
"products_limit": None,
|
||||
"team_members": 10,
|
||||
"order_history_months": None,
|
||||
"features": [
|
||||
"letzshop_sync", "inventory_locations", "inventory_purchase_orders",
|
||||
"invoice_lu", "invoice_eu_vat", "invoice_bulk", "customer_view",
|
||||
"customer_export", "analytics_dashboard", "accounting_export",
|
||||
"api_access", "automation_rules", "team_roles"
|
||||
],
|
||||
'display_order': 3,
|
||||
'is_active': True,
|
||||
'is_public': True,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"display_order": 3,
|
||||
"is_active": True,
|
||||
"is_public": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
{
|
||||
'code': 'enterprise',
|
||||
'name': 'Enterprise',
|
||||
'description': 'Custom solutions for large operations and agencies',
|
||||
'price_monthly_cents': 39900,
|
||||
'price_annual_cents': None,
|
||||
'orders_per_month': None,
|
||||
'products_limit': None,
|
||||
'team_members': None,
|
||||
'order_history_months': None,
|
||||
'features': [
|
||||
'letzshop_sync', 'inventory_locations', 'inventory_purchase_orders',
|
||||
'invoice_lu', 'invoice_eu_vat', 'invoice_bulk', 'customer_view',
|
||||
'customer_export', 'analytics_dashboard', 'accounting_export',
|
||||
'api_access', 'automation_rules', 'team_roles', 'white_label',
|
||||
'multi_vendor', 'custom_integrations', 'sla_guarantee', 'dedicated_support'
|
||||
"code": "enterprise",
|
||||
"name": "Enterprise",
|
||||
"description": "Custom solutions for large operations and agencies",
|
||||
"price_monthly_cents": 39900,
|
||||
"price_annual_cents": None,
|
||||
"orders_per_month": None,
|
||||
"products_limit": None,
|
||||
"team_members": None,
|
||||
"order_history_months": None,
|
||||
"features": [
|
||||
"letzshop_sync", "inventory_locations", "inventory_purchase_orders",
|
||||
"invoice_lu", "invoice_eu_vat", "invoice_bulk", "customer_view",
|
||||
"customer_export", "analytics_dashboard", "accounting_export",
|
||||
"api_access", "automation_rules", "team_roles", "white_label",
|
||||
"multi_vendor", "custom_integrations", "sla_guarantee", "dedicated_support"
|
||||
],
|
||||
'display_order': 4,
|
||||
'is_active': True,
|
||||
'is_public': False,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"display_order": 4,
|
||||
"is_active": True,
|
||||
"is_public": False,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -279,141 +281,141 @@ def upgrade() -> None:
|
||||
# Seed add-on products
|
||||
# =========================================================================
|
||||
addon_products = sa.table(
|
||||
'addon_products',
|
||||
sa.column('code', sa.String),
|
||||
sa.column('name', sa.String),
|
||||
sa.column('description', sa.Text),
|
||||
sa.column('category', sa.String),
|
||||
sa.column('price_cents', sa.Integer),
|
||||
sa.column('billing_period', sa.String),
|
||||
sa.column('quantity_unit', sa.String),
|
||||
sa.column('quantity_value', sa.Integer),
|
||||
sa.column('display_order', sa.Integer),
|
||||
sa.column('is_active', sa.Boolean),
|
||||
sa.column('created_at', sa.DateTime),
|
||||
sa.column('updated_at', sa.DateTime),
|
||||
"addon_products",
|
||||
sa.column("code", sa.String),
|
||||
sa.column("name", sa.String),
|
||||
sa.column("description", sa.Text),
|
||||
sa.column("category", sa.String),
|
||||
sa.column("price_cents", sa.Integer),
|
||||
sa.column("billing_period", sa.String),
|
||||
sa.column("quantity_unit", sa.String),
|
||||
sa.column("quantity_value", sa.Integer),
|
||||
sa.column("display_order", sa.Integer),
|
||||
sa.column("is_active", sa.Boolean),
|
||||
sa.column("created_at", sa.DateTime),
|
||||
sa.column("updated_at", sa.DateTime),
|
||||
)
|
||||
|
||||
op.bulk_insert(addon_products, [
|
||||
{
|
||||
'code': 'domain',
|
||||
'name': 'Custom Domain',
|
||||
'description': 'Connect your own domain with SSL certificate included',
|
||||
'category': 'domain',
|
||||
'price_cents': 1500,
|
||||
'billing_period': 'annual',
|
||||
'quantity_unit': None,
|
||||
'quantity_value': None,
|
||||
'display_order': 1,
|
||||
'is_active': True,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"code": "domain",
|
||||
"name": "Custom Domain",
|
||||
"description": "Connect your own domain with SSL certificate included",
|
||||
"category": "domain",
|
||||
"price_cents": 1500,
|
||||
"billing_period": "annual",
|
||||
"quantity_unit": None,
|
||||
"quantity_value": None,
|
||||
"display_order": 1,
|
||||
"is_active": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
{
|
||||
'code': 'email_5',
|
||||
'name': '5 Email Addresses',
|
||||
'description': 'Professional email addresses on your domain',
|
||||
'category': 'email',
|
||||
'price_cents': 500,
|
||||
'billing_period': 'monthly',
|
||||
'quantity_unit': 'emails',
|
||||
'quantity_value': 5,
|
||||
'display_order': 2,
|
||||
'is_active': True,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"code": "email_5",
|
||||
"name": "5 Email Addresses",
|
||||
"description": "Professional email addresses on your domain",
|
||||
"category": "email",
|
||||
"price_cents": 500,
|
||||
"billing_period": "monthly",
|
||||
"quantity_unit": "emails",
|
||||
"quantity_value": 5,
|
||||
"display_order": 2,
|
||||
"is_active": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
{
|
||||
'code': 'email_10',
|
||||
'name': '10 Email Addresses',
|
||||
'description': 'Professional email addresses on your domain',
|
||||
'category': 'email',
|
||||
'price_cents': 900,
|
||||
'billing_period': 'monthly',
|
||||
'quantity_unit': 'emails',
|
||||
'quantity_value': 10,
|
||||
'display_order': 3,
|
||||
'is_active': True,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"code": "email_10",
|
||||
"name": "10 Email Addresses",
|
||||
"description": "Professional email addresses on your domain",
|
||||
"category": "email",
|
||||
"price_cents": 900,
|
||||
"billing_period": "monthly",
|
||||
"quantity_unit": "emails",
|
||||
"quantity_value": 10,
|
||||
"display_order": 3,
|
||||
"is_active": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
{
|
||||
'code': 'email_25',
|
||||
'name': '25 Email Addresses',
|
||||
'description': 'Professional email addresses on your domain',
|
||||
'category': 'email',
|
||||
'price_cents': 1900,
|
||||
'billing_period': 'monthly',
|
||||
'quantity_unit': 'emails',
|
||||
'quantity_value': 25,
|
||||
'display_order': 4,
|
||||
'is_active': True,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"code": "email_25",
|
||||
"name": "25 Email Addresses",
|
||||
"description": "Professional email addresses on your domain",
|
||||
"category": "email",
|
||||
"price_cents": 1900,
|
||||
"billing_period": "monthly",
|
||||
"quantity_unit": "emails",
|
||||
"quantity_value": 25,
|
||||
"display_order": 4,
|
||||
"is_active": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
{
|
||||
'code': 'storage_10gb',
|
||||
'name': 'Additional Storage (10GB)',
|
||||
'description': 'Extra storage for product images and files',
|
||||
'category': 'storage',
|
||||
'price_cents': 500,
|
||||
'billing_period': 'monthly',
|
||||
'quantity_unit': 'GB',
|
||||
'quantity_value': 10,
|
||||
'display_order': 5,
|
||||
'is_active': True,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
"code": "storage_10gb",
|
||||
"name": "Additional Storage (10GB)",
|
||||
"description": "Extra storage for product images and files",
|
||||
"category": "storage",
|
||||
"price_cents": 500,
|
||||
"billing_period": "monthly",
|
||||
"quantity_unit": "GB",
|
||||
"quantity_value": 10,
|
||||
"display_order": 5,
|
||||
"is_active": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove new columns from vendor_subscriptions
|
||||
op.drop_column('vendor_subscriptions', 'last_payment_error')
|
||||
op.drop_column('vendor_subscriptions', 'payment_retry_count')
|
||||
op.drop_column('vendor_subscriptions', 'scheduled_change_at')
|
||||
op.drop_column('vendor_subscriptions', 'scheduled_tier_change')
|
||||
op.drop_column('vendor_subscriptions', 'proration_behavior')
|
||||
op.drop_column('vendor_subscriptions', 'stripe_payment_method_id')
|
||||
op.drop_column('vendor_subscriptions', 'stripe_price_id')
|
||||
op.drop_column("vendor_subscriptions", "last_payment_error")
|
||||
op.drop_column("vendor_subscriptions", "payment_retry_count")
|
||||
op.drop_column("vendor_subscriptions", "scheduled_change_at")
|
||||
op.drop_column("vendor_subscriptions", "scheduled_tier_change")
|
||||
op.drop_column("vendor_subscriptions", "proration_behavior")
|
||||
op.drop_column("vendor_subscriptions", "stripe_payment_method_id")
|
||||
op.drop_column("vendor_subscriptions", "stripe_price_id")
|
||||
|
||||
# Drop stripe_webhook_events
|
||||
op.drop_index(op.f('ix_stripe_webhook_events_vendor_id'), table_name='stripe_webhook_events')
|
||||
op.drop_index(op.f('ix_stripe_webhook_events_subscription_id'), table_name='stripe_webhook_events')
|
||||
op.drop_index(op.f('ix_stripe_webhook_events_status'), table_name='stripe_webhook_events')
|
||||
op.drop_index(op.f('ix_stripe_webhook_events_id'), table_name='stripe_webhook_events')
|
||||
op.drop_index(op.f('ix_stripe_webhook_events_event_type'), table_name='stripe_webhook_events')
|
||||
op.drop_index(op.f('ix_stripe_webhook_events_event_id'), table_name='stripe_webhook_events')
|
||||
op.drop_index('idx_webhook_event_type_status', table_name='stripe_webhook_events')
|
||||
op.drop_table('stripe_webhook_events')
|
||||
op.drop_index(op.f("ix_stripe_webhook_events_vendor_id"), table_name="stripe_webhook_events")
|
||||
op.drop_index(op.f("ix_stripe_webhook_events_subscription_id"), table_name="stripe_webhook_events")
|
||||
op.drop_index(op.f("ix_stripe_webhook_events_status"), table_name="stripe_webhook_events")
|
||||
op.drop_index(op.f("ix_stripe_webhook_events_id"), table_name="stripe_webhook_events")
|
||||
op.drop_index(op.f("ix_stripe_webhook_events_event_type"), table_name="stripe_webhook_events")
|
||||
op.drop_index(op.f("ix_stripe_webhook_events_event_id"), table_name="stripe_webhook_events")
|
||||
op.drop_index("idx_webhook_event_type_status", table_name="stripe_webhook_events")
|
||||
op.drop_table("stripe_webhook_events")
|
||||
|
||||
# Drop vendor_addons
|
||||
op.drop_index(op.f('ix_vendor_addons_vendor_id'), table_name='vendor_addons')
|
||||
op.drop_index(op.f('ix_vendor_addons_status'), table_name='vendor_addons')
|
||||
op.drop_index(op.f('ix_vendor_addons_id'), table_name='vendor_addons')
|
||||
op.drop_index(op.f('ix_vendor_addons_domain_name'), table_name='vendor_addons')
|
||||
op.drop_index(op.f('ix_vendor_addons_addon_product_id'), table_name='vendor_addons')
|
||||
op.drop_index('idx_vendor_addon_status', table_name='vendor_addons')
|
||||
op.drop_index('idx_vendor_addon_product', table_name='vendor_addons')
|
||||
op.drop_table('vendor_addons')
|
||||
op.drop_index(op.f("ix_vendor_addons_vendor_id"), table_name="vendor_addons")
|
||||
op.drop_index(op.f("ix_vendor_addons_status"), table_name="vendor_addons")
|
||||
op.drop_index(op.f("ix_vendor_addons_id"), table_name="vendor_addons")
|
||||
op.drop_index(op.f("ix_vendor_addons_domain_name"), table_name="vendor_addons")
|
||||
op.drop_index(op.f("ix_vendor_addons_addon_product_id"), table_name="vendor_addons")
|
||||
op.drop_index("idx_vendor_addon_status", table_name="vendor_addons")
|
||||
op.drop_index("idx_vendor_addon_product", table_name="vendor_addons")
|
||||
op.drop_table("vendor_addons")
|
||||
|
||||
# Drop billing_history
|
||||
op.drop_index(op.f('ix_billing_history_vendor_id'), table_name='billing_history')
|
||||
op.drop_index(op.f('ix_billing_history_stripe_invoice_id'), table_name='billing_history')
|
||||
op.drop_index(op.f('ix_billing_history_status'), table_name='billing_history')
|
||||
op.drop_index(op.f('ix_billing_history_id'), table_name='billing_history')
|
||||
op.drop_index('idx_billing_vendor_date', table_name='billing_history')
|
||||
op.drop_index('idx_billing_status', table_name='billing_history')
|
||||
op.drop_table('billing_history')
|
||||
op.drop_index(op.f("ix_billing_history_vendor_id"), table_name="billing_history")
|
||||
op.drop_index(op.f("ix_billing_history_stripe_invoice_id"), table_name="billing_history")
|
||||
op.drop_index(op.f("ix_billing_history_status"), table_name="billing_history")
|
||||
op.drop_index(op.f("ix_billing_history_id"), table_name="billing_history")
|
||||
op.drop_index("idx_billing_vendor_date", table_name="billing_history")
|
||||
op.drop_index("idx_billing_status", table_name="billing_history")
|
||||
op.drop_table("billing_history")
|
||||
|
||||
# Drop addon_products
|
||||
op.drop_index(op.f('ix_addon_products_id'), table_name='addon_products')
|
||||
op.drop_index(op.f('ix_addon_products_code'), table_name='addon_products')
|
||||
op.drop_index(op.f('ix_addon_products_category'), table_name='addon_products')
|
||||
op.drop_table('addon_products')
|
||||
op.drop_index(op.f("ix_addon_products_id"), table_name="addon_products")
|
||||
op.drop_index(op.f("ix_addon_products_code"), table_name="addon_products")
|
||||
op.drop_index(op.f("ix_addon_products_category"), table_name="addon_products")
|
||||
op.drop_table("addon_products")
|
||||
|
||||
# Drop subscription_tiers
|
||||
op.drop_index(op.f('ix_subscription_tiers_id'), table_name='subscription_tiers')
|
||||
op.drop_index(op.f('ix_subscription_tiers_code'), table_name='subscription_tiers')
|
||||
op.drop_table('subscription_tiers')
|
||||
op.drop_index(op.f("ix_subscription_tiers_id"), table_name="subscription_tiers")
|
||||
op.drop_index(op.f("ix_subscription_tiers_code"), table_name="subscription_tiers")
|
||||
op.drop_table("subscription_tiers")
|
||||
|
||||
@@ -9,36 +9,36 @@ Adds:
|
||||
- vendors.letzshop_vendor_slug - Letzshop shop URL slug
|
||||
- vendor_subscriptions.card_collected_at - Track when card was collected for trial
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '404b3e2d2865'
|
||||
down_revision: Union[str, None] = 'l0a1b2c3d4e5'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "404b3e2d2865"
|
||||
down_revision: str | None = "l0a1b2c3d4e5"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add Letzshop vendor identity fields to vendors table
|
||||
op.add_column('vendors', sa.Column('letzshop_vendor_id', sa.String(length=100), nullable=True))
|
||||
op.add_column('vendors', sa.Column('letzshop_vendor_slug', sa.String(length=200), nullable=True))
|
||||
op.create_index(op.f('ix_vendors_letzshop_vendor_id'), 'vendors', ['letzshop_vendor_id'], unique=True)
|
||||
op.create_index(op.f('ix_vendors_letzshop_vendor_slug'), 'vendors', ['letzshop_vendor_slug'], unique=False)
|
||||
op.add_column("vendors", sa.Column("letzshop_vendor_id", sa.String(length=100), nullable=True))
|
||||
op.add_column("vendors", sa.Column("letzshop_vendor_slug", sa.String(length=200), nullable=True))
|
||||
op.create_index(op.f("ix_vendors_letzshop_vendor_id"), "vendors", ["letzshop_vendor_id"], unique=True)
|
||||
op.create_index(op.f("ix_vendors_letzshop_vendor_slug"), "vendors", ["letzshop_vendor_slug"], unique=False)
|
||||
|
||||
# Add card collection tracking to vendor_subscriptions
|
||||
op.add_column('vendor_subscriptions', sa.Column('card_collected_at', sa.DateTime(timezone=True), nullable=True))
|
||||
op.add_column("vendor_subscriptions", sa.Column("card_collected_at", sa.DateTime(timezone=True), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove card collection tracking from vendor_subscriptions
|
||||
op.drop_column('vendor_subscriptions', 'card_collected_at')
|
||||
op.drop_column("vendor_subscriptions", "card_collected_at")
|
||||
|
||||
# Remove Letzshop vendor identity fields from vendors
|
||||
op.drop_index(op.f('ix_vendors_letzshop_vendor_slug'), table_name='vendors')
|
||||
op.drop_index(op.f('ix_vendors_letzshop_vendor_id'), table_name='vendors')
|
||||
op.drop_column('vendors', 'letzshop_vendor_slug')
|
||||
op.drop_column('vendors', 'letzshop_vendor_id')
|
||||
op.drop_index(op.f("ix_vendors_letzshop_vendor_slug"), table_name="vendors")
|
||||
op.drop_index(op.f("ix_vendors_letzshop_vendor_id"), table_name="vendors")
|
||||
op.drop_column("vendors", "letzshop_vendor_slug")
|
||||
op.drop_column("vendors", "letzshop_vendor_id")
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2025-10-27 22:28:33.137564
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -14,9 +14,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "4951b2e50581"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,27 +5,27 @@ Revises: d2e3f4a5b6c7
|
||||
Create Date: 2025-12-20 18:07:51.144136
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '55b92e155566'
|
||||
down_revision: Union[str, None] = 'd2e3f4a5b6c7'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "55b92e155566"
|
||||
down_revision: str | None = "d2e3f4a5b6c7"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add new tracking fields to orders table
|
||||
op.add_column('orders', sa.Column('tracking_url', sa.String(length=500), nullable=True))
|
||||
op.add_column('orders', sa.Column('shipment_number', sa.String(length=100), nullable=True))
|
||||
op.add_column('orders', sa.Column('shipping_carrier', sa.String(length=50), nullable=True))
|
||||
op.add_column("orders", sa.Column("tracking_url", sa.String(length=500), nullable=True))
|
||||
op.add_column("orders", sa.Column("shipment_number", sa.String(length=100), nullable=True))
|
||||
op.add_column("orders", sa.Column("shipping_carrier", sa.String(length=50), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('orders', 'shipping_carrier')
|
||||
op.drop_column('orders', 'shipment_number')
|
||||
op.drop_column('orders', 'tracking_url')
|
||||
op.drop_column("orders", "shipping_carrier")
|
||||
op.drop_column("orders", "shipment_number")
|
||||
op.drop_column("orders", "tracking_url")
|
||||
|
||||
@@ -5,17 +5,17 @@ Revises: d0325d7c0f25
|
||||
Create Date: 2025-12-01 20:30:06.158027
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '5818330181a5'
|
||||
down_revision: Union[str, None] = 'd0325d7c0f25'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "5818330181a5"
|
||||
down_revision: str | None = "d0325d7c0f25"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -29,8 +29,8 @@ def upgrade() -> None:
|
||||
This allows one company owner to manage multiple vendor brands.
|
||||
"""
|
||||
# Use batch operations for SQLite compatibility
|
||||
with op.batch_alter_table('vendors', schema=None) as batch_op:
|
||||
batch_op.alter_column('owner_user_id',
|
||||
with op.batch_alter_table("vendors", schema=None) as batch_op:
|
||||
batch_op.alter_column("owner_user_id",
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
|
||||
@@ -42,7 +42,7 @@ def downgrade() -> None:
|
||||
WARNING: This will fail if there are vendors without owner_user_id!
|
||||
"""
|
||||
# Use batch operations for SQLite compatibility
|
||||
with op.batch_alter_table('vendors', schema=None) as batch_op:
|
||||
batch_op.alter_column('owner_user_id',
|
||||
with op.batch_alter_table("vendors", schema=None) as batch_op:
|
||||
batch_op.alter_column("owner_user_id",
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2025-11-22 15:16:13.213613
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -14,9 +14,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "72aa309d4007"
|
||||
down_revision: Union[str, None] = "fef1d20ce8b4"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "fef1d20ce8b4"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -6,18 +6,17 @@ Create Date: 2025-11-28 09:21:16.545203
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "7a7ce92593d5"
|
||||
down_revision: Union[str, None] = "a2064e1dfcd4"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "a2064e1dfcd4"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,99 +5,99 @@ Revises: b4c5d6e7f8a9
|
||||
Create Date: 2025-12-12 22:48:09.501172
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '82ea1b4a3ccb'
|
||||
down_revision: Union[str, None] = 'b4c5d6e7f8a9'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "82ea1b4a3ccb"
|
||||
down_revision: str | None = "b4c5d6e7f8a9"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create test_collections table
|
||||
op.create_table('test_collections',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('total_tests', sa.Integer(), nullable=True),
|
||||
sa.Column('total_files', sa.Integer(), nullable=True),
|
||||
sa.Column('total_classes', sa.Integer(), nullable=True),
|
||||
sa.Column('unit_tests', sa.Integer(), nullable=True),
|
||||
sa.Column('integration_tests', sa.Integer(), nullable=True),
|
||||
sa.Column('performance_tests', sa.Integer(), nullable=True),
|
||||
sa.Column('system_tests', sa.Integer(), nullable=True),
|
||||
sa.Column('test_files', sa.JSON(), nullable=True),
|
||||
sa.Column('collected_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("test_collections",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("total_tests", sa.Integer(), nullable=True),
|
||||
sa.Column("total_files", sa.Integer(), nullable=True),
|
||||
sa.Column("total_classes", sa.Integer(), nullable=True),
|
||||
sa.Column("unit_tests", sa.Integer(), nullable=True),
|
||||
sa.Column("integration_tests", sa.Integer(), nullable=True),
|
||||
sa.Column("performance_tests", sa.Integer(), nullable=True),
|
||||
sa.Column("system_tests", sa.Integer(), nullable=True),
|
||||
sa.Column("test_files", sa.JSON(), nullable=True),
|
||||
sa.Column("collected_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_test_collections_id'), 'test_collections', ['id'], unique=False)
|
||||
op.create_index(op.f("ix_test_collections_id"), "test_collections", ["id"], unique=False)
|
||||
|
||||
# Create test_runs table
|
||||
op.create_table('test_runs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('total_tests', sa.Integer(), nullable=True),
|
||||
sa.Column('passed', sa.Integer(), nullable=True),
|
||||
sa.Column('failed', sa.Integer(), nullable=True),
|
||||
sa.Column('errors', sa.Integer(), nullable=True),
|
||||
sa.Column('skipped', sa.Integer(), nullable=True),
|
||||
sa.Column('xfailed', sa.Integer(), nullable=True),
|
||||
sa.Column('xpassed', sa.Integer(), nullable=True),
|
||||
sa.Column('coverage_percent', sa.Float(), nullable=True),
|
||||
sa.Column('duration_seconds', sa.Float(), nullable=True),
|
||||
sa.Column('triggered_by', sa.String(length=100), nullable=True),
|
||||
sa.Column('git_commit_hash', sa.String(length=40), nullable=True),
|
||||
sa.Column('git_branch', sa.String(length=100), nullable=True),
|
||||
sa.Column('test_path', sa.String(length=500), nullable=True),
|
||||
sa.Column('pytest_args', sa.String(length=500), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("test_runs",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("total_tests", sa.Integer(), nullable=True),
|
||||
sa.Column("passed", sa.Integer(), nullable=True),
|
||||
sa.Column("failed", sa.Integer(), nullable=True),
|
||||
sa.Column("errors", sa.Integer(), nullable=True),
|
||||
sa.Column("skipped", sa.Integer(), nullable=True),
|
||||
sa.Column("xfailed", sa.Integer(), nullable=True),
|
||||
sa.Column("xpassed", sa.Integer(), nullable=True),
|
||||
sa.Column("coverage_percent", sa.Float(), nullable=True),
|
||||
sa.Column("duration_seconds", sa.Float(), nullable=True),
|
||||
sa.Column("triggered_by", sa.String(length=100), nullable=True),
|
||||
sa.Column("git_commit_hash", sa.String(length=40), nullable=True),
|
||||
sa.Column("git_branch", sa.String(length=100), nullable=True),
|
||||
sa.Column("test_path", sa.String(length=500), nullable=True),
|
||||
sa.Column("pytest_args", sa.String(length=500), nullable=True),
|
||||
sa.Column("status", sa.String(length=20), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_test_runs_id'), 'test_runs', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_test_runs_status'), 'test_runs', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_test_runs_timestamp'), 'test_runs', ['timestamp'], unique=False)
|
||||
op.create_index(op.f("ix_test_runs_id"), "test_runs", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_test_runs_status"), "test_runs", ["status"], unique=False)
|
||||
op.create_index(op.f("ix_test_runs_timestamp"), "test_runs", ["timestamp"], unique=False)
|
||||
|
||||
# Create test_results table
|
||||
op.create_table('test_results',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('run_id', sa.Integer(), nullable=False),
|
||||
sa.Column('node_id', sa.String(length=500), nullable=False),
|
||||
sa.Column('test_name', sa.String(length=200), nullable=False),
|
||||
sa.Column('test_file', sa.String(length=300), nullable=False),
|
||||
sa.Column('test_class', sa.String(length=200), nullable=True),
|
||||
sa.Column('outcome', sa.String(length=20), nullable=False),
|
||||
sa.Column('duration_seconds', sa.Float(), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('traceback', sa.Text(), nullable=True),
|
||||
sa.Column('markers', sa.JSON(), nullable=True),
|
||||
sa.Column('parameters', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['run_id'], ['test_runs.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("test_results",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("run_id", sa.Integer(), nullable=False),
|
||||
sa.Column("node_id", sa.String(length=500), nullable=False),
|
||||
sa.Column("test_name", sa.String(length=200), nullable=False),
|
||||
sa.Column("test_file", sa.String(length=300), nullable=False),
|
||||
sa.Column("test_class", sa.String(length=200), nullable=True),
|
||||
sa.Column("outcome", sa.String(length=20), nullable=False),
|
||||
sa.Column("duration_seconds", sa.Float(), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("traceback", sa.Text(), nullable=True),
|
||||
sa.Column("markers", sa.JSON(), nullable=True),
|
||||
sa.Column("parameters", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["run_id"], ["test_runs.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_test_results_id'), 'test_results', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_test_results_node_id'), 'test_results', ['node_id'], unique=False)
|
||||
op.create_index(op.f('ix_test_results_outcome'), 'test_results', ['outcome'], unique=False)
|
||||
op.create_index(op.f('ix_test_results_run_id'), 'test_results', ['run_id'], unique=False)
|
||||
op.create_index(op.f("ix_test_results_id"), "test_results", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_test_results_node_id"), "test_results", ["node_id"], unique=False)
|
||||
op.create_index(op.f("ix_test_results_outcome"), "test_results", ["outcome"], unique=False)
|
||||
op.create_index(op.f("ix_test_results_run_id"), "test_results", ["run_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop test_results table first (has foreign key to test_runs)
|
||||
op.drop_index(op.f('ix_test_results_run_id'), table_name='test_results')
|
||||
op.drop_index(op.f('ix_test_results_outcome'), table_name='test_results')
|
||||
op.drop_index(op.f('ix_test_results_node_id'), table_name='test_results')
|
||||
op.drop_index(op.f('ix_test_results_id'), table_name='test_results')
|
||||
op.drop_table('test_results')
|
||||
op.drop_index(op.f("ix_test_results_run_id"), table_name="test_results")
|
||||
op.drop_index(op.f("ix_test_results_outcome"), table_name="test_results")
|
||||
op.drop_index(op.f("ix_test_results_node_id"), table_name="test_results")
|
||||
op.drop_index(op.f("ix_test_results_id"), table_name="test_results")
|
||||
op.drop_table("test_results")
|
||||
|
||||
# Drop test_runs table
|
||||
op.drop_index(op.f('ix_test_runs_timestamp'), table_name='test_runs')
|
||||
op.drop_index(op.f('ix_test_runs_status'), table_name='test_runs')
|
||||
op.drop_index(op.f('ix_test_runs_id'), table_name='test_runs')
|
||||
op.drop_table('test_runs')
|
||||
op.drop_index(op.f("ix_test_runs_timestamp"), table_name="test_runs")
|
||||
op.drop_index(op.f("ix_test_runs_status"), table_name="test_runs")
|
||||
op.drop_index(op.f("ix_test_runs_id"), table_name="test_runs")
|
||||
op.drop_table("test_runs")
|
||||
|
||||
# Drop test_collections table
|
||||
op.drop_index(op.f('ix_test_collections_id'), table_name='test_collections')
|
||||
op.drop_table('test_collections')
|
||||
op.drop_index(op.f("ix_test_collections_id"), table_name="test_collections")
|
||||
op.drop_table("test_collections")
|
||||
|
||||
@@ -5,40 +5,41 @@ Revises: 987b4ecfa503
|
||||
Create Date: 2025-12-13 13:13:46.969503
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '91d02647efae'
|
||||
down_revision: Union[str, None] = '987b4ecfa503'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "91d02647efae"
|
||||
down_revision: str | None = "987b4ecfa503"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create marketplace_import_errors table to store detailed import error information
|
||||
op.create_table('marketplace_import_errors',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('import_job_id', sa.Integer(), nullable=False),
|
||||
sa.Column('row_number', sa.Integer(), nullable=False),
|
||||
sa.Column('identifier', sa.String(), nullable=True),
|
||||
sa.Column('error_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('error_message', sa.Text(), nullable=False),
|
||||
sa.Column('row_data', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['import_job_id'], ['marketplace_import_jobs.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("marketplace_import_errors",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("import_job_id", sa.Integer(), nullable=False),
|
||||
sa.Column("row_number", sa.Integer(), nullable=False),
|
||||
sa.Column("identifier", sa.String(), nullable=True),
|
||||
sa.Column("error_type", sa.String(length=50), nullable=False),
|
||||
sa.Column("error_message", sa.Text(), nullable=False),
|
||||
sa.Column("row_data", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["import_job_id"], ["marketplace_import_jobs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index('idx_import_error_job_id', 'marketplace_import_errors', ['import_job_id'], unique=False)
|
||||
op.create_index('idx_import_error_type', 'marketplace_import_errors', ['error_type'], unique=False)
|
||||
op.create_index(op.f('ix_marketplace_import_errors_id'), 'marketplace_import_errors', ['id'], unique=False)
|
||||
op.create_index("idx_import_error_job_id", "marketplace_import_errors", ["import_job_id"], unique=False)
|
||||
op.create_index("idx_import_error_type", "marketplace_import_errors", ["error_type"], unique=False)
|
||||
op.create_index(op.f("ix_marketplace_import_errors_id"), "marketplace_import_errors", ["id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f('ix_marketplace_import_errors_id'), table_name='marketplace_import_errors')
|
||||
op.drop_index('idx_import_error_type', table_name='marketplace_import_errors')
|
||||
op.drop_index('idx_import_error_job_id', table_name='marketplace_import_errors')
|
||||
op.drop_table('marketplace_import_errors')
|
||||
op.drop_index(op.f("ix_marketplace_import_errors_id"), table_name="marketplace_import_errors")
|
||||
op.drop_index("idx_import_error_type", table_name="marketplace_import_errors")
|
||||
op.drop_index("idx_import_error_job_id", table_name="marketplace_import_errors")
|
||||
op.drop_table("marketplace_import_errors")
|
||||
|
||||
@@ -11,169 +11,169 @@ This migration adds:
|
||||
- letzshop_sync_logs: Audit trail for sync operations
|
||||
- Adds channel fields to orders table for multi-marketplace support
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '987b4ecfa503'
|
||||
down_revision: Union[str, None] = '82ea1b4a3ccb'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "987b4ecfa503"
|
||||
down_revision: str | None = "82ea1b4a3ccb"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add channel fields to orders table
|
||||
op.add_column('orders', sa.Column('channel', sa.String(length=50), nullable=True, server_default='direct'))
|
||||
op.add_column('orders', sa.Column('external_order_id', sa.String(length=100), nullable=True))
|
||||
op.add_column('orders', sa.Column('external_channel_data', sa.JSON(), nullable=True))
|
||||
op.create_index(op.f('ix_orders_channel'), 'orders', ['channel'], unique=False)
|
||||
op.create_index(op.f('ix_orders_external_order_id'), 'orders', ['external_order_id'], unique=False)
|
||||
op.add_column("orders", sa.Column("channel", sa.String(length=50), nullable=True, server_default="direct"))
|
||||
op.add_column("orders", sa.Column("external_order_id", sa.String(length=100), nullable=True))
|
||||
op.add_column("orders", sa.Column("external_channel_data", sa.JSON(), nullable=True))
|
||||
op.create_index(op.f("ix_orders_channel"), "orders", ["channel"], unique=False)
|
||||
op.create_index(op.f("ix_orders_external_order_id"), "orders", ["external_order_id"], unique=False)
|
||||
|
||||
# Create vendor_letzshop_credentials table
|
||||
op.create_table('vendor_letzshop_credentials',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('api_key_encrypted', sa.Text(), nullable=False),
|
||||
sa.Column('api_endpoint', sa.String(length=255), server_default='https://letzshop.lu/graphql', nullable=True),
|
||||
sa.Column('auto_sync_enabled', sa.Boolean(), server_default='0', nullable=True),
|
||||
sa.Column('sync_interval_minutes', sa.Integer(), server_default='15', nullable=True),
|
||||
sa.Column('last_sync_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('last_sync_status', sa.String(length=50), nullable=True),
|
||||
sa.Column('last_sync_error', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('vendor_id')
|
||||
op.create_table("vendor_letzshop_credentials",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("api_key_encrypted", sa.Text(), nullable=False),
|
||||
sa.Column("api_endpoint", sa.String(length=255), server_default="https://letzshop.lu/graphql", nullable=True),
|
||||
sa.Column("auto_sync_enabled", sa.Boolean(), server_default="0", nullable=True),
|
||||
sa.Column("sync_interval_minutes", sa.Integer(), server_default="15", nullable=True),
|
||||
sa.Column("last_sync_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("last_sync_status", sa.String(length=50), nullable=True),
|
||||
sa.Column("last_sync_error", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("vendor_id")
|
||||
)
|
||||
op.create_index(op.f('ix_vendor_letzshop_credentials_id'), 'vendor_letzshop_credentials', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_vendor_letzshop_credentials_vendor_id'), 'vendor_letzshop_credentials', ['vendor_id'], unique=True)
|
||||
op.create_index(op.f("ix_vendor_letzshop_credentials_id"), "vendor_letzshop_credentials", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_vendor_letzshop_credentials_vendor_id"), "vendor_letzshop_credentials", ["vendor_id"], unique=True)
|
||||
|
||||
# Create letzshop_orders table
|
||||
op.create_table('letzshop_orders',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('letzshop_order_id', sa.String(length=100), nullable=False),
|
||||
sa.Column('letzshop_shipment_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('letzshop_order_number', sa.String(length=100), nullable=True),
|
||||
sa.Column('local_order_id', sa.Integer(), nullable=True),
|
||||
sa.Column('letzshop_state', sa.String(length=50), nullable=True),
|
||||
sa.Column('customer_email', sa.String(length=255), nullable=True),
|
||||
sa.Column('customer_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('total_amount', sa.String(length=50), nullable=True),
|
||||
sa.Column('currency', sa.String(length=10), server_default='EUR', nullable=True),
|
||||
sa.Column('raw_order_data', sa.JSON(), nullable=True),
|
||||
sa.Column('inventory_units', sa.JSON(), nullable=True),
|
||||
sa.Column('sync_status', sa.String(length=50), server_default='pending', nullable=True),
|
||||
sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('sync_error', sa.Text(), nullable=True),
|
||||
sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('rejected_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('tracking_set_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('tracking_number', sa.String(length=100), nullable=True),
|
||||
sa.Column('tracking_carrier', sa.String(length=100), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['local_order_id'], ['orders.id'], ),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("letzshop_orders",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("letzshop_order_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("letzshop_shipment_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("letzshop_order_number", sa.String(length=100), nullable=True),
|
||||
sa.Column("local_order_id", sa.Integer(), nullable=True),
|
||||
sa.Column("letzshop_state", sa.String(length=50), nullable=True),
|
||||
sa.Column("customer_email", sa.String(length=255), nullable=True),
|
||||
sa.Column("customer_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("total_amount", sa.String(length=50), nullable=True),
|
||||
sa.Column("currency", sa.String(length=10), server_default="EUR", nullable=True),
|
||||
sa.Column("raw_order_data", sa.JSON(), nullable=True),
|
||||
sa.Column("inventory_units", sa.JSON(), nullable=True),
|
||||
sa.Column("sync_status", sa.String(length=50), server_default="pending", nullable=True),
|
||||
sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("sync_error", sa.Text(), nullable=True),
|
||||
sa.Column("confirmed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("rejected_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("tracking_set_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("tracking_number", sa.String(length=100), nullable=True),
|
||||
sa.Column("tracking_carrier", sa.String(length=100), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["local_order_id"], ["orders.id"], ),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_letzshop_orders_id'), 'letzshop_orders', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_orders_letzshop_order_id'), 'letzshop_orders', ['letzshop_order_id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_orders_letzshop_shipment_id'), 'letzshop_orders', ['letzshop_shipment_id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_orders_vendor_id'), 'letzshop_orders', ['vendor_id'], unique=False)
|
||||
op.create_index('idx_letzshop_order_vendor', 'letzshop_orders', ['vendor_id', 'letzshop_order_id'], unique=False)
|
||||
op.create_index('idx_letzshop_order_state', 'letzshop_orders', ['vendor_id', 'letzshop_state'], unique=False)
|
||||
op.create_index('idx_letzshop_order_sync', 'letzshop_orders', ['vendor_id', 'sync_status'], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_orders_id"), "letzshop_orders", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_orders_letzshop_order_id"), "letzshop_orders", ["letzshop_order_id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_orders_letzshop_shipment_id"), "letzshop_orders", ["letzshop_shipment_id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_orders_vendor_id"), "letzshop_orders", ["vendor_id"], unique=False)
|
||||
op.create_index("idx_letzshop_order_vendor", "letzshop_orders", ["vendor_id", "letzshop_order_id"], unique=False)
|
||||
op.create_index("idx_letzshop_order_state", "letzshop_orders", ["vendor_id", "letzshop_state"], unique=False)
|
||||
op.create_index("idx_letzshop_order_sync", "letzshop_orders", ["vendor_id", "sync_status"], unique=False)
|
||||
|
||||
# Create letzshop_fulfillment_queue table
|
||||
op.create_table('letzshop_fulfillment_queue',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('letzshop_order_id', sa.Integer(), nullable=False),
|
||||
sa.Column('operation', sa.String(length=50), nullable=False),
|
||||
sa.Column('payload', sa.JSON(), nullable=False),
|
||||
sa.Column('status', sa.String(length=50), server_default='pending', nullable=True),
|
||||
sa.Column('attempts', sa.Integer(), server_default='0', nullable=True),
|
||||
sa.Column('max_attempts', sa.Integer(), server_default='3', nullable=True),
|
||||
sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('next_retry_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('response_data', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['letzshop_order_id'], ['letzshop_orders.id'], ),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("letzshop_fulfillment_queue",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("letzshop_order_id", sa.Integer(), nullable=False),
|
||||
sa.Column("operation", sa.String(length=50), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=False),
|
||||
sa.Column("status", sa.String(length=50), server_default="pending", nullable=True),
|
||||
sa.Column("attempts", sa.Integer(), server_default="0", nullable=True),
|
||||
sa.Column("max_attempts", sa.Integer(), server_default="3", nullable=True),
|
||||
sa.Column("last_attempt_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("next_retry_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("response_data", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["letzshop_order_id"], ["letzshop_orders.id"], ),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_letzshop_fulfillment_queue_id'), 'letzshop_fulfillment_queue', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), 'letzshop_fulfillment_queue', ['vendor_id'], unique=False)
|
||||
op.create_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue', ['status', 'vendor_id'], unique=False)
|
||||
op.create_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue', ['status', 'next_retry_at'], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_fulfillment_queue_id"), "letzshop_fulfillment_queue", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_fulfillment_queue_vendor_id"), "letzshop_fulfillment_queue", ["vendor_id"], unique=False)
|
||||
op.create_index("idx_fulfillment_queue_status", "letzshop_fulfillment_queue", ["status", "vendor_id"], unique=False)
|
||||
op.create_index("idx_fulfillment_queue_retry", "letzshop_fulfillment_queue", ["status", "next_retry_at"], unique=False)
|
||||
|
||||
# Create letzshop_sync_logs table
|
||||
op.create_table('letzshop_sync_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('operation_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('direction', sa.String(length=10), nullable=False),
|
||||
sa.Column('status', sa.String(length=50), nullable=False),
|
||||
sa.Column('records_processed', sa.Integer(), server_default='0', nullable=True),
|
||||
sa.Column('records_succeeded', sa.Integer(), server_default='0', nullable=True),
|
||||
sa.Column('records_failed', sa.Integer(), server_default='0', nullable=True),
|
||||
sa.Column('error_details', sa.JSON(), nullable=True),
|
||||
sa.Column('started_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('duration_seconds', sa.Integer(), nullable=True),
|
||||
sa.Column('triggered_by', sa.String(length=100), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("letzshop_sync_logs",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("operation_type", sa.String(length=50), nullable=False),
|
||||
sa.Column("direction", sa.String(length=10), nullable=False),
|
||||
sa.Column("status", sa.String(length=50), nullable=False),
|
||||
sa.Column("records_processed", sa.Integer(), server_default="0", nullable=True),
|
||||
sa.Column("records_succeeded", sa.Integer(), server_default="0", nullable=True),
|
||||
sa.Column("records_failed", sa.Integer(), server_default="0", nullable=True),
|
||||
sa.Column("error_details", sa.JSON(), nullable=True),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("duration_seconds", sa.Integer(), nullable=True),
|
||||
sa.Column("triggered_by", sa.String(length=100), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_letzshop_sync_logs_id'), 'letzshop_sync_logs', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_sync_logs_vendor_id'), 'letzshop_sync_logs', ['vendor_id'], unique=False)
|
||||
op.create_index('idx_sync_log_vendor_type', 'letzshop_sync_logs', ['vendor_id', 'operation_type'], unique=False)
|
||||
op.create_index('idx_sync_log_vendor_date', 'letzshop_sync_logs', ['vendor_id', 'started_at'], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_sync_logs_id"), "letzshop_sync_logs", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_sync_logs_vendor_id"), "letzshop_sync_logs", ["vendor_id"], unique=False)
|
||||
op.create_index("idx_sync_log_vendor_type", "letzshop_sync_logs", ["vendor_id", "operation_type"], unique=False)
|
||||
op.create_index("idx_sync_log_vendor_date", "letzshop_sync_logs", ["vendor_id", "started_at"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop letzshop_sync_logs table
|
||||
op.drop_index('idx_sync_log_vendor_date', table_name='letzshop_sync_logs')
|
||||
op.drop_index('idx_sync_log_vendor_type', table_name='letzshop_sync_logs')
|
||||
op.drop_index(op.f('ix_letzshop_sync_logs_vendor_id'), table_name='letzshop_sync_logs')
|
||||
op.drop_index(op.f('ix_letzshop_sync_logs_id'), table_name='letzshop_sync_logs')
|
||||
op.drop_table('letzshop_sync_logs')
|
||||
op.drop_index("idx_sync_log_vendor_date", table_name="letzshop_sync_logs")
|
||||
op.drop_index("idx_sync_log_vendor_type", table_name="letzshop_sync_logs")
|
||||
op.drop_index(op.f("ix_letzshop_sync_logs_vendor_id"), table_name="letzshop_sync_logs")
|
||||
op.drop_index(op.f("ix_letzshop_sync_logs_id"), table_name="letzshop_sync_logs")
|
||||
op.drop_table("letzshop_sync_logs")
|
||||
|
||||
# Drop letzshop_fulfillment_queue table
|
||||
op.drop_index('idx_fulfillment_queue_retry', table_name='letzshop_fulfillment_queue')
|
||||
op.drop_index('idx_fulfillment_queue_status', table_name='letzshop_fulfillment_queue')
|
||||
op.drop_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), table_name='letzshop_fulfillment_queue')
|
||||
op.drop_index(op.f('ix_letzshop_fulfillment_queue_id'), table_name='letzshop_fulfillment_queue')
|
||||
op.drop_table('letzshop_fulfillment_queue')
|
||||
op.drop_index("idx_fulfillment_queue_retry", table_name="letzshop_fulfillment_queue")
|
||||
op.drop_index("idx_fulfillment_queue_status", table_name="letzshop_fulfillment_queue")
|
||||
op.drop_index(op.f("ix_letzshop_fulfillment_queue_vendor_id"), table_name="letzshop_fulfillment_queue")
|
||||
op.drop_index(op.f("ix_letzshop_fulfillment_queue_id"), table_name="letzshop_fulfillment_queue")
|
||||
op.drop_table("letzshop_fulfillment_queue")
|
||||
|
||||
# Drop letzshop_orders table
|
||||
op.drop_index('idx_letzshop_order_sync', table_name='letzshop_orders')
|
||||
op.drop_index('idx_letzshop_order_state', table_name='letzshop_orders')
|
||||
op.drop_index('idx_letzshop_order_vendor', table_name='letzshop_orders')
|
||||
op.drop_index(op.f('ix_letzshop_orders_vendor_id'), table_name='letzshop_orders')
|
||||
op.drop_index(op.f('ix_letzshop_orders_letzshop_shipment_id'), table_name='letzshop_orders')
|
||||
op.drop_index(op.f('ix_letzshop_orders_letzshop_order_id'), table_name='letzshop_orders')
|
||||
op.drop_index(op.f('ix_letzshop_orders_id'), table_name='letzshop_orders')
|
||||
op.drop_table('letzshop_orders')
|
||||
op.drop_index("idx_letzshop_order_sync", table_name="letzshop_orders")
|
||||
op.drop_index("idx_letzshop_order_state", table_name="letzshop_orders")
|
||||
op.drop_index("idx_letzshop_order_vendor", table_name="letzshop_orders")
|
||||
op.drop_index(op.f("ix_letzshop_orders_vendor_id"), table_name="letzshop_orders")
|
||||
op.drop_index(op.f("ix_letzshop_orders_letzshop_shipment_id"), table_name="letzshop_orders")
|
||||
op.drop_index(op.f("ix_letzshop_orders_letzshop_order_id"), table_name="letzshop_orders")
|
||||
op.drop_index(op.f("ix_letzshop_orders_id"), table_name="letzshop_orders")
|
||||
op.drop_table("letzshop_orders")
|
||||
|
||||
# Drop vendor_letzshop_credentials table
|
||||
op.drop_index(op.f('ix_vendor_letzshop_credentials_vendor_id'), table_name='vendor_letzshop_credentials')
|
||||
op.drop_index(op.f('ix_vendor_letzshop_credentials_id'), table_name='vendor_letzshop_credentials')
|
||||
op.drop_table('vendor_letzshop_credentials')
|
||||
op.drop_index(op.f("ix_vendor_letzshop_credentials_vendor_id"), table_name="vendor_letzshop_credentials")
|
||||
op.drop_index(op.f("ix_vendor_letzshop_credentials_id"), table_name="vendor_letzshop_credentials")
|
||||
op.drop_table("vendor_letzshop_credentials")
|
||||
|
||||
# Drop channel fields from orders table
|
||||
op.drop_index(op.f('ix_orders_external_order_id'), table_name='orders')
|
||||
op.drop_index(op.f('ix_orders_channel'), table_name='orders')
|
||||
op.drop_column('orders', 'external_channel_data')
|
||||
op.drop_column('orders', 'external_order_id')
|
||||
op.drop_column('orders', 'channel')
|
||||
op.drop_index(op.f("ix_orders_external_order_id"), table_name="orders")
|
||||
op.drop_index(op.f("ix_orders_channel"), table_name="orders")
|
||||
op.drop_column("orders", "external_channel_data")
|
||||
op.drop_column("orders", "external_order_id")
|
||||
op.drop_column("orders", "channel")
|
||||
|
||||
@@ -13,17 +13,17 @@ Architecture Change:
|
||||
The vendor ownership is now determined via the company relationship:
|
||||
- vendor.company.owner_user_id contains the owner
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '9f3a25ea4991'
|
||||
down_revision: Union[str, None] = '5818330181a5'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "9f3a25ea4991"
|
||||
down_revision: str | None = "5818330181a5"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -35,9 +35,9 @@ def upgrade() -> None:
|
||||
Note: SQLite batch mode recreates the table without the column,
|
||||
so we don't need to explicitly drop constraints.
|
||||
"""
|
||||
with op.batch_alter_table('vendors', schema=None) as batch_op:
|
||||
with op.batch_alter_table("vendors", schema=None) as batch_op:
|
||||
# Drop the column - batch mode handles constraints automatically
|
||||
batch_op.drop_column('owner_user_id')
|
||||
batch_op.drop_column("owner_user_id")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
@@ -48,13 +48,13 @@ def downgrade() -> None:
|
||||
You will need to manually populate owner_user_id from company.owner_user_id
|
||||
if reverting this migration.
|
||||
"""
|
||||
with op.batch_alter_table('vendors', schema=None) as batch_op:
|
||||
with op.batch_alter_table("vendors", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column('owner_user_id', sa.Integer(), nullable=True)
|
||||
sa.Column("owner_user_id", sa.Integer(), nullable=True)
|
||||
)
|
||||
batch_op.create_foreign_key(
|
||||
'vendors_owner_user_id_fkey',
|
||||
'users',
|
||||
['owner_user_id'],
|
||||
['id']
|
||||
"vendors_owner_user_id_fkey",
|
||||
"users",
|
||||
["owner_user_id"],
|
||||
["id"]
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2025-11-23 19:52:40.509538
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -14,9 +14,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "a2064e1dfcd4"
|
||||
down_revision: Union[str, None] = "f68d8da5315a"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "f68d8da5315a"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -15,7 +15,7 @@ The override pattern: NULL value means "inherit from marketplace_product".
|
||||
Setting a value creates a vendor-specific override.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -23,9 +23,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "a3b4c5d6e7f8"
|
||||
down_revision: Union[str, None] = "f2b3c4d5e6f7"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "f2b3c4d5e6f7"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,27 +5,27 @@ Revises: fcfdc02d5138
|
||||
Create Date: 2025-12-17 20:55:41.477848
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a9a86cef6cca'
|
||||
down_revision: Union[str, None] = 'fcfdc02d5138'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "a9a86cef6cca"
|
||||
down_revision: str | None = "fcfdc02d5138"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add new columns to letzshop_orders for customer locale and country
|
||||
op.add_column('letzshop_orders', sa.Column('customer_locale', sa.String(length=10), nullable=True))
|
||||
op.add_column('letzshop_orders', sa.Column('shipping_country_iso', sa.String(length=5), nullable=True))
|
||||
op.add_column('letzshop_orders', sa.Column('billing_country_iso', sa.String(length=5), nullable=True))
|
||||
op.add_column("letzshop_orders", sa.Column("customer_locale", sa.String(length=10), nullable=True))
|
||||
op.add_column("letzshop_orders", sa.Column("shipping_country_iso", sa.String(length=5), nullable=True))
|
||||
op.add_column("letzshop_orders", sa.Column("billing_country_iso", sa.String(length=5), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('letzshop_orders', 'billing_country_iso')
|
||||
op.drop_column('letzshop_orders', 'shipping_country_iso')
|
||||
op.drop_column('letzshop_orders', 'customer_locale')
|
||||
op.drop_column("letzshop_orders", "billing_country_iso")
|
||||
op.drop_column("letzshop_orders", "shipping_country_iso")
|
||||
op.drop_column("letzshop_orders", "customer_locale")
|
||||
|
||||
@@ -5,26 +5,26 @@ Revises: 91d02647efae
|
||||
Create Date: 2025-12-13 13:35:46.524893
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b412e0b49c2e'
|
||||
down_revision: Union[str, None] = '91d02647efae'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "b412e0b49c2e"
|
||||
down_revision: str | None = "91d02647efae"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add language column with default value for existing rows
|
||||
op.add_column(
|
||||
'marketplace_import_jobs',
|
||||
sa.Column('language', sa.String(length=5), nullable=False, server_default='en')
|
||||
"marketplace_import_jobs",
|
||||
sa.Column("language", sa.String(length=5), nullable=False, server_default="en")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('marketplace_import_jobs', 'language')
|
||||
op.drop_column("marketplace_import_jobs", "language")
|
||||
|
||||
@@ -15,7 +15,7 @@ after migrating the data to the new structure.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import text
|
||||
@@ -24,9 +24,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "b4c5d6e7f8a9"
|
||||
down_revision: Union[str, None] = "a3b4c5d6e7f8"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "a3b4c5d6e7f8"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def parse_price(price_str: str) -> float | None:
|
||||
|
||||
@@ -5,17 +5,17 @@ Revises: m1b2c3d4e5f6
|
||||
Create Date: 2025-12-28 20:00:24.263518
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'ba2c0ce78396'
|
||||
down_revision: Union[str, None] = 'm1b2c3d4e5f6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "ba2c0ce78396"
|
||||
down_revision: str | None = "m1b2c3d4e5f6"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -25,8 +25,8 @@ def upgrade() -> None:
|
||||
alongside the copyright notice (e.g., Privacy Policy, Terms of Service).
|
||||
"""
|
||||
op.add_column(
|
||||
'content_pages',
|
||||
sa.Column('show_in_legal', sa.Boolean(), nullable=True, default=False)
|
||||
"content_pages",
|
||||
sa.Column("show_in_legal", sa.Boolean(), nullable=True, default=False)
|
||||
)
|
||||
|
||||
# Set default value for existing rows (PostgreSQL uses true/false for boolean)
|
||||
@@ -38,4 +38,4 @@ def upgrade() -> None:
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove show_in_legal column from content_pages table."""
|
||||
op.drop_column('content_pages', 'show_in_legal')
|
||||
op.drop_column("content_pages", "show_in_legal")
|
||||
|
||||
@@ -5,31 +5,31 @@ Revises: 55b92e155566
|
||||
Create Date: 2025-12-20 18:49:53.432904
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c00d2985701f'
|
||||
down_revision: Union[str, None] = '55b92e155566'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "c00d2985701f"
|
||||
down_revision: str | None = "55b92e155566"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add carrier settings and test mode to vendor_letzshop_credentials
|
||||
op.add_column('vendor_letzshop_credentials', sa.Column('test_mode_enabled', sa.Boolean(), nullable=True, server_default='0'))
|
||||
op.add_column('vendor_letzshop_credentials', sa.Column('default_carrier', sa.String(length=50), nullable=True))
|
||||
op.add_column('vendor_letzshop_credentials', sa.Column('carrier_greco_label_url', sa.String(length=500), nullable=True, server_default='https://dispatchweb.fr/Tracky/Home/'))
|
||||
op.add_column('vendor_letzshop_credentials', sa.Column('carrier_colissimo_label_url', sa.String(length=500), nullable=True))
|
||||
op.add_column('vendor_letzshop_credentials', sa.Column('carrier_xpresslogistics_label_url', sa.String(length=500), nullable=True))
|
||||
op.add_column("vendor_letzshop_credentials", sa.Column("test_mode_enabled", sa.Boolean(), nullable=True, server_default="0"))
|
||||
op.add_column("vendor_letzshop_credentials", sa.Column("default_carrier", sa.String(length=50), nullable=True))
|
||||
op.add_column("vendor_letzshop_credentials", sa.Column("carrier_greco_label_url", sa.String(length=500), nullable=True, server_default="https://dispatchweb.fr/Tracky/Home/"))
|
||||
op.add_column("vendor_letzshop_credentials", sa.Column("carrier_colissimo_label_url", sa.String(length=500), nullable=True))
|
||||
op.add_column("vendor_letzshop_credentials", sa.Column("carrier_xpresslogistics_label_url", sa.String(length=500), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('vendor_letzshop_credentials', 'carrier_xpresslogistics_label_url')
|
||||
op.drop_column('vendor_letzshop_credentials', 'carrier_colissimo_label_url')
|
||||
op.drop_column('vendor_letzshop_credentials', 'carrier_greco_label_url')
|
||||
op.drop_column('vendor_letzshop_credentials', 'default_carrier')
|
||||
op.drop_column('vendor_letzshop_credentials', 'test_mode_enabled')
|
||||
op.drop_column("vendor_letzshop_credentials", "carrier_xpresslogistics_label_url")
|
||||
op.drop_column("vendor_letzshop_credentials", "carrier_colissimo_label_url")
|
||||
op.drop_column("vendor_letzshop_credentials", "carrier_greco_label_url")
|
||||
op.drop_column("vendor_letzshop_credentials", "default_carrier")
|
||||
op.drop_column("vendor_letzshop_credentials", "test_mode_enabled")
|
||||
|
||||
@@ -21,18 +21,18 @@ Design principles:
|
||||
- Customer/address data snapshotted at order time
|
||||
- Products must exist in catalog (enforced by FK)
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c1d2e3f4a5b6'
|
||||
down_revision: Union[str, None] = '2362c2723a93'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "c1d2e3f4a5b6"
|
||||
down_revision: str | None = "2362c2723a93"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def table_exists(table_name: str) -> bool:
|
||||
@@ -48,7 +48,7 @@ def index_exists(index_name: str, table_name: str) -> bool:
|
||||
inspector = inspect(bind)
|
||||
try:
|
||||
indexes = inspector.get_indexes(table_name)
|
||||
return any(idx['name'] == index_name for idx in indexes)
|
||||
return any(idx["name"] == index_name for idx in indexes)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -71,382 +71,382 @@ def upgrade() -> None:
|
||||
# =========================================================================
|
||||
|
||||
# Drop letzshop_fulfillment_queue (references letzshop_orders)
|
||||
if table_exists('letzshop_fulfillment_queue'):
|
||||
safe_drop_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue')
|
||||
safe_drop_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue')
|
||||
safe_drop_index('ix_letzshop_fulfillment_queue_vendor_id', 'letzshop_fulfillment_queue')
|
||||
safe_drop_index('ix_letzshop_fulfillment_queue_id', 'letzshop_fulfillment_queue')
|
||||
op.drop_table('letzshop_fulfillment_queue')
|
||||
if table_exists("letzshop_fulfillment_queue"):
|
||||
safe_drop_index("idx_fulfillment_queue_retry", "letzshop_fulfillment_queue")
|
||||
safe_drop_index("idx_fulfillment_queue_status", "letzshop_fulfillment_queue")
|
||||
safe_drop_index("ix_letzshop_fulfillment_queue_vendor_id", "letzshop_fulfillment_queue")
|
||||
safe_drop_index("ix_letzshop_fulfillment_queue_id", "letzshop_fulfillment_queue")
|
||||
op.drop_table("letzshop_fulfillment_queue")
|
||||
|
||||
# Drop letzshop_orders table (replaced by unified orders)
|
||||
if table_exists('letzshop_orders'):
|
||||
safe_drop_index('idx_letzshop_order_sync', 'letzshop_orders')
|
||||
safe_drop_index('idx_letzshop_order_state', 'letzshop_orders')
|
||||
safe_drop_index('idx_letzshop_order_vendor', 'letzshop_orders')
|
||||
safe_drop_index('ix_letzshop_orders_vendor_id', 'letzshop_orders')
|
||||
safe_drop_index('ix_letzshop_orders_letzshop_shipment_id', 'letzshop_orders')
|
||||
safe_drop_index('ix_letzshop_orders_letzshop_order_id', 'letzshop_orders')
|
||||
safe_drop_index('ix_letzshop_orders_id', 'letzshop_orders')
|
||||
op.drop_table('letzshop_orders')
|
||||
if table_exists("letzshop_orders"):
|
||||
safe_drop_index("idx_letzshop_order_sync", "letzshop_orders")
|
||||
safe_drop_index("idx_letzshop_order_state", "letzshop_orders")
|
||||
safe_drop_index("idx_letzshop_order_vendor", "letzshop_orders")
|
||||
safe_drop_index("ix_letzshop_orders_vendor_id", "letzshop_orders")
|
||||
safe_drop_index("ix_letzshop_orders_letzshop_shipment_id", "letzshop_orders")
|
||||
safe_drop_index("ix_letzshop_orders_letzshop_order_id", "letzshop_orders")
|
||||
safe_drop_index("ix_letzshop_orders_id", "letzshop_orders")
|
||||
op.drop_table("letzshop_orders")
|
||||
|
||||
# Drop order_items (references orders)
|
||||
if table_exists('order_items'):
|
||||
safe_drop_index('ix_order_items_id', 'order_items')
|
||||
safe_drop_index('ix_order_items_order_id', 'order_items')
|
||||
op.drop_table('order_items')
|
||||
if table_exists("order_items"):
|
||||
safe_drop_index("ix_order_items_id", "order_items")
|
||||
safe_drop_index("ix_order_items_order_id", "order_items")
|
||||
op.drop_table("order_items")
|
||||
|
||||
# Drop old orders table
|
||||
if table_exists('orders'):
|
||||
safe_drop_index('ix_orders_external_order_id', 'orders')
|
||||
safe_drop_index('ix_orders_channel', 'orders')
|
||||
safe_drop_index('ix_orders_vendor_id', 'orders')
|
||||
safe_drop_index('ix_orders_status', 'orders')
|
||||
safe_drop_index('ix_orders_order_number', 'orders')
|
||||
safe_drop_index('ix_orders_id', 'orders')
|
||||
safe_drop_index('ix_orders_customer_id', 'orders')
|
||||
op.drop_table('orders')
|
||||
if table_exists("orders"):
|
||||
safe_drop_index("ix_orders_external_order_id", "orders")
|
||||
safe_drop_index("ix_orders_channel", "orders")
|
||||
safe_drop_index("ix_orders_vendor_id", "orders")
|
||||
safe_drop_index("ix_orders_status", "orders")
|
||||
safe_drop_index("ix_orders_order_number", "orders")
|
||||
safe_drop_index("ix_orders_id", "orders")
|
||||
safe_drop_index("ix_orders_customer_id", "orders")
|
||||
op.drop_table("orders")
|
||||
|
||||
# =========================================================================
|
||||
# Step 2: Create new unified orders table
|
||||
# =========================================================================
|
||||
op.create_table('orders',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('customer_id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(length=100), nullable=False),
|
||||
op.create_table("orders",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("customer_id", sa.Integer(), nullable=False),
|
||||
sa.Column("order_number", sa.String(length=100), nullable=False),
|
||||
|
||||
# Channel/Source
|
||||
sa.Column('channel', sa.String(length=50), nullable=False, server_default='direct'),
|
||||
sa.Column("channel", sa.String(length=50), nullable=False, server_default="direct"),
|
||||
|
||||
# External references (for marketplace orders)
|
||||
sa.Column('external_order_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('external_shipment_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('external_order_number', sa.String(length=100), nullable=True),
|
||||
sa.Column('external_data', sa.JSON(), nullable=True),
|
||||
sa.Column("external_order_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("external_shipment_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("external_order_number", sa.String(length=100), nullable=True),
|
||||
sa.Column("external_data", sa.JSON(), nullable=True),
|
||||
|
||||
# Status
|
||||
sa.Column('status', sa.String(length=50), nullable=False, server_default='pending'),
|
||||
sa.Column("status", sa.String(length=50), nullable=False, server_default="pending"),
|
||||
|
||||
# Financials
|
||||
sa.Column('subtotal', sa.Float(), nullable=True),
|
||||
sa.Column('tax_amount', sa.Float(), nullable=True),
|
||||
sa.Column('shipping_amount', sa.Float(), nullable=True),
|
||||
sa.Column('discount_amount', sa.Float(), nullable=True),
|
||||
sa.Column('total_amount', sa.Float(), nullable=False),
|
||||
sa.Column('currency', sa.String(length=10), server_default='EUR', nullable=True),
|
||||
sa.Column("subtotal", sa.Float(), nullable=True),
|
||||
sa.Column("tax_amount", sa.Float(), nullable=True),
|
||||
sa.Column("shipping_amount", sa.Float(), nullable=True),
|
||||
sa.Column("discount_amount", sa.Float(), nullable=True),
|
||||
sa.Column("total_amount", sa.Float(), nullable=False),
|
||||
sa.Column("currency", sa.String(length=10), server_default="EUR", nullable=True),
|
||||
|
||||
# Customer snapshot
|
||||
sa.Column('customer_first_name', sa.String(length=100), nullable=False),
|
||||
sa.Column('customer_last_name', sa.String(length=100), nullable=False),
|
||||
sa.Column('customer_email', sa.String(length=255), nullable=False),
|
||||
sa.Column('customer_phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('customer_locale', sa.String(length=10), nullable=True),
|
||||
sa.Column("customer_first_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("customer_last_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("customer_email", sa.String(length=255), nullable=False),
|
||||
sa.Column("customer_phone", sa.String(length=50), nullable=True),
|
||||
sa.Column("customer_locale", sa.String(length=10), nullable=True),
|
||||
|
||||
# Shipping address snapshot
|
||||
sa.Column('ship_first_name', sa.String(length=100), nullable=False),
|
||||
sa.Column('ship_last_name', sa.String(length=100), nullable=False),
|
||||
sa.Column('ship_company', sa.String(length=200), nullable=True),
|
||||
sa.Column('ship_address_line_1', sa.String(length=255), nullable=False),
|
||||
sa.Column('ship_address_line_2', sa.String(length=255), nullable=True),
|
||||
sa.Column('ship_city', sa.String(length=100), nullable=False),
|
||||
sa.Column('ship_postal_code', sa.String(length=20), nullable=False),
|
||||
sa.Column('ship_country_iso', sa.String(length=5), nullable=False),
|
||||
sa.Column("ship_first_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("ship_last_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("ship_company", sa.String(length=200), nullable=True),
|
||||
sa.Column("ship_address_line_1", sa.String(length=255), nullable=False),
|
||||
sa.Column("ship_address_line_2", sa.String(length=255), nullable=True),
|
||||
sa.Column("ship_city", sa.String(length=100), nullable=False),
|
||||
sa.Column("ship_postal_code", sa.String(length=20), nullable=False),
|
||||
sa.Column("ship_country_iso", sa.String(length=5), nullable=False),
|
||||
|
||||
# Billing address snapshot
|
||||
sa.Column('bill_first_name', sa.String(length=100), nullable=False),
|
||||
sa.Column('bill_last_name', sa.String(length=100), nullable=False),
|
||||
sa.Column('bill_company', sa.String(length=200), nullable=True),
|
||||
sa.Column('bill_address_line_1', sa.String(length=255), nullable=False),
|
||||
sa.Column('bill_address_line_2', sa.String(length=255), nullable=True),
|
||||
sa.Column('bill_city', sa.String(length=100), nullable=False),
|
||||
sa.Column('bill_postal_code', sa.String(length=20), nullable=False),
|
||||
sa.Column('bill_country_iso', sa.String(length=5), nullable=False),
|
||||
sa.Column("bill_first_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("bill_last_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("bill_company", sa.String(length=200), nullable=True),
|
||||
sa.Column("bill_address_line_1", sa.String(length=255), nullable=False),
|
||||
sa.Column("bill_address_line_2", sa.String(length=255), nullable=True),
|
||||
sa.Column("bill_city", sa.String(length=100), nullable=False),
|
||||
sa.Column("bill_postal_code", sa.String(length=20), nullable=False),
|
||||
sa.Column("bill_country_iso", sa.String(length=5), nullable=False),
|
||||
|
||||
# Tracking
|
||||
sa.Column('shipping_method', sa.String(length=100), nullable=True),
|
||||
sa.Column('tracking_number', sa.String(length=100), nullable=True),
|
||||
sa.Column('tracking_provider', sa.String(length=100), nullable=True),
|
||||
sa.Column("shipping_method", sa.String(length=100), nullable=True),
|
||||
sa.Column("tracking_number", sa.String(length=100), nullable=True),
|
||||
sa.Column("tracking_provider", sa.String(length=100), nullable=True),
|
||||
|
||||
# Notes
|
||||
sa.Column('customer_notes', sa.Text(), nullable=True),
|
||||
sa.Column('internal_notes', sa.Text(), nullable=True),
|
||||
sa.Column("customer_notes", sa.Text(), nullable=True),
|
||||
sa.Column("internal_notes", sa.Text(), nullable=True),
|
||||
|
||||
# Timestamps
|
||||
sa.Column('order_date', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('shipped_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('delivered_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('cancelled_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column("order_date", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("confirmed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("shipped_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("delivered_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("cancelled_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
|
||||
# Foreign keys
|
||||
sa.ForeignKeyConstraint(['customer_id'], ['customers.id']),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
sa.ForeignKeyConstraint(["customer_id"], ["customers.id"]),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
|
||||
# Indexes for orders
|
||||
op.create_index(op.f('ix_orders_id'), 'orders', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_orders_vendor_id'), 'orders', ['vendor_id'], unique=False)
|
||||
op.create_index(op.f('ix_orders_customer_id'), 'orders', ['customer_id'], unique=False)
|
||||
op.create_index(op.f('ix_orders_order_number'), 'orders', ['order_number'], unique=True)
|
||||
op.create_index(op.f('ix_orders_channel'), 'orders', ['channel'], unique=False)
|
||||
op.create_index(op.f('ix_orders_status'), 'orders', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_orders_external_order_id'), 'orders', ['external_order_id'], unique=False)
|
||||
op.create_index(op.f('ix_orders_external_shipment_id'), 'orders', ['external_shipment_id'], unique=False)
|
||||
op.create_index('idx_order_vendor_status', 'orders', ['vendor_id', 'status'], unique=False)
|
||||
op.create_index('idx_order_vendor_channel', 'orders', ['vendor_id', 'channel'], unique=False)
|
||||
op.create_index('idx_order_vendor_date', 'orders', ['vendor_id', 'order_date'], unique=False)
|
||||
op.create_index(op.f("ix_orders_id"), "orders", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_orders_vendor_id"), "orders", ["vendor_id"], unique=False)
|
||||
op.create_index(op.f("ix_orders_customer_id"), "orders", ["customer_id"], unique=False)
|
||||
op.create_index(op.f("ix_orders_order_number"), "orders", ["order_number"], unique=True)
|
||||
op.create_index(op.f("ix_orders_channel"), "orders", ["channel"], unique=False)
|
||||
op.create_index(op.f("ix_orders_status"), "orders", ["status"], unique=False)
|
||||
op.create_index(op.f("ix_orders_external_order_id"), "orders", ["external_order_id"], unique=False)
|
||||
op.create_index(op.f("ix_orders_external_shipment_id"), "orders", ["external_shipment_id"], unique=False)
|
||||
op.create_index("idx_order_vendor_status", "orders", ["vendor_id", "status"], unique=False)
|
||||
op.create_index("idx_order_vendor_channel", "orders", ["vendor_id", "channel"], unique=False)
|
||||
op.create_index("idx_order_vendor_date", "orders", ["vendor_id", "order_date"], unique=False)
|
||||
|
||||
# =========================================================================
|
||||
# Step 3: Create new order_items table
|
||||
# =========================================================================
|
||||
op.create_table('order_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
op.create_table("order_items",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("order_id", sa.Integer(), nullable=False),
|
||||
sa.Column("product_id", sa.Integer(), nullable=False),
|
||||
|
||||
# Product snapshot
|
||||
sa.Column('product_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('product_sku', sa.String(length=100), nullable=True),
|
||||
sa.Column('gtin', sa.String(length=50), nullable=True),
|
||||
sa.Column('gtin_type', sa.String(length=20), nullable=True),
|
||||
sa.Column("product_name", sa.String(length=255), nullable=False),
|
||||
sa.Column("product_sku", sa.String(length=100), nullable=True),
|
||||
sa.Column("gtin", sa.String(length=50), nullable=True),
|
||||
sa.Column("gtin_type", sa.String(length=20), nullable=True),
|
||||
|
||||
# Pricing
|
||||
sa.Column('quantity', sa.Integer(), nullable=False),
|
||||
sa.Column('unit_price', sa.Float(), nullable=False),
|
||||
sa.Column('total_price', sa.Float(), nullable=False),
|
||||
sa.Column("quantity", sa.Integer(), nullable=False),
|
||||
sa.Column("unit_price", sa.Float(), nullable=False),
|
||||
sa.Column("total_price", sa.Float(), nullable=False),
|
||||
|
||||
# External references (for marketplace items)
|
||||
sa.Column('external_item_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('external_variant_id', sa.String(length=100), nullable=True),
|
||||
sa.Column("external_item_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("external_variant_id", sa.String(length=100), nullable=True),
|
||||
|
||||
# Item state (for marketplace confirmation flow)
|
||||
sa.Column('item_state', sa.String(length=50), nullable=True),
|
||||
sa.Column("item_state", sa.String(length=50), nullable=True),
|
||||
|
||||
# Inventory tracking
|
||||
sa.Column('inventory_reserved', sa.Boolean(), server_default='0', nullable=True),
|
||||
sa.Column('inventory_fulfilled', sa.Boolean(), server_default='0', nullable=True),
|
||||
sa.Column("inventory_reserved", sa.Boolean(), server_default="0", nullable=True),
|
||||
sa.Column("inventory_fulfilled", sa.Boolean(), server_default="0", nullable=True),
|
||||
|
||||
# Timestamps
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
|
||||
# Foreign keys
|
||||
sa.ForeignKeyConstraint(['order_id'], ['orders.id']),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
sa.ForeignKeyConstraint(["order_id"], ["orders.id"]),
|
||||
sa.ForeignKeyConstraint(["product_id"], ["products.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
|
||||
# Indexes for order_items
|
||||
op.create_index(op.f('ix_order_items_id'), 'order_items', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_order_items_order_id'), 'order_items', ['order_id'], unique=False)
|
||||
op.create_index(op.f('ix_order_items_product_id'), 'order_items', ['product_id'], unique=False)
|
||||
op.create_index(op.f('ix_order_items_gtin'), 'order_items', ['gtin'], unique=False)
|
||||
op.create_index(op.f("ix_order_items_id"), "order_items", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_order_items_order_id"), "order_items", ["order_id"], unique=False)
|
||||
op.create_index(op.f("ix_order_items_product_id"), "order_items", ["product_id"], unique=False)
|
||||
op.create_index(op.f("ix_order_items_gtin"), "order_items", ["gtin"], unique=False)
|
||||
|
||||
# =========================================================================
|
||||
# Step 4: Create updated letzshop_fulfillment_queue (references orders)
|
||||
# =========================================================================
|
||||
op.create_table('letzshop_fulfillment_queue',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_id', sa.Integer(), nullable=False),
|
||||
op.create_table("letzshop_fulfillment_queue",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("order_id", sa.Integer(), nullable=False),
|
||||
|
||||
# Operation type
|
||||
sa.Column('operation', sa.String(length=50), nullable=False),
|
||||
sa.Column("operation", sa.String(length=50), nullable=False),
|
||||
|
||||
# Operation payload
|
||||
sa.Column('payload', sa.JSON(), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=False),
|
||||
|
||||
# Status and retry
|
||||
sa.Column('status', sa.String(length=50), server_default='pending', nullable=True),
|
||||
sa.Column('attempts', sa.Integer(), server_default='0', nullable=True),
|
||||
sa.Column('max_attempts', sa.Integer(), server_default='3', nullable=True),
|
||||
sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('next_retry_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("status", sa.String(length=50), server_default="pending", nullable=True),
|
||||
sa.Column("attempts", sa.Integer(), server_default="0", nullable=True),
|
||||
sa.Column("max_attempts", sa.Integer(), server_default="3", nullable=True),
|
||||
sa.Column("last_attempt_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("next_retry_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
|
||||
# Response from Letzshop
|
||||
sa.Column('response_data', sa.JSON(), nullable=True),
|
||||
sa.Column("response_data", sa.JSON(), nullable=True),
|
||||
|
||||
# Timestamps
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
|
||||
# Foreign keys
|
||||
sa.ForeignKeyConstraint(['order_id'], ['orders.id']),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
sa.ForeignKeyConstraint(["order_id"], ["orders.id"]),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
|
||||
# Indexes for letzshop_fulfillment_queue
|
||||
op.create_index(op.f('ix_letzshop_fulfillment_queue_id'), 'letzshop_fulfillment_queue', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), 'letzshop_fulfillment_queue', ['vendor_id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_fulfillment_queue_order_id'), 'letzshop_fulfillment_queue', ['order_id'], unique=False)
|
||||
op.create_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue', ['status', 'vendor_id'], unique=False)
|
||||
op.create_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue', ['status', 'next_retry_at'], unique=False)
|
||||
op.create_index('idx_fulfillment_queue_order', 'letzshop_fulfillment_queue', ['order_id'], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_fulfillment_queue_id"), "letzshop_fulfillment_queue", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_fulfillment_queue_vendor_id"), "letzshop_fulfillment_queue", ["vendor_id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_fulfillment_queue_order_id"), "letzshop_fulfillment_queue", ["order_id"], unique=False)
|
||||
op.create_index("idx_fulfillment_queue_status", "letzshop_fulfillment_queue", ["status", "vendor_id"], unique=False)
|
||||
op.create_index("idx_fulfillment_queue_retry", "letzshop_fulfillment_queue", ["status", "next_retry_at"], unique=False)
|
||||
op.create_index("idx_fulfillment_queue_order", "letzshop_fulfillment_queue", ["order_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop new letzshop_fulfillment_queue
|
||||
safe_drop_index('idx_fulfillment_queue_order', 'letzshop_fulfillment_queue')
|
||||
safe_drop_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue')
|
||||
safe_drop_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue')
|
||||
safe_drop_index('ix_letzshop_fulfillment_queue_order_id', 'letzshop_fulfillment_queue')
|
||||
safe_drop_index('ix_letzshop_fulfillment_queue_vendor_id', 'letzshop_fulfillment_queue')
|
||||
safe_drop_index('ix_letzshop_fulfillment_queue_id', 'letzshop_fulfillment_queue')
|
||||
safe_drop_table('letzshop_fulfillment_queue')
|
||||
safe_drop_index("idx_fulfillment_queue_order", "letzshop_fulfillment_queue")
|
||||
safe_drop_index("idx_fulfillment_queue_retry", "letzshop_fulfillment_queue")
|
||||
safe_drop_index("idx_fulfillment_queue_status", "letzshop_fulfillment_queue")
|
||||
safe_drop_index("ix_letzshop_fulfillment_queue_order_id", "letzshop_fulfillment_queue")
|
||||
safe_drop_index("ix_letzshop_fulfillment_queue_vendor_id", "letzshop_fulfillment_queue")
|
||||
safe_drop_index("ix_letzshop_fulfillment_queue_id", "letzshop_fulfillment_queue")
|
||||
safe_drop_table("letzshop_fulfillment_queue")
|
||||
|
||||
# Drop new order_items
|
||||
safe_drop_index('ix_order_items_gtin', 'order_items')
|
||||
safe_drop_index('ix_order_items_product_id', 'order_items')
|
||||
safe_drop_index('ix_order_items_order_id', 'order_items')
|
||||
safe_drop_index('ix_order_items_id', 'order_items')
|
||||
safe_drop_table('order_items')
|
||||
safe_drop_index("ix_order_items_gtin", "order_items")
|
||||
safe_drop_index("ix_order_items_product_id", "order_items")
|
||||
safe_drop_index("ix_order_items_order_id", "order_items")
|
||||
safe_drop_index("ix_order_items_id", "order_items")
|
||||
safe_drop_table("order_items")
|
||||
|
||||
# Drop new orders
|
||||
safe_drop_index('idx_order_vendor_date', 'orders')
|
||||
safe_drop_index('idx_order_vendor_channel', 'orders')
|
||||
safe_drop_index('idx_order_vendor_status', 'orders')
|
||||
safe_drop_index('ix_orders_external_shipment_id', 'orders')
|
||||
safe_drop_index('ix_orders_external_order_id', 'orders')
|
||||
safe_drop_index('ix_orders_status', 'orders')
|
||||
safe_drop_index('ix_orders_channel', 'orders')
|
||||
safe_drop_index('ix_orders_order_number', 'orders')
|
||||
safe_drop_index('ix_orders_customer_id', 'orders')
|
||||
safe_drop_index('ix_orders_vendor_id', 'orders')
|
||||
safe_drop_index('ix_orders_id', 'orders')
|
||||
safe_drop_table('orders')
|
||||
safe_drop_index("idx_order_vendor_date", "orders")
|
||||
safe_drop_index("idx_order_vendor_channel", "orders")
|
||||
safe_drop_index("idx_order_vendor_status", "orders")
|
||||
safe_drop_index("ix_orders_external_shipment_id", "orders")
|
||||
safe_drop_index("ix_orders_external_order_id", "orders")
|
||||
safe_drop_index("ix_orders_status", "orders")
|
||||
safe_drop_index("ix_orders_channel", "orders")
|
||||
safe_drop_index("ix_orders_order_number", "orders")
|
||||
safe_drop_index("ix_orders_customer_id", "orders")
|
||||
safe_drop_index("ix_orders_vendor_id", "orders")
|
||||
safe_drop_index("ix_orders_id", "orders")
|
||||
safe_drop_table("orders")
|
||||
|
||||
# Recreate old orders table
|
||||
op.create_table('orders',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('customer_id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_number', sa.String(), nullable=False),
|
||||
sa.Column('channel', sa.String(length=50), nullable=True, server_default='direct'),
|
||||
sa.Column('external_order_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('external_channel_data', sa.JSON(), nullable=True),
|
||||
sa.Column('status', sa.String(), nullable=False),
|
||||
sa.Column('subtotal', sa.Float(), nullable=False),
|
||||
sa.Column('tax_amount', sa.Float(), nullable=True),
|
||||
sa.Column('shipping_amount', sa.Float(), nullable=True),
|
||||
sa.Column('discount_amount', sa.Float(), nullable=True),
|
||||
sa.Column('total_amount', sa.Float(), nullable=False),
|
||||
sa.Column('currency', sa.String(), nullable=True),
|
||||
sa.Column('shipping_address_id', sa.Integer(), nullable=False),
|
||||
sa.Column('billing_address_id', sa.Integer(), nullable=False),
|
||||
sa.Column('shipping_method', sa.String(), nullable=True),
|
||||
sa.Column('tracking_number', sa.String(), nullable=True),
|
||||
sa.Column('customer_notes', sa.Text(), nullable=True),
|
||||
sa.Column('internal_notes', sa.Text(), nullable=True),
|
||||
sa.Column('paid_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('shipped_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('delivered_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('cancelled_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['billing_address_id'], ['customer_addresses.id']),
|
||||
sa.ForeignKeyConstraint(['customer_id'], ['customers.id']),
|
||||
sa.ForeignKeyConstraint(['shipping_address_id'], ['customer_addresses.id']),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("orders",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("customer_id", sa.Integer(), nullable=False),
|
||||
sa.Column("order_number", sa.String(), nullable=False),
|
||||
sa.Column("channel", sa.String(length=50), nullable=True, server_default="direct"),
|
||||
sa.Column("external_order_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("external_channel_data", sa.JSON(), nullable=True),
|
||||
sa.Column("status", sa.String(), nullable=False),
|
||||
sa.Column("subtotal", sa.Float(), nullable=False),
|
||||
sa.Column("tax_amount", sa.Float(), nullable=True),
|
||||
sa.Column("shipping_amount", sa.Float(), nullable=True),
|
||||
sa.Column("discount_amount", sa.Float(), nullable=True),
|
||||
sa.Column("total_amount", sa.Float(), nullable=False),
|
||||
sa.Column("currency", sa.String(), nullable=True),
|
||||
sa.Column("shipping_address_id", sa.Integer(), nullable=False),
|
||||
sa.Column("billing_address_id", sa.Integer(), nullable=False),
|
||||
sa.Column("shipping_method", sa.String(), nullable=True),
|
||||
sa.Column("tracking_number", sa.String(), nullable=True),
|
||||
sa.Column("customer_notes", sa.Text(), nullable=True),
|
||||
sa.Column("internal_notes", sa.Text(), nullable=True),
|
||||
sa.Column("paid_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("shipped_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("delivered_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("cancelled_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["billing_address_id"], ["customer_addresses.id"]),
|
||||
sa.ForeignKeyConstraint(["customer_id"], ["customers.id"]),
|
||||
sa.ForeignKeyConstraint(["shipping_address_id"], ["customer_addresses.id"]),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_orders_customer_id'), 'orders', ['customer_id'], unique=False)
|
||||
op.create_index(op.f('ix_orders_id'), 'orders', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_orders_order_number'), 'orders', ['order_number'], unique=True)
|
||||
op.create_index(op.f('ix_orders_status'), 'orders', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_orders_vendor_id'), 'orders', ['vendor_id'], unique=False)
|
||||
op.create_index(op.f('ix_orders_channel'), 'orders', ['channel'], unique=False)
|
||||
op.create_index(op.f('ix_orders_external_order_id'), 'orders', ['external_order_id'], unique=False)
|
||||
op.create_index(op.f("ix_orders_customer_id"), "orders", ["customer_id"], unique=False)
|
||||
op.create_index(op.f("ix_orders_id"), "orders", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_orders_order_number"), "orders", ["order_number"], unique=True)
|
||||
op.create_index(op.f("ix_orders_status"), "orders", ["status"], unique=False)
|
||||
op.create_index(op.f("ix_orders_vendor_id"), "orders", ["vendor_id"], unique=False)
|
||||
op.create_index(op.f("ix_orders_channel"), "orders", ["channel"], unique=False)
|
||||
op.create_index(op.f("ix_orders_external_order_id"), "orders", ["external_order_id"], unique=False)
|
||||
|
||||
# Recreate old order_items table
|
||||
op.create_table('order_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_name', sa.String(), nullable=False),
|
||||
sa.Column('product_sku', sa.String(), nullable=True),
|
||||
sa.Column('quantity', sa.Integer(), nullable=False),
|
||||
sa.Column('unit_price', sa.Float(), nullable=False),
|
||||
sa.Column('total_price', sa.Float(), nullable=False),
|
||||
sa.Column('inventory_reserved', sa.Boolean(), nullable=True),
|
||||
sa.Column('inventory_fulfilled', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['order_id'], ['orders.id']),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("order_items",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("order_id", sa.Integer(), nullable=False),
|
||||
sa.Column("product_id", sa.Integer(), nullable=False),
|
||||
sa.Column("product_name", sa.String(), nullable=False),
|
||||
sa.Column("product_sku", sa.String(), nullable=True),
|
||||
sa.Column("quantity", sa.Integer(), nullable=False),
|
||||
sa.Column("unit_price", sa.Float(), nullable=False),
|
||||
sa.Column("total_price", sa.Float(), nullable=False),
|
||||
sa.Column("inventory_reserved", sa.Boolean(), nullable=True),
|
||||
sa.Column("inventory_fulfilled", sa.Boolean(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["order_id"], ["orders.id"]),
|
||||
sa.ForeignKeyConstraint(["product_id"], ["products.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_order_items_id'), 'order_items', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_order_items_order_id'), 'order_items', ['order_id'], unique=False)
|
||||
op.create_index(op.f("ix_order_items_id"), "order_items", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_order_items_order_id"), "order_items", ["order_id"], unique=False)
|
||||
|
||||
# Recreate old letzshop_orders table
|
||||
op.create_table('letzshop_orders',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('letzshop_order_id', sa.String(length=100), nullable=False),
|
||||
sa.Column('letzshop_shipment_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('letzshop_order_number', sa.String(length=100), nullable=True),
|
||||
sa.Column('local_order_id', sa.Integer(), nullable=True),
|
||||
sa.Column('letzshop_state', sa.String(length=50), nullable=True),
|
||||
sa.Column('customer_email', sa.String(length=255), nullable=True),
|
||||
sa.Column('customer_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('total_amount', sa.String(length=50), nullable=True),
|
||||
sa.Column('currency', sa.String(length=10), server_default='EUR', nullable=True),
|
||||
sa.Column('customer_locale', sa.String(length=10), nullable=True),
|
||||
sa.Column('shipping_country_iso', sa.String(length=5), nullable=True),
|
||||
sa.Column('billing_country_iso', sa.String(length=5), nullable=True),
|
||||
sa.Column('order_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('raw_order_data', sa.JSON(), nullable=True),
|
||||
sa.Column('inventory_units', sa.JSON(), nullable=True),
|
||||
sa.Column('sync_status', sa.String(length=50), server_default='pending', nullable=True),
|
||||
sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('sync_error', sa.Text(), nullable=True),
|
||||
sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('rejected_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('tracking_set_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('tracking_number', sa.String(length=100), nullable=True),
|
||||
sa.Column('tracking_carrier', sa.String(length=100), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['local_order_id'], ['orders.id']),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("letzshop_orders",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("letzshop_order_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("letzshop_shipment_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("letzshop_order_number", sa.String(length=100), nullable=True),
|
||||
sa.Column("local_order_id", sa.Integer(), nullable=True),
|
||||
sa.Column("letzshop_state", sa.String(length=50), nullable=True),
|
||||
sa.Column("customer_email", sa.String(length=255), nullable=True),
|
||||
sa.Column("customer_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("total_amount", sa.String(length=50), nullable=True),
|
||||
sa.Column("currency", sa.String(length=10), server_default="EUR", nullable=True),
|
||||
sa.Column("customer_locale", sa.String(length=10), nullable=True),
|
||||
sa.Column("shipping_country_iso", sa.String(length=5), nullable=True),
|
||||
sa.Column("billing_country_iso", sa.String(length=5), nullable=True),
|
||||
sa.Column("order_date", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("raw_order_data", sa.JSON(), nullable=True),
|
||||
sa.Column("inventory_units", sa.JSON(), nullable=True),
|
||||
sa.Column("sync_status", sa.String(length=50), server_default="pending", nullable=True),
|
||||
sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("sync_error", sa.Text(), nullable=True),
|
||||
sa.Column("confirmed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("rejected_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("tracking_set_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("tracking_number", sa.String(length=100), nullable=True),
|
||||
sa.Column("tracking_carrier", sa.String(length=100), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["local_order_id"], ["orders.id"]),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_letzshop_orders_id'), 'letzshop_orders', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_orders_letzshop_order_id'), 'letzshop_orders', ['letzshop_order_id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_orders_letzshop_shipment_id'), 'letzshop_orders', ['letzshop_shipment_id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_orders_vendor_id'), 'letzshop_orders', ['vendor_id'], unique=False)
|
||||
op.create_index('idx_letzshop_order_vendor', 'letzshop_orders', ['vendor_id', 'letzshop_order_id'], unique=False)
|
||||
op.create_index('idx_letzshop_order_state', 'letzshop_orders', ['vendor_id', 'letzshop_state'], unique=False)
|
||||
op.create_index('idx_letzshop_order_sync', 'letzshop_orders', ['vendor_id', 'sync_status'], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_orders_id"), "letzshop_orders", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_orders_letzshop_order_id"), "letzshop_orders", ["letzshop_order_id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_orders_letzshop_shipment_id"), "letzshop_orders", ["letzshop_shipment_id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_orders_vendor_id"), "letzshop_orders", ["vendor_id"], unique=False)
|
||||
op.create_index("idx_letzshop_order_vendor", "letzshop_orders", ["vendor_id", "letzshop_order_id"], unique=False)
|
||||
op.create_index("idx_letzshop_order_state", "letzshop_orders", ["vendor_id", "letzshop_state"], unique=False)
|
||||
op.create_index("idx_letzshop_order_sync", "letzshop_orders", ["vendor_id", "sync_status"], unique=False)
|
||||
|
||||
# Recreate old letzshop_fulfillment_queue table
|
||||
op.create_table('letzshop_fulfillment_queue',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('letzshop_order_id', sa.Integer(), nullable=False),
|
||||
sa.Column('operation', sa.String(length=50), nullable=False),
|
||||
sa.Column('payload', sa.JSON(), nullable=False),
|
||||
sa.Column('status', sa.String(length=50), server_default='pending', nullable=True),
|
||||
sa.Column('attempts', sa.Integer(), server_default='0', nullable=True),
|
||||
sa.Column('max_attempts', sa.Integer(), server_default='3', nullable=True),
|
||||
sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('next_retry_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('response_data', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['letzshop_order_id'], ['letzshop_orders.id']),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("letzshop_fulfillment_queue",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("letzshop_order_id", sa.Integer(), nullable=False),
|
||||
sa.Column("operation", sa.String(length=50), nullable=False),
|
||||
sa.Column("payload", sa.JSON(), nullable=False),
|
||||
sa.Column("status", sa.String(length=50), server_default="pending", nullable=True),
|
||||
sa.Column("attempts", sa.Integer(), server_default="0", nullable=True),
|
||||
sa.Column("max_attempts", sa.Integer(), server_default="3", nullable=True),
|
||||
sa.Column("last_attempt_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("next_retry_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("response_data", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["letzshop_order_id"], ["letzshop_orders.id"]),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_letzshop_fulfillment_queue_id'), 'letzshop_fulfillment_queue', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), 'letzshop_fulfillment_queue', ['vendor_id'], unique=False)
|
||||
op.create_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue', ['status', 'vendor_id'], unique=False)
|
||||
op.create_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue', ['status', 'next_retry_at'], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_fulfillment_queue_id"), "letzshop_fulfillment_queue", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_letzshop_fulfillment_queue_vendor_id"), "letzshop_fulfillment_queue", ["vendor_id"], unique=False)
|
||||
op.create_index("idx_fulfillment_queue_status", "letzshop_fulfillment_queue", ["status", "vendor_id"], unique=False)
|
||||
op.create_index("idx_fulfillment_queue_retry", "letzshop_fulfillment_queue", ["status", "next_retry_at"], unique=False)
|
||||
|
||||
@@ -9,56 +9,56 @@ Adds:
|
||||
- cost_cents to products (for profit calculation)
|
||||
- Letzshop feed settings to vendors (tax_rate, boost_sort, delivery_method, preorder_days)
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c9e22eadf533'
|
||||
down_revision: Union[str, None] = 'e1f2a3b4c5d6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "c9e22eadf533"
|
||||
down_revision: str | None = "e1f2a3b4c5d6"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# === MARKETPLACE PRODUCTS: Add tax_rate_percent ===
|
||||
with op.batch_alter_table('marketplace_products', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('tax_rate_percent', sa.Integer(), nullable=False, server_default='17'))
|
||||
with op.batch_alter_table("marketplace_products", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("tax_rate_percent", sa.Integer(), nullable=False, server_default="17"))
|
||||
|
||||
# === PRODUCTS: Add tax_rate_percent and cost_cents, rename supplier_cost_cents ===
|
||||
with op.batch_alter_table('products', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('tax_rate_percent', sa.Integer(), nullable=False, server_default='17'))
|
||||
batch_op.add_column(sa.Column('cost_cents', sa.Integer(), nullable=True))
|
||||
with op.batch_alter_table("products", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("tax_rate_percent", sa.Integer(), nullable=False, server_default="17"))
|
||||
batch_op.add_column(sa.Column("cost_cents", sa.Integer(), nullable=True))
|
||||
# Drop old supplier_cost_cents column (data migrated to cost_cents if needed)
|
||||
try:
|
||||
batch_op.drop_column('supplier_cost_cents')
|
||||
batch_op.drop_column("supplier_cost_cents")
|
||||
except Exception:
|
||||
pass # Column may not exist
|
||||
|
||||
# === VENDORS: Add Letzshop feed settings ===
|
||||
with op.batch_alter_table('vendors', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('letzshop_default_tax_rate', sa.Integer(), nullable=False, server_default='17'))
|
||||
batch_op.add_column(sa.Column('letzshop_boost_sort', sa.String(length=10), nullable=True, server_default='5.0'))
|
||||
batch_op.add_column(sa.Column('letzshop_delivery_method', sa.String(length=100), nullable=True, server_default='package_delivery'))
|
||||
batch_op.add_column(sa.Column('letzshop_preorder_days', sa.Integer(), nullable=True, server_default='1'))
|
||||
with op.batch_alter_table("vendors", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("letzshop_default_tax_rate", sa.Integer(), nullable=False, server_default="17"))
|
||||
batch_op.add_column(sa.Column("letzshop_boost_sort", sa.String(length=10), nullable=True, server_default="5.0"))
|
||||
batch_op.add_column(sa.Column("letzshop_delivery_method", sa.String(length=100), nullable=True, server_default="package_delivery"))
|
||||
batch_op.add_column(sa.Column("letzshop_preorder_days", sa.Integer(), nullable=True, server_default="1"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# === VENDORS: Remove Letzshop feed settings ===
|
||||
with op.batch_alter_table('vendors', schema=None) as batch_op:
|
||||
batch_op.drop_column('letzshop_preorder_days')
|
||||
batch_op.drop_column('letzshop_delivery_method')
|
||||
batch_op.drop_column('letzshop_boost_sort')
|
||||
batch_op.drop_column('letzshop_default_tax_rate')
|
||||
with op.batch_alter_table("vendors", schema=None) as batch_op:
|
||||
batch_op.drop_column("letzshop_preorder_days")
|
||||
batch_op.drop_column("letzshop_delivery_method")
|
||||
batch_op.drop_column("letzshop_boost_sort")
|
||||
batch_op.drop_column("letzshop_default_tax_rate")
|
||||
|
||||
# === PRODUCTS: Remove tax_rate_percent and cost_cents ===
|
||||
with op.batch_alter_table('products', schema=None) as batch_op:
|
||||
batch_op.drop_column('cost_cents')
|
||||
batch_op.drop_column('tax_rate_percent')
|
||||
batch_op.add_column(sa.Column('supplier_cost_cents', sa.Integer(), nullable=True))
|
||||
with op.batch_alter_table("products", schema=None) as batch_op:
|
||||
batch_op.drop_column("cost_cents")
|
||||
batch_op.drop_column("tax_rate_percent")
|
||||
batch_op.add_column(sa.Column("supplier_cost_cents", sa.Integer(), nullable=True))
|
||||
|
||||
# === MARKETPLACE PRODUCTS: Remove tax_rate_percent ===
|
||||
with op.batch_alter_table('marketplace_products', schema=None) as batch_op:
|
||||
batch_op.drop_column('tax_rate_percent')
|
||||
with op.batch_alter_table("marketplace_products", schema=None) as batch_op:
|
||||
batch_op.drop_column("tax_rate_percent")
|
||||
|
||||
@@ -5,33 +5,33 @@ Revises: a9a86cef6cca
|
||||
Create Date: 2025-12-18 20:54:55.185857
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'cb88bc9b5f86'
|
||||
down_revision: Union[str, None] = 'a9a86cef6cca'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "cb88bc9b5f86"
|
||||
down_revision: str | None = "a9a86cef6cca"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add GTIN (EAN/UPC barcode) columns to products table for order EAN matching
|
||||
# gtin: The barcode number (e.g., "0889698273022")
|
||||
# gtin_type: The format type from Letzshop (e.g., "gtin13", "gtin14", "isbn13")
|
||||
op.add_column('products', sa.Column('gtin', sa.String(length=50), nullable=True))
|
||||
op.add_column('products', sa.Column('gtin_type', sa.String(length=20), nullable=True))
|
||||
op.add_column("products", sa.Column("gtin", sa.String(length=50), nullable=True))
|
||||
op.add_column("products", sa.Column("gtin_type", sa.String(length=20), nullable=True))
|
||||
|
||||
# Add index for EAN lookups during order matching
|
||||
op.create_index('idx_product_gtin', 'products', ['gtin'], unique=False)
|
||||
op.create_index('idx_product_vendor_gtin', 'products', ['vendor_id', 'gtin'], unique=False)
|
||||
op.create_index("idx_product_gtin", "products", ["gtin"], unique=False)
|
||||
op.create_index("idx_product_vendor_gtin", "products", ["vendor_id", "gtin"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('idx_product_vendor_gtin', table_name='products')
|
||||
op.drop_index('idx_product_gtin', table_name='products')
|
||||
op.drop_column('products', 'gtin_type')
|
||||
op.drop_column('products', 'gtin')
|
||||
op.drop_index("idx_product_vendor_gtin", table_name="products")
|
||||
op.drop_index("idx_product_gtin", table_name="products")
|
||||
op.drop_column("products", "gtin_type")
|
||||
op.drop_column("products", "gtin")
|
||||
|
||||
@@ -5,73 +5,73 @@ Revises: 0bd9ffaaced1
|
||||
Create Date: 2025-11-30 14:58:17.165142
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'd0325d7c0f25'
|
||||
down_revision: Union[str, None] = '0bd9ffaaced1'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "d0325d7c0f25"
|
||||
down_revision: str | None = "0bd9ffaaced1"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create companies table
|
||||
op.create_table(
|
||||
'companies',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('owner_user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('contact_email', sa.String(), nullable=False),
|
||||
sa.Column('contact_phone', sa.String(), nullable=True),
|
||||
sa.Column('website', sa.String(), nullable=True),
|
||||
sa.Column('business_address', sa.Text(), nullable=True),
|
||||
sa.Column('tax_number', sa.String(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('is_verified', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(['owner_user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
"companies",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("owner_user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("contact_email", sa.String(), nullable=False),
|
||||
sa.Column("contact_phone", sa.String(), nullable=True),
|
||||
sa.Column("website", sa.String(), nullable=True),
|
||||
sa.Column("business_address", sa.Text(), nullable=True),
|
||||
sa.Column("tax_number", sa.String(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("is_verified", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(["owner_user_id"], ["users.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_companies_id'), 'companies', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_companies_name'), 'companies', ['name'], unique=False)
|
||||
op.create_index(op.f("ix_companies_id"), "companies", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_companies_name"), "companies", ["name"], unique=False)
|
||||
|
||||
# Use batch mode for SQLite to modify vendors table
|
||||
with op.batch_alter_table('vendors', schema=None) as batch_op:
|
||||
with op.batch_alter_table("vendors", schema=None) as batch_op:
|
||||
# Add company_id column
|
||||
batch_op.add_column(sa.Column('company_id', sa.Integer(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_vendors_company_id'), ['company_id'], unique=False)
|
||||
batch_op.create_foreign_key('fk_vendors_company_id', 'companies', ['company_id'], ['id'])
|
||||
batch_op.add_column(sa.Column("company_id", sa.Integer(), nullable=True))
|
||||
batch_op.create_index(batch_op.f("ix_vendors_company_id"), ["company_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_vendors_company_id", "companies", ["company_id"], ["id"])
|
||||
|
||||
# Remove old contact fields
|
||||
batch_op.drop_column('contact_email')
|
||||
batch_op.drop_column('contact_phone')
|
||||
batch_op.drop_column('website')
|
||||
batch_op.drop_column('business_address')
|
||||
batch_op.drop_column('tax_number')
|
||||
batch_op.drop_column("contact_email")
|
||||
batch_op.drop_column("contact_phone")
|
||||
batch_op.drop_column("website")
|
||||
batch_op.drop_column("business_address")
|
||||
batch_op.drop_column("tax_number")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Use batch mode for SQLite to modify vendors table
|
||||
with op.batch_alter_table('vendors', schema=None) as batch_op:
|
||||
with op.batch_alter_table("vendors", schema=None) as batch_op:
|
||||
# Re-add contact fields to vendors
|
||||
batch_op.add_column(sa.Column('tax_number', sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column('business_address', sa.Text(), nullable=True))
|
||||
batch_op.add_column(sa.Column('website', sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column('contact_phone', sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column('contact_email', sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column("tax_number", sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column("business_address", sa.Text(), nullable=True))
|
||||
batch_op.add_column(sa.Column("website", sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column("contact_phone", sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column("contact_email", sa.String(), nullable=True))
|
||||
|
||||
# Remove company_id from vendors
|
||||
batch_op.drop_constraint('fk_vendors_company_id', type_='foreignkey')
|
||||
batch_op.drop_index(batch_op.f('ix_vendors_company_id'))
|
||||
batch_op.drop_column('company_id')
|
||||
batch_op.drop_constraint("fk_vendors_company_id", type_="foreignkey")
|
||||
batch_op.drop_index(batch_op.f("ix_vendors_company_id"))
|
||||
batch_op.drop_column("company_id")
|
||||
|
||||
# Drop companies table
|
||||
op.drop_index(op.f('ix_companies_name'), table_name='companies')
|
||||
op.drop_index(op.f('ix_companies_id'), table_name='companies')
|
||||
op.drop_table('companies')
|
||||
op.drop_index(op.f("ix_companies_name"), table_name="companies")
|
||||
op.drop_index(op.f("ix_companies_id"), table_name="companies")
|
||||
op.drop_table("companies")
|
||||
|
||||
@@ -12,25 +12,25 @@ The exception system allows marketplace orders to be imported even when
|
||||
products are not found by GTIN. Items are linked to a placeholder product
|
||||
and exceptions are tracked for QC resolution.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'd2e3f4a5b6c7'
|
||||
down_revision: Union[str, None] = 'c1d2e3f4a5b6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "d2e3f4a5b6c7"
|
||||
down_revision: str | None = "c1d2e3f4a5b6"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def column_exists(table_name: str, column_name: str) -> bool:
|
||||
"""Check if a column exists in a table."""
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
columns = [col['name'] for col in inspector.get_columns(table_name)]
|
||||
columns = [col["name"] for col in inspector.get_columns(table_name)]
|
||||
return column_name in columns
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def index_exists(index_name: str, table_name: str) -> bool:
|
||||
inspector = inspect(bind)
|
||||
try:
|
||||
indexes = inspector.get_indexes(table_name)
|
||||
return any(idx['name'] == index_name for idx in indexes)
|
||||
return any(idx["name"] == index_name for idx in indexes)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -56,124 +56,124 @@ def upgrade() -> None:
|
||||
# =========================================================================
|
||||
# Step 1: Add needs_product_match column to order_items
|
||||
# =========================================================================
|
||||
if not column_exists('order_items', 'needs_product_match'):
|
||||
if not column_exists("order_items", "needs_product_match"):
|
||||
op.add_column(
|
||||
'order_items',
|
||||
"order_items",
|
||||
sa.Column(
|
||||
'needs_product_match',
|
||||
"needs_product_match",
|
||||
sa.Boolean(),
|
||||
server_default='0',
|
||||
server_default="0",
|
||||
nullable=False
|
||||
)
|
||||
)
|
||||
|
||||
if not index_exists('ix_order_items_needs_product_match', 'order_items'):
|
||||
if not index_exists("ix_order_items_needs_product_match", "order_items"):
|
||||
op.create_index(
|
||||
'ix_order_items_needs_product_match',
|
||||
'order_items',
|
||||
['needs_product_match']
|
||||
"ix_order_items_needs_product_match",
|
||||
"order_items",
|
||||
["needs_product_match"]
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Step 2: Create order_item_exceptions table
|
||||
# =========================================================================
|
||||
if not table_exists('order_item_exceptions'):
|
||||
if not table_exists("order_item_exceptions"):
|
||||
op.create_table(
|
||||
'order_item_exceptions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_item_id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('original_gtin', sa.String(length=50), nullable=True),
|
||||
sa.Column('original_product_name', sa.String(length=500), nullable=True),
|
||||
sa.Column('original_sku', sa.String(length=100), nullable=True),
|
||||
"order_item_exceptions",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("order_item_id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
sa.Column("original_gtin", sa.String(length=50), nullable=True),
|
||||
sa.Column("original_product_name", sa.String(length=500), nullable=True),
|
||||
sa.Column("original_sku", sa.String(length=100), nullable=True),
|
||||
sa.Column(
|
||||
'exception_type',
|
||||
"exception_type",
|
||||
sa.String(length=50),
|
||||
nullable=False,
|
||||
server_default='product_not_found'
|
||||
server_default="product_not_found"
|
||||
),
|
||||
sa.Column(
|
||||
'status',
|
||||
"status",
|
||||
sa.String(length=50),
|
||||
nullable=False,
|
||||
server_default='pending'
|
||||
server_default="pending"
|
||||
),
|
||||
sa.Column('resolved_product_id', sa.Integer(), nullable=True),
|
||||
sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('resolved_by', sa.Integer(), nullable=True),
|
||||
sa.Column('resolution_notes', sa.Text(), nullable=True),
|
||||
sa.Column("resolved_product_id", sa.Integer(), nullable=True),
|
||||
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("resolved_by", sa.Integer(), nullable=True),
|
||||
sa.Column("resolution_notes", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
'created_at',
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text('(CURRENT_TIMESTAMP)'),
|
||||
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
||||
nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
'updated_at',
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text('(CURRENT_TIMESTAMP)'),
|
||||
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
||||
nullable=False
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['order_item_id'],
|
||||
['order_items.id'],
|
||||
ondelete='CASCADE'
|
||||
["order_item_id"],
|
||||
["order_items.id"],
|
||||
ondelete="CASCADE"
|
||||
),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']),
|
||||
sa.ForeignKeyConstraint(['resolved_product_id'], ['products.id']),
|
||||
sa.ForeignKeyConstraint(['resolved_by'], ['users.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"]),
|
||||
sa.ForeignKeyConstraint(["resolved_product_id"], ["products.id"]),
|
||||
sa.ForeignKeyConstraint(["resolved_by"], ["users.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
|
||||
# Create indexes
|
||||
op.create_index(
|
||||
'ix_order_item_exceptions_id',
|
||||
'order_item_exceptions',
|
||||
['id']
|
||||
"ix_order_item_exceptions_id",
|
||||
"order_item_exceptions",
|
||||
["id"]
|
||||
)
|
||||
op.create_index(
|
||||
'ix_order_item_exceptions_vendor_id',
|
||||
'order_item_exceptions',
|
||||
['vendor_id']
|
||||
"ix_order_item_exceptions_vendor_id",
|
||||
"order_item_exceptions",
|
||||
["vendor_id"]
|
||||
)
|
||||
op.create_index(
|
||||
'ix_order_item_exceptions_status',
|
||||
'order_item_exceptions',
|
||||
['status']
|
||||
"ix_order_item_exceptions_status",
|
||||
"order_item_exceptions",
|
||||
["status"]
|
||||
)
|
||||
op.create_index(
|
||||
'idx_exception_vendor_status',
|
||||
'order_item_exceptions',
|
||||
['vendor_id', 'status']
|
||||
"idx_exception_vendor_status",
|
||||
"order_item_exceptions",
|
||||
["vendor_id", "status"]
|
||||
)
|
||||
op.create_index(
|
||||
'idx_exception_gtin',
|
||||
'order_item_exceptions',
|
||||
['vendor_id', 'original_gtin']
|
||||
"idx_exception_gtin",
|
||||
"order_item_exceptions",
|
||||
["vendor_id", "original_gtin"]
|
||||
)
|
||||
|
||||
# Unique constraint on order_item_id (one exception per item)
|
||||
op.create_index(
|
||||
'uq_order_item_exception',
|
||||
'order_item_exceptions',
|
||||
['order_item_id'],
|
||||
"uq_order_item_exception",
|
||||
"order_item_exceptions",
|
||||
["order_item_id"],
|
||||
unique=True
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop order_item_exceptions table
|
||||
if table_exists('order_item_exceptions'):
|
||||
op.drop_index('uq_order_item_exception', table_name='order_item_exceptions')
|
||||
op.drop_index('idx_exception_gtin', table_name='order_item_exceptions')
|
||||
op.drop_index('idx_exception_vendor_status', table_name='order_item_exceptions')
|
||||
op.drop_index('ix_order_item_exceptions_status', table_name='order_item_exceptions')
|
||||
op.drop_index('ix_order_item_exceptions_vendor_id', table_name='order_item_exceptions')
|
||||
op.drop_index('ix_order_item_exceptions_id', table_name='order_item_exceptions')
|
||||
op.drop_table('order_item_exceptions')
|
||||
if table_exists("order_item_exceptions"):
|
||||
op.drop_index("uq_order_item_exception", table_name="order_item_exceptions")
|
||||
op.drop_index("idx_exception_gtin", table_name="order_item_exceptions")
|
||||
op.drop_index("idx_exception_vendor_status", table_name="order_item_exceptions")
|
||||
op.drop_index("ix_order_item_exceptions_status", table_name="order_item_exceptions")
|
||||
op.drop_index("ix_order_item_exceptions_vendor_id", table_name="order_item_exceptions")
|
||||
op.drop_index("ix_order_item_exceptions_id", table_name="order_item_exceptions")
|
||||
op.drop_table("order_item_exceptions")
|
||||
|
||||
# Remove needs_product_match column from order_items
|
||||
if column_exists('order_items', 'needs_product_match'):
|
||||
if index_exists('ix_order_items_needs_product_match', 'order_items'):
|
||||
op.drop_index('ix_order_items_needs_product_match', table_name='order_items')
|
||||
op.drop_column('order_items', 'needs_product_match')
|
||||
if column_exists("order_items", "needs_product_match"):
|
||||
if index_exists("ix_order_items_needs_product_match", "order_items"):
|
||||
op.drop_index("ix_order_items_needs_product_match", table_name="order_items")
|
||||
op.drop_column("order_items", "needs_product_match")
|
||||
|
||||
@@ -5,87 +5,87 @@ Revises: 404b3e2d2865
|
||||
Create Date: 2025-12-27 20:48:00.661523
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import text
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'd7a4a3f06394'
|
||||
down_revision: Union[str, None] = '404b3e2d2865'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "d7a4a3f06394"
|
||||
down_revision: str | None = "404b3e2d2865"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create email_templates table
|
||||
op.create_table('email_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=100), nullable=False),
|
||||
sa.Column('language', sa.String(length=5), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('category', sa.String(length=50), nullable=False),
|
||||
sa.Column('subject', sa.String(length=500), nullable=False),
|
||||
sa.Column('body_html', sa.Text(), nullable=False),
|
||||
sa.Column('body_text', sa.Text(), nullable=True),
|
||||
sa.Column('variables', sa.Text(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
op.create_table("email_templates",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("code", sa.String(length=100), nullable=False),
|
||||
sa.Column("language", sa.String(length=5), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("category", sa.String(length=50), nullable=False),
|
||||
sa.Column("subject", sa.String(length=500), nullable=False),
|
||||
sa.Column("body_html", sa.Text(), nullable=False),
|
||||
sa.Column("body_text", sa.Text(), nullable=True),
|
||||
sa.Column("variables", sa.Text(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f('ix_email_templates_category'), 'email_templates', ['category'], unique=False)
|
||||
op.create_index(op.f('ix_email_templates_code'), 'email_templates', ['code'], unique=False)
|
||||
op.create_index(op.f('ix_email_templates_id'), 'email_templates', ['id'], unique=False)
|
||||
op.create_index(op.f("ix_email_templates_category"), "email_templates", ["category"], unique=False)
|
||||
op.create_index(op.f("ix_email_templates_code"), "email_templates", ["code"], unique=False)
|
||||
op.create_index(op.f("ix_email_templates_id"), "email_templates", ["id"], unique=False)
|
||||
|
||||
# Create email_logs table
|
||||
op.create_table('email_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('template_code', sa.String(length=100), nullable=True),
|
||||
sa.Column('template_id', sa.Integer(), nullable=True),
|
||||
sa.Column('recipient_email', sa.String(length=255), nullable=False),
|
||||
sa.Column('recipient_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('subject', sa.String(length=500), nullable=False),
|
||||
sa.Column('body_html', sa.Text(), nullable=True),
|
||||
sa.Column('body_text', sa.Text(), nullable=True),
|
||||
sa.Column('from_email', sa.String(length=255), nullable=False),
|
||||
sa.Column('from_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('reply_to', sa.String(length=255), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=False),
|
||||
sa.Column('sent_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('delivered_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('opened_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('clicked_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('retry_count', sa.Integer(), nullable=False),
|
||||
sa.Column('provider', sa.String(length=50), nullable=True),
|
||||
sa.Column('provider_message_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('related_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('related_id', sa.Integer(), nullable=True),
|
||||
sa.Column('extra_data', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['template_id'], ['email_templates.id']),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id']),
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table("email_logs",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("template_code", sa.String(length=100), nullable=True),
|
||||
sa.Column("template_id", sa.Integer(), nullable=True),
|
||||
sa.Column("recipient_email", sa.String(length=255), nullable=False),
|
||||
sa.Column("recipient_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("subject", sa.String(length=500), nullable=False),
|
||||
sa.Column("body_html", sa.Text(), nullable=True),
|
||||
sa.Column("body_text", sa.Text(), nullable=True),
|
||||
sa.Column("from_email", sa.String(length=255), nullable=False),
|
||||
sa.Column("from_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("reply_to", sa.String(length=255), nullable=True),
|
||||
sa.Column("status", sa.String(length=20), nullable=False),
|
||||
sa.Column("sent_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("delivered_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("opened_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("clicked_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("retry_count", sa.Integer(), nullable=False),
|
||||
sa.Column("provider", sa.String(length=50), nullable=True),
|
||||
sa.Column("provider_message_id", sa.String(length=255), nullable=True),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=True),
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("related_type", sa.String(length=50), nullable=True),
|
||||
sa.Column("related_id", sa.Integer(), nullable=True),
|
||||
sa.Column("extra_data", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["template_id"], ["email_templates.id"]),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"]),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_index(op.f('ix_email_logs_id'), 'email_logs', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_email_logs_provider_message_id'), 'email_logs', ['provider_message_id'], unique=False)
|
||||
op.create_index(op.f('ix_email_logs_recipient_email'), 'email_logs', ['recipient_email'], unique=False)
|
||||
op.create_index(op.f('ix_email_logs_status'), 'email_logs', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_email_logs_template_code'), 'email_logs', ['template_code'], unique=False)
|
||||
op.create_index(op.f('ix_email_logs_user_id'), 'email_logs', ['user_id'], unique=False)
|
||||
op.create_index(op.f('ix_email_logs_vendor_id'), 'email_logs', ['vendor_id'], unique=False)
|
||||
op.create_index(op.f("ix_email_logs_id"), "email_logs", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_email_logs_provider_message_id"), "email_logs", ["provider_message_id"], unique=False)
|
||||
op.create_index(op.f("ix_email_logs_recipient_email"), "email_logs", ["recipient_email"], unique=False)
|
||||
op.create_index(op.f("ix_email_logs_status"), "email_logs", ["status"], unique=False)
|
||||
op.create_index(op.f("ix_email_logs_template_code"), "email_logs", ["template_code"], unique=False)
|
||||
op.create_index(op.f("ix_email_logs_user_id"), "email_logs", ["user_id"], unique=False)
|
||||
op.create_index(op.f("ix_email_logs_vendor_id"), "email_logs", ["vendor_id"], unique=False)
|
||||
|
||||
# application_logs - alter columns
|
||||
op.alter_column('application_logs', 'created_at', existing_type=sa.DATETIME(), nullable=False)
|
||||
op.alter_column('application_logs', 'updated_at', existing_type=sa.DATETIME(), nullable=False)
|
||||
op.alter_column("application_logs", "created_at", existing_type=sa.DATETIME(), nullable=False)
|
||||
op.alter_column("application_logs", "updated_at", existing_type=sa.DATETIME(), nullable=False)
|
||||
|
||||
# capacity_snapshots indexes (PostgreSQL IF EXISTS/IF NOT EXISTS)
|
||||
op.execute(text("DROP INDEX IF EXISTS ix_capacity_snapshots_date"))
|
||||
@@ -93,17 +93,17 @@ def upgrade() -> None:
|
||||
op.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_capacity_snapshots_snapshot_date ON capacity_snapshots (snapshot_date)"))
|
||||
|
||||
# cart_items - alter columns
|
||||
op.alter_column('cart_items', 'created_at', existing_type=sa.DATETIME(), nullable=False)
|
||||
op.alter_column('cart_items', 'updated_at', existing_type=sa.DATETIME(), nullable=False)
|
||||
op.alter_column("cart_items", "created_at", existing_type=sa.DATETIME(), nullable=False)
|
||||
op.alter_column("cart_items", "updated_at", existing_type=sa.DATETIME(), nullable=False)
|
||||
|
||||
# customer_addresses index rename
|
||||
op.execute(text("DROP INDEX IF EXISTS ix_customers_addresses_id"))
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS ix_customer_addresses_id ON customer_addresses (id)"))
|
||||
|
||||
# inventory - alter columns and constraints
|
||||
op.alter_column('inventory', 'warehouse', existing_type=sa.VARCHAR(), nullable=False)
|
||||
op.alter_column('inventory', 'bin_location', existing_type=sa.VARCHAR(), nullable=False)
|
||||
op.alter_column('inventory', 'location', existing_type=sa.VARCHAR(), nullable=True)
|
||||
op.alter_column("inventory", "warehouse", existing_type=sa.VARCHAR(), nullable=False)
|
||||
op.alter_column("inventory", "bin_location", existing_type=sa.VARCHAR(), nullable=False)
|
||||
op.alter_column("inventory", "location", existing_type=sa.VARCHAR(), nullable=True)
|
||||
op.execute(text("DROP INDEX IF EXISTS idx_inventory_product_location"))
|
||||
op.execute(text("ALTER TABLE inventory DROP CONSTRAINT IF EXISTS uq_inventory_product_location"))
|
||||
op.execute(text("""
|
||||
@@ -120,8 +120,8 @@ def upgrade() -> None:
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS ix_marketplace_product_translations_id ON marketplace_product_translations (id)"))
|
||||
|
||||
# marketplace_products - alter columns
|
||||
op.alter_column('marketplace_products', 'is_digital', existing_type=sa.BOOLEAN(), nullable=True)
|
||||
op.alter_column('marketplace_products', 'is_active', existing_type=sa.BOOLEAN(), nullable=True)
|
||||
op.alter_column("marketplace_products", "is_digital", existing_type=sa.BOOLEAN(), nullable=True)
|
||||
op.alter_column("marketplace_products", "is_active", existing_type=sa.BOOLEAN(), nullable=True)
|
||||
|
||||
# marketplace_products indexes
|
||||
op.execute(text("DROP INDEX IF EXISTS idx_mp_is_active"))
|
||||
@@ -146,7 +146,7 @@ def upgrade() -> None:
|
||||
"""))
|
||||
|
||||
# order_items - alter column
|
||||
op.alter_column('order_items', 'needs_product_match', existing_type=sa.BOOLEAN(), nullable=True)
|
||||
op.alter_column("order_items", "needs_product_match", existing_type=sa.BOOLEAN(), nullable=True)
|
||||
|
||||
# order_items indexes
|
||||
op.execute(text("DROP INDEX IF EXISTS ix_order_items_gtin"))
|
||||
@@ -185,7 +185,7 @@ def upgrade() -> None:
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendor_domains_id ON vendor_domains (id)"))
|
||||
|
||||
# vendor_subscriptions - alter column and FK
|
||||
op.alter_column('vendor_subscriptions', 'payment_retry_count', existing_type=sa.INTEGER(), nullable=False)
|
||||
op.alter_column("vendor_subscriptions", "payment_retry_count", existing_type=sa.INTEGER(), nullable=False)
|
||||
op.execute(text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
@@ -207,12 +207,12 @@ def upgrade() -> None:
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS ix_vendor_users_invitation_token ON vendor_users (invitation_token)"))
|
||||
|
||||
# vendors - alter column
|
||||
op.alter_column('vendors', 'company_id', existing_type=sa.INTEGER(), nullable=False)
|
||||
op.alter_column("vendors", "company_id", existing_type=sa.INTEGER(), nullable=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# vendors
|
||||
op.alter_column('vendors', 'company_id', existing_type=sa.INTEGER(), nullable=True)
|
||||
op.alter_column("vendors", "company_id", existing_type=sa.INTEGER(), nullable=True)
|
||||
|
||||
# vendor_users indexes
|
||||
op.execute(text("DROP INDEX IF EXISTS ix_vendor_users_invitation_token"))
|
||||
@@ -226,7 +226,7 @@ def downgrade() -> None:
|
||||
|
||||
# vendor_subscriptions
|
||||
op.execute(text("ALTER TABLE vendor_subscriptions DROP CONSTRAINT IF EXISTS fk_vendor_subscriptions_tier_id"))
|
||||
op.alter_column('vendor_subscriptions', 'payment_retry_count', existing_type=sa.INTEGER(), nullable=True)
|
||||
op.alter_column("vendor_subscriptions", "payment_retry_count", existing_type=sa.INTEGER(), nullable=True)
|
||||
|
||||
# vendor_domains indexes
|
||||
op.execute(text("DROP INDEX IF EXISTS ix_vendor_domains_id"))
|
||||
@@ -260,7 +260,7 @@ def downgrade() -> None:
|
||||
# order_items
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS ix_order_items_product_id ON order_items (product_id)"))
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS ix_order_items_gtin ON order_items (gtin)"))
|
||||
op.alter_column('order_items', 'needs_product_match', existing_type=sa.BOOLEAN(), nullable=False)
|
||||
op.alter_column("order_items", "needs_product_match", existing_type=sa.BOOLEAN(), nullable=False)
|
||||
|
||||
# order_item_exceptions
|
||||
op.execute(text("ALTER TABLE order_item_exceptions DROP CONSTRAINT IF EXISTS uq_order_item_exceptions_order_item_id"))
|
||||
@@ -278,8 +278,8 @@ def downgrade() -> None:
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS idx_mp_is_active ON marketplace_products (is_active)"))
|
||||
|
||||
# marketplace_products columns
|
||||
op.alter_column('marketplace_products', 'is_active', existing_type=sa.BOOLEAN(), nullable=False)
|
||||
op.alter_column('marketplace_products', 'is_digital', existing_type=sa.BOOLEAN(), nullable=False)
|
||||
op.alter_column("marketplace_products", "is_active", existing_type=sa.BOOLEAN(), nullable=False)
|
||||
op.alter_column("marketplace_products", "is_digital", existing_type=sa.BOOLEAN(), nullable=False)
|
||||
|
||||
# marketplace imports
|
||||
op.execute(text("DROP INDEX IF EXISTS ix_marketplace_product_translations_id"))
|
||||
@@ -296,17 +296,17 @@ def downgrade() -> None:
|
||||
END $$;
|
||||
"""))
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS idx_inventory_product_location ON inventory (product_id, location)"))
|
||||
op.alter_column('inventory', 'location', existing_type=sa.VARCHAR(), nullable=False)
|
||||
op.alter_column('inventory', 'bin_location', existing_type=sa.VARCHAR(), nullable=True)
|
||||
op.alter_column('inventory', 'warehouse', existing_type=sa.VARCHAR(), nullable=True)
|
||||
op.alter_column("inventory", "location", existing_type=sa.VARCHAR(), nullable=False)
|
||||
op.alter_column("inventory", "bin_location", existing_type=sa.VARCHAR(), nullable=True)
|
||||
op.alter_column("inventory", "warehouse", existing_type=sa.VARCHAR(), nullable=True)
|
||||
|
||||
# customer_addresses
|
||||
op.execute(text("DROP INDEX IF EXISTS ix_customer_addresses_id"))
|
||||
op.execute(text("CREATE INDEX IF NOT EXISTS ix_customers_addresses_id ON customer_addresses (id)"))
|
||||
|
||||
# cart_items
|
||||
op.alter_column('cart_items', 'updated_at', existing_type=sa.DATETIME(), nullable=True)
|
||||
op.alter_column('cart_items', 'created_at', existing_type=sa.DATETIME(), nullable=True)
|
||||
op.alter_column("cart_items", "updated_at", existing_type=sa.DATETIME(), nullable=True)
|
||||
op.alter_column("cart_items", "created_at", existing_type=sa.DATETIME(), nullable=True)
|
||||
|
||||
# capacity_snapshots
|
||||
op.execute(text("DROP INDEX IF EXISTS ix_capacity_snapshots_snapshot_date"))
|
||||
@@ -314,19 +314,19 @@ def downgrade() -> None:
|
||||
op.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_capacity_snapshots_date ON capacity_snapshots (snapshot_date)"))
|
||||
|
||||
# application_logs
|
||||
op.alter_column('application_logs', 'updated_at', existing_type=sa.DATETIME(), nullable=True)
|
||||
op.alter_column('application_logs', 'created_at', existing_type=sa.DATETIME(), nullable=True)
|
||||
op.alter_column("application_logs", "updated_at", existing_type=sa.DATETIME(), nullable=True)
|
||||
op.alter_column("application_logs", "created_at", existing_type=sa.DATETIME(), nullable=True)
|
||||
|
||||
# Drop email tables
|
||||
op.drop_index(op.f('ix_email_logs_vendor_id'), table_name='email_logs')
|
||||
op.drop_index(op.f('ix_email_logs_user_id'), table_name='email_logs')
|
||||
op.drop_index(op.f('ix_email_logs_template_code'), table_name='email_logs')
|
||||
op.drop_index(op.f('ix_email_logs_status'), table_name='email_logs')
|
||||
op.drop_index(op.f('ix_email_logs_recipient_email'), table_name='email_logs')
|
||||
op.drop_index(op.f('ix_email_logs_provider_message_id'), table_name='email_logs')
|
||||
op.drop_index(op.f('ix_email_logs_id'), table_name='email_logs')
|
||||
op.drop_table('email_logs')
|
||||
op.drop_index(op.f('ix_email_templates_id'), table_name='email_templates')
|
||||
op.drop_index(op.f('ix_email_templates_code'), table_name='email_templates')
|
||||
op.drop_index(op.f('ix_email_templates_category'), table_name='email_templates')
|
||||
op.drop_table('email_templates')
|
||||
op.drop_index(op.f("ix_email_logs_vendor_id"), table_name="email_logs")
|
||||
op.drop_index(op.f("ix_email_logs_user_id"), table_name="email_logs")
|
||||
op.drop_index(op.f("ix_email_logs_template_code"), table_name="email_logs")
|
||||
op.drop_index(op.f("ix_email_logs_status"), table_name="email_logs")
|
||||
op.drop_index(op.f("ix_email_logs_recipient_email"), table_name="email_logs")
|
||||
op.drop_index(op.f("ix_email_logs_provider_message_id"), table_name="email_logs")
|
||||
op.drop_index(op.f("ix_email_logs_id"), table_name="email_logs")
|
||||
op.drop_table("email_logs")
|
||||
op.drop_index(op.f("ix_email_templates_id"), table_name="email_templates")
|
||||
op.drop_index(op.f("ix_email_templates_code"), table_name="email_templates")
|
||||
op.drop_index(op.f("ix_email_templates_category"), table_name="email_templates")
|
||||
op.drop_table("email_templates")
|
||||
|
||||
@@ -17,16 +17,17 @@ It also renames 'product_type' to 'product_type_raw' to preserve the original
|
||||
Google Shopping feed value while using 'product_type' for the new enum.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "e1a2b3c4d5e6"
|
||||
down_revision: Union[str, None] = "28d44d503cac"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "28d44d503cac"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,18 +5,18 @@ Revises: j8e9f0a1b2c3
|
||||
Create Date: 2025-12-25 12:21:24.006548
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import text
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e1bfb453fbe9'
|
||||
down_revision: Union[str, None] = 'j8e9f0a1b2c3'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "e1bfb453fbe9"
|
||||
down_revision: str | None = "j8e9f0a1b2c3"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def get_column_names(conn, table_name: str) -> set:
|
||||
@@ -43,11 +43,11 @@ def upgrade() -> None:
|
||||
# Check if columns already exist (idempotent)
|
||||
columns = get_column_names(conn, "inventory")
|
||||
|
||||
if 'warehouse' not in columns:
|
||||
op.add_column('inventory', sa.Column('warehouse', sa.String(), nullable=False, server_default='strassen'))
|
||||
if "warehouse" not in columns:
|
||||
op.add_column("inventory", sa.Column("warehouse", sa.String(), nullable=False, server_default="strassen"))
|
||||
|
||||
if 'bin_location' not in columns:
|
||||
op.add_column('inventory', sa.Column('bin_location', sa.String(), nullable=False, server_default=''))
|
||||
if "bin_location" not in columns:
|
||||
op.add_column("inventory", sa.Column("bin_location", sa.String(), nullable=False, server_default=""))
|
||||
|
||||
# Migrate existing data: copy location to bin_location, set default warehouse
|
||||
conn.execute(text("""
|
||||
@@ -60,12 +60,12 @@ def upgrade() -> None:
|
||||
# Create indexes if they don't exist
|
||||
existing_indexes = get_index_names(conn, "inventory")
|
||||
|
||||
if 'idx_inventory_warehouse_bin' not in existing_indexes:
|
||||
op.create_index('idx_inventory_warehouse_bin', 'inventory', ['warehouse', 'bin_location'], unique=False)
|
||||
if 'ix_inventory_bin_location' not in existing_indexes:
|
||||
op.create_index(op.f('ix_inventory_bin_location'), 'inventory', ['bin_location'], unique=False)
|
||||
if 'ix_inventory_warehouse' not in existing_indexes:
|
||||
op.create_index(op.f('ix_inventory_warehouse'), 'inventory', ['warehouse'], unique=False)
|
||||
if "idx_inventory_warehouse_bin" not in existing_indexes:
|
||||
op.create_index("idx_inventory_warehouse_bin", "inventory", ["warehouse", "bin_location"], unique=False)
|
||||
if "ix_inventory_bin_location" not in existing_indexes:
|
||||
op.create_index(op.f("ix_inventory_bin_location"), "inventory", ["bin_location"], unique=False)
|
||||
if "ix_inventory_warehouse" not in existing_indexes:
|
||||
op.create_index(op.f("ix_inventory_warehouse"), "inventory", ["warehouse"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
@@ -74,17 +74,17 @@ def downgrade() -> None:
|
||||
# Check which indexes exist before dropping
|
||||
existing_indexes = get_index_names(conn, "inventory")
|
||||
|
||||
if 'ix_inventory_warehouse' in existing_indexes:
|
||||
op.drop_index(op.f('ix_inventory_warehouse'), table_name='inventory')
|
||||
if 'ix_inventory_bin_location' in existing_indexes:
|
||||
op.drop_index(op.f('ix_inventory_bin_location'), table_name='inventory')
|
||||
if 'idx_inventory_warehouse_bin' in existing_indexes:
|
||||
op.drop_index('idx_inventory_warehouse_bin', table_name='inventory')
|
||||
if "ix_inventory_warehouse" in existing_indexes:
|
||||
op.drop_index(op.f("ix_inventory_warehouse"), table_name="inventory")
|
||||
if "ix_inventory_bin_location" in existing_indexes:
|
||||
op.drop_index(op.f("ix_inventory_bin_location"), table_name="inventory")
|
||||
if "idx_inventory_warehouse_bin" in existing_indexes:
|
||||
op.drop_index("idx_inventory_warehouse_bin", table_name="inventory")
|
||||
|
||||
# Check if columns exist before dropping
|
||||
columns = get_column_names(conn, "inventory")
|
||||
|
||||
if 'bin_location' in columns:
|
||||
op.drop_column('inventory', 'bin_location')
|
||||
if 'warehouse' in columns:
|
||||
op.drop_column('inventory', 'warehouse')
|
||||
if "bin_location" in columns:
|
||||
op.drop_column("inventory", "bin_location")
|
||||
if "warehouse" in columns:
|
||||
op.drop_column("inventory", "warehouse")
|
||||
|
||||
@@ -20,17 +20,17 @@ Affected tables:
|
||||
See docs/architecture/money-handling.md for full documentation.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e1f2a3b4c5d6'
|
||||
down_revision: Union[str, None] = 'c00d2985701f'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "e1f2a3b4c5d6"
|
||||
down_revision: str | None = "c00d2985701f"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -38,186 +38,186 @@ def upgrade() -> None:
|
||||
# Strategy: Add new _cents columns, migrate data, drop old columns
|
||||
|
||||
# === PRODUCTS TABLE ===
|
||||
with op.batch_alter_table('products', schema=None) as batch_op:
|
||||
with op.batch_alter_table("products", schema=None) as batch_op:
|
||||
# Add new cents columns
|
||||
batch_op.add_column(sa.Column('price_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('sale_price_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('supplier_cost_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('margin_percent_x100', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("price_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("sale_price_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("supplier_cost_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("margin_percent_x100", sa.Integer(), nullable=True))
|
||||
|
||||
# Migrate data for products
|
||||
op.execute('UPDATE products SET price_cents = ROUND(COALESCE(price, 0) * 100)')
|
||||
op.execute('UPDATE products SET sale_price_cents = ROUND(sale_price * 100) WHERE sale_price IS NOT NULL')
|
||||
op.execute('UPDATE products SET supplier_cost_cents = ROUND(supplier_cost * 100) WHERE supplier_cost IS NOT NULL')
|
||||
op.execute('UPDATE products SET margin_percent_x100 = ROUND(margin_percent * 100) WHERE margin_percent IS NOT NULL')
|
||||
op.execute("UPDATE products SET price_cents = ROUND(COALESCE(price, 0) * 100)")
|
||||
op.execute("UPDATE products SET sale_price_cents = ROUND(sale_price * 100) WHERE sale_price IS NOT NULL")
|
||||
op.execute("UPDATE products SET supplier_cost_cents = ROUND(supplier_cost * 100) WHERE supplier_cost IS NOT NULL")
|
||||
op.execute("UPDATE products SET margin_percent_x100 = ROUND(margin_percent * 100) WHERE margin_percent IS NOT NULL")
|
||||
|
||||
# Drop old columns
|
||||
with op.batch_alter_table('products', schema=None) as batch_op:
|
||||
batch_op.drop_column('price')
|
||||
batch_op.drop_column('sale_price')
|
||||
batch_op.drop_column('supplier_cost')
|
||||
batch_op.drop_column('margin_percent')
|
||||
with op.batch_alter_table("products", schema=None) as batch_op:
|
||||
batch_op.drop_column("price")
|
||||
batch_op.drop_column("sale_price")
|
||||
batch_op.drop_column("supplier_cost")
|
||||
batch_op.drop_column("margin_percent")
|
||||
|
||||
# === ORDERS TABLE ===
|
||||
with op.batch_alter_table('orders', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('subtotal_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('tax_amount_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('shipping_amount_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('discount_amount_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('total_amount_cents', sa.Integer(), nullable=True))
|
||||
with op.batch_alter_table("orders", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("subtotal_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("tax_amount_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("shipping_amount_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("discount_amount_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("total_amount_cents", sa.Integer(), nullable=True))
|
||||
|
||||
# Migrate data for orders
|
||||
op.execute('UPDATE orders SET subtotal_cents = ROUND(COALESCE(subtotal, 0) * 100)')
|
||||
op.execute('UPDATE orders SET tax_amount_cents = ROUND(COALESCE(tax_amount, 0) * 100)')
|
||||
op.execute('UPDATE orders SET shipping_amount_cents = ROUND(COALESCE(shipping_amount, 0) * 100)')
|
||||
op.execute('UPDATE orders SET discount_amount_cents = ROUND(COALESCE(discount_amount, 0) * 100)')
|
||||
op.execute('UPDATE orders SET total_amount_cents = ROUND(COALESCE(total_amount, 0) * 100)')
|
||||
op.execute("UPDATE orders SET subtotal_cents = ROUND(COALESCE(subtotal, 0) * 100)")
|
||||
op.execute("UPDATE orders SET tax_amount_cents = ROUND(COALESCE(tax_amount, 0) * 100)")
|
||||
op.execute("UPDATE orders SET shipping_amount_cents = ROUND(COALESCE(shipping_amount, 0) * 100)")
|
||||
op.execute("UPDATE orders SET discount_amount_cents = ROUND(COALESCE(discount_amount, 0) * 100)")
|
||||
op.execute("UPDATE orders SET total_amount_cents = ROUND(COALESCE(total_amount, 0) * 100)")
|
||||
|
||||
# Make total_amount_cents NOT NULL after migration
|
||||
with op.batch_alter_table('orders', schema=None) as batch_op:
|
||||
batch_op.drop_column('subtotal')
|
||||
batch_op.drop_column('tax_amount')
|
||||
batch_op.drop_column('shipping_amount')
|
||||
batch_op.drop_column('discount_amount')
|
||||
batch_op.drop_column('total_amount')
|
||||
with op.batch_alter_table("orders", schema=None) as batch_op:
|
||||
batch_op.drop_column("subtotal")
|
||||
batch_op.drop_column("tax_amount")
|
||||
batch_op.drop_column("shipping_amount")
|
||||
batch_op.drop_column("discount_amount")
|
||||
batch_op.drop_column("total_amount")
|
||||
# Alter total_amount_cents to be NOT NULL
|
||||
batch_op.alter_column('total_amount_cents',
|
||||
batch_op.alter_column("total_amount_cents",
|
||||
existing_type=sa.Integer(),
|
||||
nullable=False)
|
||||
|
||||
# === ORDER_ITEMS TABLE ===
|
||||
with op.batch_alter_table('order_items', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('unit_price_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('total_price_cents', sa.Integer(), nullable=True))
|
||||
with op.batch_alter_table("order_items", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("unit_price_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("total_price_cents", sa.Integer(), nullable=True))
|
||||
|
||||
# Migrate data for order_items
|
||||
op.execute('UPDATE order_items SET unit_price_cents = ROUND(COALESCE(unit_price, 0) * 100)')
|
||||
op.execute('UPDATE order_items SET total_price_cents = ROUND(COALESCE(total_price, 0) * 100)')
|
||||
op.execute("UPDATE order_items SET unit_price_cents = ROUND(COALESCE(unit_price, 0) * 100)")
|
||||
op.execute("UPDATE order_items SET total_price_cents = ROUND(COALESCE(total_price, 0) * 100)")
|
||||
|
||||
with op.batch_alter_table('order_items', schema=None) as batch_op:
|
||||
batch_op.drop_column('unit_price')
|
||||
batch_op.drop_column('total_price')
|
||||
batch_op.alter_column('unit_price_cents',
|
||||
with op.batch_alter_table("order_items", schema=None) as batch_op:
|
||||
batch_op.drop_column("unit_price")
|
||||
batch_op.drop_column("total_price")
|
||||
batch_op.alter_column("unit_price_cents",
|
||||
existing_type=sa.Integer(),
|
||||
nullable=False)
|
||||
batch_op.alter_column('total_price_cents',
|
||||
batch_op.alter_column("total_price_cents",
|
||||
existing_type=sa.Integer(),
|
||||
nullable=False)
|
||||
|
||||
# === CART_ITEMS TABLE ===
|
||||
with op.batch_alter_table('cart_items', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('price_at_add_cents', sa.Integer(), nullable=True))
|
||||
with op.batch_alter_table("cart_items", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("price_at_add_cents", sa.Integer(), nullable=True))
|
||||
|
||||
# Migrate data for cart_items
|
||||
op.execute('UPDATE cart_items SET price_at_add_cents = ROUND(COALESCE(price_at_add, 0) * 100)')
|
||||
op.execute("UPDATE cart_items SET price_at_add_cents = ROUND(COALESCE(price_at_add, 0) * 100)")
|
||||
|
||||
with op.batch_alter_table('cart_items', schema=None) as batch_op:
|
||||
batch_op.drop_column('price_at_add')
|
||||
batch_op.alter_column('price_at_add_cents',
|
||||
with op.batch_alter_table("cart_items", schema=None) as batch_op:
|
||||
batch_op.drop_column("price_at_add")
|
||||
batch_op.alter_column("price_at_add_cents",
|
||||
existing_type=sa.Integer(),
|
||||
nullable=False)
|
||||
|
||||
# === MARKETPLACE_PRODUCTS TABLE ===
|
||||
with op.batch_alter_table('marketplace_products', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('price_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('sale_price_cents', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('weight_grams', sa.Integer(), nullable=True))
|
||||
with op.batch_alter_table("marketplace_products", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("price_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("sale_price_cents", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("weight_grams", sa.Integer(), nullable=True))
|
||||
|
||||
# Migrate data for marketplace_products
|
||||
op.execute('UPDATE marketplace_products SET price_cents = ROUND(price_numeric * 100) WHERE price_numeric IS NOT NULL')
|
||||
op.execute('UPDATE marketplace_products SET sale_price_cents = ROUND(sale_price_numeric * 100) WHERE sale_price_numeric IS NOT NULL')
|
||||
op.execute('UPDATE marketplace_products SET weight_grams = ROUND(weight * 1000) WHERE weight IS NOT NULL')
|
||||
op.execute("UPDATE marketplace_products SET price_cents = ROUND(price_numeric * 100) WHERE price_numeric IS NOT NULL")
|
||||
op.execute("UPDATE marketplace_products SET sale_price_cents = ROUND(sale_price_numeric * 100) WHERE sale_price_numeric IS NOT NULL")
|
||||
op.execute("UPDATE marketplace_products SET weight_grams = ROUND(weight * 1000) WHERE weight IS NOT NULL")
|
||||
|
||||
with op.batch_alter_table('marketplace_products', schema=None) as batch_op:
|
||||
batch_op.drop_column('price_numeric')
|
||||
batch_op.drop_column('sale_price_numeric')
|
||||
batch_op.drop_column('weight')
|
||||
with op.batch_alter_table("marketplace_products", schema=None) as batch_op:
|
||||
batch_op.drop_column("price_numeric")
|
||||
batch_op.drop_column("sale_price_numeric")
|
||||
batch_op.drop_column("weight")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# === MARKETPLACE_PRODUCTS TABLE ===
|
||||
with op.batch_alter_table('marketplace_products', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('price_numeric', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('sale_price_numeric', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('weight', sa.Float(), nullable=True))
|
||||
with op.batch_alter_table("marketplace_products", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("price_numeric", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("sale_price_numeric", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("weight", sa.Float(), nullable=True))
|
||||
|
||||
op.execute('UPDATE marketplace_products SET price_numeric = price_cents / 100.0 WHERE price_cents IS NOT NULL')
|
||||
op.execute('UPDATE marketplace_products SET sale_price_numeric = sale_price_cents / 100.0 WHERE sale_price_cents IS NOT NULL')
|
||||
op.execute('UPDATE marketplace_products SET weight = weight_grams / 1000.0 WHERE weight_grams IS NOT NULL')
|
||||
op.execute("UPDATE marketplace_products SET price_numeric = price_cents / 100.0 WHERE price_cents IS NOT NULL")
|
||||
op.execute("UPDATE marketplace_products SET sale_price_numeric = sale_price_cents / 100.0 WHERE sale_price_cents IS NOT NULL")
|
||||
op.execute("UPDATE marketplace_products SET weight = weight_grams / 1000.0 WHERE weight_grams IS NOT NULL")
|
||||
|
||||
with op.batch_alter_table('marketplace_products', schema=None) as batch_op:
|
||||
batch_op.drop_column('price_cents')
|
||||
batch_op.drop_column('sale_price_cents')
|
||||
batch_op.drop_column('weight_grams')
|
||||
with op.batch_alter_table("marketplace_products", schema=None) as batch_op:
|
||||
batch_op.drop_column("price_cents")
|
||||
batch_op.drop_column("sale_price_cents")
|
||||
batch_op.drop_column("weight_grams")
|
||||
|
||||
# === CART_ITEMS TABLE ===
|
||||
with op.batch_alter_table('cart_items', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('price_at_add', sa.Float(), nullable=True))
|
||||
with op.batch_alter_table("cart_items", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("price_at_add", sa.Float(), nullable=True))
|
||||
|
||||
op.execute('UPDATE cart_items SET price_at_add = price_at_add_cents / 100.0')
|
||||
op.execute("UPDATE cart_items SET price_at_add = price_at_add_cents / 100.0")
|
||||
|
||||
with op.batch_alter_table('cart_items', schema=None) as batch_op:
|
||||
batch_op.drop_column('price_at_add_cents')
|
||||
batch_op.alter_column('price_at_add',
|
||||
with op.batch_alter_table("cart_items", schema=None) as batch_op:
|
||||
batch_op.drop_column("price_at_add_cents")
|
||||
batch_op.alter_column("price_at_add",
|
||||
existing_type=sa.Float(),
|
||||
nullable=False)
|
||||
|
||||
# === ORDER_ITEMS TABLE ===
|
||||
with op.batch_alter_table('order_items', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('unit_price', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('total_price', sa.Float(), nullable=True))
|
||||
with op.batch_alter_table("order_items", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("unit_price", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("total_price", sa.Float(), nullable=True))
|
||||
|
||||
op.execute('UPDATE order_items SET unit_price = unit_price_cents / 100.0')
|
||||
op.execute('UPDATE order_items SET total_price = total_price_cents / 100.0')
|
||||
op.execute("UPDATE order_items SET unit_price = unit_price_cents / 100.0")
|
||||
op.execute("UPDATE order_items SET total_price = total_price_cents / 100.0")
|
||||
|
||||
with op.batch_alter_table('order_items', schema=None) as batch_op:
|
||||
batch_op.drop_column('unit_price_cents')
|
||||
batch_op.drop_column('total_price_cents')
|
||||
batch_op.alter_column('unit_price',
|
||||
with op.batch_alter_table("order_items", schema=None) as batch_op:
|
||||
batch_op.drop_column("unit_price_cents")
|
||||
batch_op.drop_column("total_price_cents")
|
||||
batch_op.alter_column("unit_price",
|
||||
existing_type=sa.Float(),
|
||||
nullable=False)
|
||||
batch_op.alter_column('total_price',
|
||||
batch_op.alter_column("total_price",
|
||||
existing_type=sa.Float(),
|
||||
nullable=False)
|
||||
|
||||
# === ORDERS TABLE ===
|
||||
with op.batch_alter_table('orders', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('subtotal', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('tax_amount', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('shipping_amount', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('discount_amount', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('total_amount', sa.Float(), nullable=True))
|
||||
with op.batch_alter_table("orders", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("subtotal", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("tax_amount", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("shipping_amount", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("discount_amount", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("total_amount", sa.Float(), nullable=True))
|
||||
|
||||
op.execute('UPDATE orders SET subtotal = subtotal_cents / 100.0')
|
||||
op.execute('UPDATE orders SET tax_amount = tax_amount_cents / 100.0')
|
||||
op.execute('UPDATE orders SET shipping_amount = shipping_amount_cents / 100.0')
|
||||
op.execute('UPDATE orders SET discount_amount = discount_amount_cents / 100.0')
|
||||
op.execute('UPDATE orders SET total_amount = total_amount_cents / 100.0')
|
||||
op.execute("UPDATE orders SET subtotal = subtotal_cents / 100.0")
|
||||
op.execute("UPDATE orders SET tax_amount = tax_amount_cents / 100.0")
|
||||
op.execute("UPDATE orders SET shipping_amount = shipping_amount_cents / 100.0")
|
||||
op.execute("UPDATE orders SET discount_amount = discount_amount_cents / 100.0")
|
||||
op.execute("UPDATE orders SET total_amount = total_amount_cents / 100.0")
|
||||
|
||||
with op.batch_alter_table('orders', schema=None) as batch_op:
|
||||
batch_op.drop_column('subtotal_cents')
|
||||
batch_op.drop_column('tax_amount_cents')
|
||||
batch_op.drop_column('shipping_amount_cents')
|
||||
batch_op.drop_column('discount_amount_cents')
|
||||
batch_op.drop_column('total_amount_cents')
|
||||
batch_op.alter_column('total_amount',
|
||||
with op.batch_alter_table("orders", schema=None) as batch_op:
|
||||
batch_op.drop_column("subtotal_cents")
|
||||
batch_op.drop_column("tax_amount_cents")
|
||||
batch_op.drop_column("shipping_amount_cents")
|
||||
batch_op.drop_column("discount_amount_cents")
|
||||
batch_op.drop_column("total_amount_cents")
|
||||
batch_op.alter_column("total_amount",
|
||||
existing_type=sa.Float(),
|
||||
nullable=False)
|
||||
|
||||
# === PRODUCTS TABLE ===
|
||||
with op.batch_alter_table('products', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('price', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('sale_price', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('supplier_cost', sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column('margin_percent', sa.Float(), nullable=True))
|
||||
with op.batch_alter_table("products", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("price", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("sale_price", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("supplier_cost", sa.Float(), nullable=True))
|
||||
batch_op.add_column(sa.Column("margin_percent", sa.Float(), nullable=True))
|
||||
|
||||
op.execute('UPDATE products SET price = price_cents / 100.0 WHERE price_cents IS NOT NULL')
|
||||
op.execute('UPDATE products SET sale_price = sale_price_cents / 100.0 WHERE sale_price_cents IS NOT NULL')
|
||||
op.execute('UPDATE products SET supplier_cost = supplier_cost_cents / 100.0 WHERE supplier_cost_cents IS NOT NULL')
|
||||
op.execute('UPDATE products SET margin_percent = margin_percent_x100 / 100.0 WHERE margin_percent_x100 IS NOT NULL')
|
||||
op.execute("UPDATE products SET price = price_cents / 100.0 WHERE price_cents IS NOT NULL")
|
||||
op.execute("UPDATE products SET sale_price = sale_price_cents / 100.0 WHERE sale_price_cents IS NOT NULL")
|
||||
op.execute("UPDATE products SET supplier_cost = supplier_cost_cents / 100.0 WHERE supplier_cost_cents IS NOT NULL")
|
||||
op.execute("UPDATE products SET margin_percent = margin_percent_x100 / 100.0 WHERE margin_percent_x100 IS NOT NULL")
|
||||
|
||||
with op.batch_alter_table('products', schema=None) as batch_op:
|
||||
batch_op.drop_column('price_cents')
|
||||
batch_op.drop_column('sale_price_cents')
|
||||
batch_op.drop_column('supplier_cost_cents')
|
||||
batch_op.drop_column('margin_percent_x100')
|
||||
with op.batch_alter_table("products", schema=None) as batch_op:
|
||||
batch_op.drop_column("price_cents")
|
||||
batch_op.drop_column("sale_price_cents")
|
||||
batch_op.drop_column("supplier_cost_cents")
|
||||
batch_op.drop_column("margin_percent_x100")
|
||||
|
||||
@@ -16,18 +16,18 @@ Supports three communication channels:
|
||||
- Admin <-> Customer
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "e3f4a5b6c7d8"
|
||||
down_revision: Union[str, None] = "c9e22eadf533"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "c9e22eadf533"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def table_exists(table_name: str) -> bool:
|
||||
|
||||
@@ -13,7 +13,7 @@ language fallback capabilities. Fields in product_translations can be
|
||||
NULL to inherit from marketplace_product_translations.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -21,9 +21,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "f2b3c4d5e6f7"
|
||||
down_revision: Union[str, None] = "e1a2b3c4d5e6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "e1a2b3c4d5e6"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -8,17 +8,17 @@ This migration adds validator_type column to architecture scans and violations
|
||||
to support multiple validator types (architecture, security, performance).
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "f4a5b6c7d8e9"
|
||||
down_revision: Union[str, None] = "e3f4a5b6c7d8"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "e3f4a5b6c7d8"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2025-11-22 23:51:40.694983
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -14,9 +14,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "f68d8da5315a"
|
||||
down_revision: Union[str, None] = "72aa309d4007"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "72aa309d4007"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -7,7 +7,7 @@ Create Date: 2025-11-13 16:51:25.010057
|
||||
SQLite-compatible version
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -15,9 +15,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "fa7d4d10e358"
|
||||
down_revision: Union[str, None] = "4951b2e50581"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "4951b2e50581"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
@@ -80,10 +80,10 @@ def upgrade():
|
||||
# SQLite-compatible UPDATE with subquery
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE vendor_users
|
||||
SET user_type = 'owner'
|
||||
UPDATE vendor_users
|
||||
SET user_type = 'owner'
|
||||
WHERE (vendor_id, user_id) IN (
|
||||
SELECT id, owner_user_id
|
||||
SELECT id, owner_user_id
|
||||
FROM vendors
|
||||
)
|
||||
"""
|
||||
@@ -92,8 +92,8 @@ def upgrade():
|
||||
# Set existing owners as active
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE vendor_users
|
||||
SET is_active = TRUE
|
||||
UPDATE vendor_users
|
||||
SET is_active = TRUE
|
||||
WHERE user_type = 'owner'
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -11,17 +11,17 @@ This migration adds language preference fields to support multi-language UI:
|
||||
|
||||
Supported languages: en (English), fr (French), de (German), lb (Luxembourgish)
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'fcfdc02d5138'
|
||||
down_revision: Union[str, None] = 'b412e0b49c2e'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "fcfdc02d5138"
|
||||
down_revision: str | None = "b412e0b49c2e"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -30,25 +30,25 @@ def upgrade() -> None:
|
||||
# ========================================================================
|
||||
# default_language: Default language for vendor content (products, etc.)
|
||||
op.add_column(
|
||||
'vendors',
|
||||
sa.Column('default_language', sa.String(5), nullable=False, server_default='fr')
|
||||
"vendors",
|
||||
sa.Column("default_language", sa.String(5), nullable=False, server_default="fr")
|
||||
)
|
||||
# dashboard_language: Language for vendor team dashboard UI
|
||||
op.add_column(
|
||||
'vendors',
|
||||
sa.Column('dashboard_language', sa.String(5), nullable=False, server_default='fr')
|
||||
"vendors",
|
||||
sa.Column("dashboard_language", sa.String(5), nullable=False, server_default="fr")
|
||||
)
|
||||
# storefront_language: Default language for customer-facing shop
|
||||
op.add_column(
|
||||
'vendors',
|
||||
sa.Column('storefront_language', sa.String(5), nullable=False, server_default='fr')
|
||||
"vendors",
|
||||
sa.Column("storefront_language", sa.String(5), nullable=False, server_default="fr")
|
||||
)
|
||||
# storefront_languages: JSON array of enabled languages for storefront
|
||||
# Allows vendors to enable/disable specific languages
|
||||
op.add_column(
|
||||
'vendors',
|
||||
"vendors",
|
||||
sa.Column(
|
||||
'storefront_languages',
|
||||
"storefront_languages",
|
||||
sa.JSON,
|
||||
nullable=False,
|
||||
server_default='["fr", "de", "en"]'
|
||||
@@ -60,8 +60,8 @@ def upgrade() -> None:
|
||||
# ========================================================================
|
||||
# preferred_language: User's preferred UI language (NULL = use context default)
|
||||
op.add_column(
|
||||
'users',
|
||||
sa.Column('preferred_language', sa.String(5), nullable=True)
|
||||
"users",
|
||||
sa.Column("preferred_language", sa.String(5), nullable=True)
|
||||
)
|
||||
|
||||
# ========================================================================
|
||||
@@ -69,16 +69,16 @@ def upgrade() -> None:
|
||||
# ========================================================================
|
||||
# preferred_language: Customer's preferred language (NULL = use storefront default)
|
||||
op.add_column(
|
||||
'customers',
|
||||
sa.Column('preferred_language', sa.String(5), nullable=True)
|
||||
"customers",
|
||||
sa.Column("preferred_language", sa.String(5), nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove columns in reverse order
|
||||
op.drop_column('customers', 'preferred_language')
|
||||
op.drop_column('users', 'preferred_language')
|
||||
op.drop_column('vendors', 'storefront_languages')
|
||||
op.drop_column('vendors', 'storefront_language')
|
||||
op.drop_column('vendors', 'dashboard_language')
|
||||
op.drop_column('vendors', 'default_language')
|
||||
op.drop_column("customers", "preferred_language")
|
||||
op.drop_column("users", "preferred_language")
|
||||
op.drop_column("vendors", "storefront_languages")
|
||||
op.drop_column("vendors", "storefront_language")
|
||||
op.drop_column("vendors", "dashboard_language")
|
||||
op.drop_column("vendors", "default_language")
|
||||
|
||||
@@ -6,17 +6,15 @@ Create Date: 2025-11-22 13:41:18.069674
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "fef1d20ce8b4"
|
||||
down_revision: Union[str, None] = "fa7d4d10e358"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "fa7d4d10e358"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -12,6 +12,7 @@ Create Date: 2024-12-21
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
@@ -9,16 +9,17 @@ This migration adds:
|
||||
- invoices: Invoice records with seller/buyer snapshots
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "h6c7d8e9f0a1"
|
||||
down_revision: Union[str, None] = "g5b6c7d8e9f0"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "g5b6c7d8e9f0"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -8,16 +8,17 @@ This migration adds:
|
||||
- vendor_subscriptions: Per-vendor subscription tracking with tier limits
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "i7d8e9f0a1b2"
|
||||
down_revision: Union[str, None] = "h6c7d8e9f0a1"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "h6c7d8e9f0a1"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -15,16 +15,17 @@ After this migration:
|
||||
- The marketplace_product_id FK is kept for "view original source" feature
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "j8e9f0a1b2c3"
|
||||
down_revision: Union[str, None] = "i7d8e9f0a1b2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "i7d8e9f0a1b2"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -259,4 +260,3 @@ def downgrade() -> None:
|
||||
1. It would lose any vendor customizations made after migration
|
||||
2. The model code may still work with populated fields
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -8,9 +8,9 @@ Adds tier_id column to vendor_subscriptions table with FK to subscription_tiers.
|
||||
Backfills tier_id based on existing tier (code) values.
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "k9f0a1b2c3d4"
|
||||
|
||||
@@ -7,9 +7,9 @@ Create Date: 2025-12-26
|
||||
Adds table for tracking daily platform capacity metrics for growth forecasting.
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "l0a1b2c3d4e5"
|
||||
|
||||
@@ -5,67 +5,67 @@ Revises: d7a4a3f06394
|
||||
Create Date: 2025-12-27 22:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'm1b2c3d4e5f6'
|
||||
down_revision: Union[str, None] = 'd7a4a3f06394'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "m1b2c3d4e5f6"
|
||||
down_revision: str | None = "d7a4a3f06394"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table('vendor_onboarding',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor_id', sa.Integer(), nullable=False),
|
||||
op.create_table("vendor_onboarding",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vendor_id", sa.Integer(), nullable=False),
|
||||
# Overall status
|
||||
sa.Column('status', sa.String(length=20), nullable=False, server_default='not_started'),
|
||||
sa.Column('current_step', sa.String(length=30), nullable=False, server_default='company_profile'),
|
||||
sa.Column("status", sa.String(length=20), nullable=False, server_default="not_started"),
|
||||
sa.Column("current_step", sa.String(length=30), nullable=False, server_default="company_profile"),
|
||||
# Step 1: Company Profile
|
||||
sa.Column('step_company_profile_completed', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
sa.Column('step_company_profile_completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('step_company_profile_data', sa.JSON(), nullable=True),
|
||||
sa.Column("step_company_profile_completed", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("step_company_profile_completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("step_company_profile_data", sa.JSON(), nullable=True),
|
||||
# Step 2: Letzshop API Configuration
|
||||
sa.Column('step_letzshop_api_completed', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
sa.Column('step_letzshop_api_completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('step_letzshop_api_connection_verified', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
sa.Column("step_letzshop_api_completed", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("step_letzshop_api_completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("step_letzshop_api_connection_verified", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
# Step 3: Product Import
|
||||
sa.Column('step_product_import_completed', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
sa.Column('step_product_import_completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('step_product_import_csv_url_set', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
sa.Column("step_product_import_completed", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("step_product_import_completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("step_product_import_csv_url_set", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
# Step 4: Order Sync
|
||||
sa.Column('step_order_sync_completed', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
sa.Column('step_order_sync_completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('step_order_sync_job_id', sa.Integer(), nullable=True),
|
||||
sa.Column("step_order_sync_completed", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("step_order_sync_completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("step_order_sync_job_id", sa.Integer(), nullable=True),
|
||||
# Completion tracking
|
||||
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
# Admin override
|
||||
sa.Column('skipped_by_admin', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
sa.Column('skipped_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('skipped_reason', sa.Text(), nullable=True),
|
||||
sa.Column('skipped_by_user_id', sa.Integer(), nullable=True),
|
||||
sa.Column("skipped_by_admin", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("skipped_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("skipped_reason", sa.Text(), nullable=True),
|
||||
sa.Column("skipped_by_user_id", sa.Integer(), nullable=True),
|
||||
# Timestamps
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
# Constraints
|
||||
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['skipped_by_user_id'], ['users.id']),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(["vendor_id"], ["vendors.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["skipped_by_user_id"], ["users.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f('ix_vendor_onboarding_id'), 'vendor_onboarding', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_vendor_onboarding_vendor_id'), 'vendor_onboarding', ['vendor_id'], unique=True)
|
||||
op.create_index(op.f('ix_vendor_onboarding_status'), 'vendor_onboarding', ['status'], unique=False)
|
||||
op.create_index('idx_onboarding_vendor_status', 'vendor_onboarding', ['vendor_id', 'status'], unique=False)
|
||||
op.create_index(op.f("ix_vendor_onboarding_id"), "vendor_onboarding", ["id"], unique=False)
|
||||
op.create_index(op.f("ix_vendor_onboarding_vendor_id"), "vendor_onboarding", ["vendor_id"], unique=True)
|
||||
op.create_index(op.f("ix_vendor_onboarding_status"), "vendor_onboarding", ["status"], unique=False)
|
||||
op.create_index("idx_onboarding_vendor_status", "vendor_onboarding", ["vendor_id", "status"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('idx_onboarding_vendor_status', table_name='vendor_onboarding')
|
||||
op.drop_index(op.f('ix_vendor_onboarding_status'), table_name='vendor_onboarding')
|
||||
op.drop_index(op.f('ix_vendor_onboarding_vendor_id'), table_name='vendor_onboarding')
|
||||
op.drop_index(op.f('ix_vendor_onboarding_id'), table_name='vendor_onboarding')
|
||||
op.drop_table('vendor_onboarding')
|
||||
op.drop_index("idx_onboarding_vendor_status", table_name="vendor_onboarding")
|
||||
op.drop_index(op.f("ix_vendor_onboarding_status"), table_name="vendor_onboarding")
|
||||
op.drop_index(op.f("ix_vendor_onboarding_vendor_id"), table_name="vendor_onboarding")
|
||||
op.drop_index(op.f("ix_vendor_onboarding_id"), table_name="vendor_onboarding")
|
||||
op.drop_table("vendor_onboarding")
|
||||
|
||||
@@ -17,9 +17,9 @@ Alters:
|
||||
Revision ID: billing_001
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# Revision identifiers
|
||||
revision = "billing_001"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,17 +15,18 @@ Phase 2 changes:
|
||||
- NEW COLUMN on loyalty_cards: last_activity_at
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "loyalty_003_phase2"
|
||||
down_revision: Union[str, None] = "0fb5d6d6ff97"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "0fb5d6d6ff97"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -7,16 +7,17 @@ Create Date: 2025-12-31 10:00:00.000000
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "n2c3d4e5f6a7"
|
||||
down_revision: Union[str, None] = "ba2c0ce78396"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "ba2c0ce78396"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -245,7 +246,7 @@ def upgrade() -> None:
|
||||
tier_ids[row[1]] = row[0]
|
||||
|
||||
# Insert features
|
||||
now = sa.func.now()
|
||||
sa.func.now()
|
||||
for category, code, name, description, ui_location, ui_icon, ui_route, display_order in FEATURES:
|
||||
minimum_tier_code = MINIMUM_TIER.get(code)
|
||||
minimum_tier_id = tier_ids.get(minimum_tier_code) if minimum_tier_code else None
|
||||
|
||||
@@ -10,9 +10,10 @@ Adds an audit trail for inventory movements:
|
||||
- Store quantity snapshots for historical analysis
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "o3c4d5e6f7a8"
|
||||
down_revision = "n2c3d4e5f6a7"
|
||||
|
||||
@@ -6,24 +6,24 @@ Revises: o3c4d5e6f7a8
|
||||
Create Date: 2026-01-01 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'p4d5e6f7a8b9'
|
||||
down_revision: Union[str, None] = 'o3c4d5e6f7a8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "p4d5e6f7a8b9"
|
||||
down_revision: str | None = "o3c4d5e6f7a8"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add shipped_quantity column to order_items
|
||||
op.add_column(
|
||||
'order_items',
|
||||
sa.Column('shipped_quantity', sa.Integer(), nullable=False, server_default='0')
|
||||
"order_items",
|
||||
sa.Column("shipped_quantity", sa.Integer(), nullable=False, server_default="0")
|
||||
)
|
||||
|
||||
# Set shipped_quantity = quantity for already fulfilled items
|
||||
@@ -36,4 +36,4 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('order_items', 'shipped_quantity')
|
||||
op.drop_column("order_items", "shipped_quantity")
|
||||
|
||||
@@ -10,42 +10,42 @@ Revises: p4d5e6f7a8b9
|
||||
Create Date: 2026-01-02 10:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'q5e6f7a8b9c0'
|
||||
down_revision: Union[str, None] = 'p4d5e6f7a8b9'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
revision: str = "q5e6f7a8b9c0"
|
||||
down_revision: str | None = "p4d5e6f7a8b9"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add VAT regime (domestic, oss, reverse_charge, origin, exempt)
|
||||
op.add_column(
|
||||
'orders',
|
||||
sa.Column('vat_regime', sa.String(20), nullable=True)
|
||||
"orders",
|
||||
sa.Column("vat_regime", sa.String(20), nullable=True)
|
||||
)
|
||||
|
||||
# Add VAT rate as percentage (e.g., 17.00 for 17%)
|
||||
op.add_column(
|
||||
'orders',
|
||||
sa.Column('vat_rate', sa.Numeric(5, 2), nullable=True)
|
||||
"orders",
|
||||
sa.Column("vat_rate", sa.Numeric(5, 2), nullable=True)
|
||||
)
|
||||
|
||||
# Add human-readable VAT label (e.g., "Luxembourg VAT 17%")
|
||||
op.add_column(
|
||||
'orders',
|
||||
sa.Column('vat_rate_label', sa.String(100), nullable=True)
|
||||
"orders",
|
||||
sa.Column("vat_rate_label", sa.String(100), nullable=True)
|
||||
)
|
||||
|
||||
# Add destination country for cross-border sales (ISO code)
|
||||
op.add_column(
|
||||
'orders',
|
||||
sa.Column('vat_destination_country', sa.String(2), nullable=True)
|
||||
"orders",
|
||||
sa.Column("vat_destination_country", sa.String(2), nullable=True)
|
||||
)
|
||||
|
||||
# Populate VAT fields for existing orders based on shipping country
|
||||
@@ -66,7 +66,7 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('orders', 'vat_destination_country')
|
||||
op.drop_column('orders', 'vat_rate_label')
|
||||
op.drop_column('orders', 'vat_rate')
|
||||
op.drop_column('orders', 'vat_regime')
|
||||
op.drop_column("orders", "vat_destination_country")
|
||||
op.drop_column("orders", "vat_rate_label")
|
||||
op.drop_column("orders", "vat_rate")
|
||||
op.drop_column("orders", "vat_regime")
|
||||
|
||||
@@ -11,10 +11,10 @@ This migration is idempotent - it checks for existing columns before
|
||||
making changes.
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import text
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "r6f7a8b9c0d1"
|
||||
|
||||
@@ -10,9 +10,9 @@ NULL means the vendor inherits from platform defaults.
|
||||
Examples: 'fr-LU', 'de-DE', 'en-GB'
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "s7a8b9c0d1e2"
|
||||
|
||||
@@ -18,16 +18,17 @@ Major terminology migration:
|
||||
- letzshop_vendor_cache -> letzshop_store_cache
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "t001_terminology"
|
||||
down_revision: Union[str, None] = "loyalty_003_phase2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "loyalty_003_phase2"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _col_exists(table: str, col: str) -> bool:
|
||||
|
||||
@@ -8,15 +8,15 @@ Completes the Company/Vendor -> Merchant/Store terminology migration by
|
||||
renaming 4 constraints and 12 indexes that still used "vendor" in their names.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "t002_constraints"
|
||||
down_revision: Union[str, None] = "t001_terminology"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "t001_terminology"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
# (old_name, new_name, table) — table is needed for RENAME CONSTRAINT
|
||||
CONSTRAINTS = [
|
||||
|
||||
@@ -6,9 +6,9 @@ Create Date: 2026-01-03
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "t8b9c0d1e2f3"
|
||||
|
||||
@@ -11,9 +11,9 @@ Changes:
|
||||
- Create vendor_email_templates table for vendor-specific template overrides
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "u9c0d1e2f3g4"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user