Compare commits
4 Commits
11f1909f68
...
ba130d4171
| Author | SHA1 | Date | |
|---|---|---|---|
| ba130d4171 | |||
| e9253fbd84 | |||
| 34ee7bb7ad | |||
| 481deaa67d |
@@ -145,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):
|
||||
@@ -213,7 +213,6 @@ api_endpoint_rules:
|
||||
file_pattern:
|
||||
- "app/api/v1/vendor/**/*.py"
|
||||
- "app/modules/*/routes/api/store*.py"
|
||||
file_pattern:
|
||||
- "app/api/v1/storefront/**/*.py"
|
||||
- "app/modules/*/routes/api/storefront*.py"
|
||||
discouraged_patterns:
|
||||
|
||||
@@ -44,17 +44,17 @@ exception_rules:
|
||||
- "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"
|
||||
- "app/modules/*/exceptions.py"
|
||||
required_base_class: "WizamartException"
|
||||
required_base_class: "OrionException"
|
||||
example_good: |
|
||||
class VendorNotFoundException(ResourceNotFoundException):
|
||||
def __init__(self, vendor_code: str):
|
||||
@@ -65,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
22
.env.example
22
.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@orion.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.orion.lu
|
||||
# Production
|
||||
# DOCUMENTATION_URL=https://docs.wizamart.com
|
||||
# DOCUMENTATION_URL=https://docs.orion.lu
|
||||
|
||||
# =============================================================================
|
||||
# RATE LIMITING
|
||||
@@ -70,7 +70,7 @@ LOG_FILE=logs/app.log
|
||||
# PLATFORM DOMAIN CONFIGURATION
|
||||
# =============================================================================
|
||||
# Your main platform domain
|
||||
PLATFORM_DOMAIN=wizamart.com
|
||||
PLATFORM_DOMAIN=orion.lu
|
||||
|
||||
# Custom domain features
|
||||
# Enable/disable custom domains
|
||||
@@ -85,7 +85,7 @@ SSL_PROVIDER=letsencrypt
|
||||
AUTO_PROVISION_SSL=False
|
||||
|
||||
# DNS verification
|
||||
DNS_VERIFICATION_PREFIX=_wizamart-verify
|
||||
DNS_VERIFICATION_PREFIX=_orion-verify
|
||||
DNS_VERIFICATION_TTL=3600
|
||||
|
||||
# =============================================================================
|
||||
@@ -103,8 +103,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@orion.lu
|
||||
EMAIL_FROM_NAME=Orion
|
||||
EMAIL_REPLY_TO=
|
||||
|
||||
# SMTP Settings (used when EMAIL_PROVIDER=smtp)
|
||||
@@ -185,7 +185,7 @@ 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
|
||||
|
||||
@@ -45,11 +45,11 @@ jobs:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_DB: wizamart_test
|
||||
POSTGRES_DB: orion_test
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
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
|
||||
@@ -57,8 +57,8 @@ jobs:
|
||||
env:
|
||||
# act_runner executes jobs in Docker containers on the same network as services,
|
||||
# so use the service name (postgres) as hostname with the internal port (5432)
|
||||
TEST_DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/wizamart_test"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres: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"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -183,5 +183,5 @@ tailadmin-free-tailwind-dashboard-template/
|
||||
static/shared/css/tailwind.css
|
||||
|
||||
# Export files
|
||||
wizamart_letzshop_export_*.csv
|
||||
orion_letzshop_export_*.csv
|
||||
exports/
|
||||
|
||||
@@ -43,13 +43,13 @@ pytest:
|
||||
alias: postgres
|
||||
variables:
|
||||
# PostgreSQL service configuration
|
||||
POSTGRES_DB: wizamart_test
|
||||
POSTGRES_DB: orion_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"
|
||||
TEST_DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/orion_test"
|
||||
# Skip database validation during import (tests use TEST_DATABASE_URL)
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/wizamart_test"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/orion_test"
|
||||
before_script:
|
||||
- pip install uv
|
||||
- uv sync --frozen
|
||||
|
||||
@@ -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
|
||||
|
||||
10
Makefile
10
Makefile
@@ -1,4 +1,4 @@
|
||||
# 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
|
||||
@@ -132,7 +132,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
|
||||
|
||||
@@ -235,7 +235,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 :=
|
||||
@@ -530,7 +530,7 @@ endif
|
||||
# =============================================================================
|
||||
|
||||
help:
|
||||
@echo "Wizamart Platform Development Commands"
|
||||
@echo "Orion Platform Development Commands"
|
||||
@echo ""
|
||||
@echo "=== SETUP ==="
|
||||
@echo " install - Install production dependencies"
|
||||
@@ -681,4 +681,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
|
||||
|
||||
@@ -19,7 +19,7 @@ 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("path_prefix", sa.String(50), unique=True, nullable=True, index=True, comment="Development path prefix (e.g., 'oms' for localhost:9999/oms/*)"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# app/core/celery_config.py
|
||||
"""
|
||||
Celery configuration for Wizamart background task processing.
|
||||
Celery configuration for Orion background task processing.
|
||||
|
||||
This module configures Celery with Redis as the broker and result backend.
|
||||
It includes:
|
||||
@@ -66,7 +66,7 @@ def get_all_task_modules() -> list[str]:
|
||||
|
||||
# Create Celery application
|
||||
celery_app = Celery(
|
||||
"wizamart",
|
||||
"orion",
|
||||
broker=REDIS_URL,
|
||||
backend=REDIS_URL,
|
||||
include=get_all_task_modules(),
|
||||
|
||||
@@ -27,7 +27,7 @@ class Settings(BaseSettings):
|
||||
# =============================================================================
|
||||
# PROJECT INFORMATION
|
||||
# =============================================================================
|
||||
project_name: str = "Wizamart - Multi-Store Marketplace Platform"
|
||||
project_name: str = "Orion - Multi-Store Marketplace Platform"
|
||||
version: str = "2.2.0"
|
||||
|
||||
# Clean description without HTML
|
||||
@@ -47,12 +47,12 @@ class Settings(BaseSettings):
|
||||
# =============================================================================
|
||||
# DATABASE (PostgreSQL only)
|
||||
# =============================================================================
|
||||
database_url: str = "postgresql://wizamart_user:secure_password@localhost:5432/wizamart_db"
|
||||
database_url: str = "postgresql://orion_user:secure_password@localhost:5432/orion_db"
|
||||
|
||||
# =============================================================================
|
||||
# ADMIN INITIALIZATION (for init_production.py)
|
||||
# =============================================================================
|
||||
admin_email: str = "admin@wizamart.com"
|
||||
admin_email: str = "admin@orion.lu"
|
||||
admin_username: str = "admin"
|
||||
admin_password: str = "admin123" # CHANGE IN PRODUCTION!
|
||||
admin_first_name: str = "Platform"
|
||||
@@ -96,7 +96,7 @@ class Settings(BaseSettings):
|
||||
# =============================================================================
|
||||
# PLATFORM DOMAIN CONFIGURATION
|
||||
# =============================================================================
|
||||
platform_domain: str = "wizamart.com"
|
||||
platform_domain: str = "orion.lu"
|
||||
|
||||
# Custom domain features
|
||||
allow_custom_domains: bool = True
|
||||
@@ -107,7 +107,7 @@ class Settings(BaseSettings):
|
||||
auto_provision_ssl: bool = False
|
||||
|
||||
# DNS verification
|
||||
dns_verification_prefix: str = "_wizamart-verify"
|
||||
dns_verification_prefix: str = "_orion-verify"
|
||||
dns_verification_ttl: int = 3600
|
||||
|
||||
# =============================================================================
|
||||
@@ -130,8 +130,8 @@ class Settings(BaseSettings):
|
||||
# =============================================================================
|
||||
# Provider: smtp, sendgrid, mailgun, ses
|
||||
email_provider: str = "smtp"
|
||||
email_from_address: str = "noreply@wizamart.com"
|
||||
email_from_name: str = "Wizamart"
|
||||
email_from_address: str = "noreply@orion.lu"
|
||||
email_from_name: str = "Orion"
|
||||
email_reply_to: str = "" # Optional reply-to address
|
||||
|
||||
# SMTP Settings (used when email_provider=smtp)
|
||||
@@ -201,7 +201,7 @@ class Settings(BaseSettings):
|
||||
r2_account_id: str | None = None
|
||||
r2_access_key_id: str | None = None
|
||||
r2_secret_access_key: str | None = None
|
||||
r2_bucket_name: str = "wizamart-media"
|
||||
r2_bucket_name: str = "orion-media"
|
||||
r2_public_url: str | None = None # Custom domain for public access (e.g., https://media.yoursite.com)
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -9,7 +9,7 @@ Detection priority:
|
||||
1. Admin subdomain (admin.oms.lu)
|
||||
2. Path-based admin/store (/admin/*, /store/*, /api/v1/admin/*)
|
||||
3. Custom domain lookup (mybakery.lu -> STOREFRONT)
|
||||
4. Store subdomain (wizamart.oms.lu -> STOREFRONT)
|
||||
4. Store subdomain (orion.oms.lu -> STOREFRONT)
|
||||
5. Storefront paths (/storefront/*, /api/v1/storefront/*)
|
||||
6. Default to PLATFORM (marketing pages)
|
||||
|
||||
@@ -62,7 +62,7 @@ class FrontendDetector:
|
||||
Detect frontend type from request.
|
||||
|
||||
Args:
|
||||
host: Request host header (e.g., "oms.lu", "wizamart.oms.lu", "localhost:8000")
|
||||
host: Request host header (e.g., "oms.lu", "orion.oms.lu", "localhost:8000")
|
||||
path: Request path (e.g., "/admin/stores", "/storefront/products")
|
||||
has_store_context: True if request.state.store is set (from middleware)
|
||||
|
||||
@@ -110,7 +110,7 @@ class FrontendDetector:
|
||||
logger.debug("[FRONTEND_DETECTOR] Detected PLATFORM from path")
|
||||
return FrontendType.PLATFORM
|
||||
|
||||
# 3. Store subdomain detection (wizamart.oms.lu)
|
||||
# 3. Store subdomain detection (orion.oms.lu)
|
||||
# If subdomain exists and is not reserved -> it's a store storefront
|
||||
if subdomain and subdomain not in cls.RESERVED_SUBDOMAINS:
|
||||
logger.debug(
|
||||
@@ -138,7 +138,7 @@ class FrontendDetector:
|
||||
@classmethod
|
||||
def _get_subdomain(cls, host: str) -> str | None:
|
||||
"""
|
||||
Extract subdomain from host (e.g., 'wizamart' from 'wizamart.oms.lu').
|
||||
Extract subdomain from host (e.g., 'orion' from 'orion.oms.lu').
|
||||
|
||||
Returns None for localhost, IP addresses, or root domains.
|
||||
Handles special case of admin.localhost for development.
|
||||
|
||||
@@ -32,13 +32,13 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
# === STARTUP ===
|
||||
app_logger = setup_logging()
|
||||
app_logger.info("Starting Wizamart multi-tenant platform")
|
||||
app_logger.info("Starting Orion multi-tenant platform")
|
||||
logger.info("[OK] Application startup completed")
|
||||
|
||||
yield
|
||||
|
||||
# === SHUTDOWN ===
|
||||
app_logger.info("Shutting down Wizamart platform")
|
||||
app_logger.info("Shutting down Orion platform")
|
||||
# Add cleanup tasks here if needed
|
||||
|
||||
|
||||
|
||||
@@ -33,16 +33,16 @@ from .base import (
|
||||
BusinessLogicException,
|
||||
ConflictException,
|
||||
ExternalServiceException,
|
||||
OrionException,
|
||||
RateLimitException,
|
||||
ResourceNotFoundException,
|
||||
ServiceUnavailableException,
|
||||
ValidationException,
|
||||
WizamartException,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base exception class
|
||||
"WizamartException",
|
||||
"OrionException",
|
||||
# Validation and business logic
|
||||
"ValidationException",
|
||||
"BusinessLogicException",
|
||||
|
||||
@@ -11,7 +11,7 @@ This module provides classes and functions for:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class WizamartException(Exception):
|
||||
class OrionException(Exception):
|
||||
"""Base exception class for all custom exceptions."""
|
||||
|
||||
def __init__(
|
||||
@@ -39,7 +39,7 @@ class WizamartException(Exception):
|
||||
return result
|
||||
|
||||
|
||||
class ValidationException(WizamartException):
|
||||
class ValidationException(OrionException):
|
||||
"""Raised when request validation fails."""
|
||||
|
||||
def __init__(
|
||||
@@ -60,7 +60,7 @@ class ValidationException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
class AuthenticationException(WizamartException):
|
||||
class AuthenticationException(OrionException):
|
||||
"""Raised when authentication fails."""
|
||||
|
||||
def __init__(
|
||||
@@ -77,7 +77,7 @@ class AuthenticationException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
class AuthorizationException(WizamartException):
|
||||
class AuthorizationException(OrionException):
|
||||
"""Raised when user lacks permission for an operation."""
|
||||
|
||||
def __init__(
|
||||
@@ -94,7 +94,7 @@ class AuthorizationException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
class ResourceNotFoundException(WizamartException):
|
||||
class ResourceNotFoundException(OrionException):
|
||||
"""Raised when a requested resource is not found."""
|
||||
|
||||
def __init__(
|
||||
@@ -120,7 +120,7 @@ class ResourceNotFoundException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
class ConflictException(WizamartException):
|
||||
class ConflictException(OrionException):
|
||||
"""Raised when a resource conflict occurs."""
|
||||
|
||||
def __init__(
|
||||
@@ -137,7 +137,7 @@ class ConflictException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
class BusinessLogicException(WizamartException):
|
||||
class BusinessLogicException(OrionException):
|
||||
"""Raised when business logic rules are violated."""
|
||||
|
||||
def __init__(
|
||||
@@ -154,7 +154,7 @@ class BusinessLogicException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
class ExternalServiceException(WizamartException):
|
||||
class ExternalServiceException(OrionException):
|
||||
"""Raised when an external service fails."""
|
||||
|
||||
def __init__(
|
||||
@@ -175,7 +175,7 @@ class ExternalServiceException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
class RateLimitException(WizamartException):
|
||||
class RateLimitException(OrionException):
|
||||
"""Raised when rate limit is exceeded."""
|
||||
|
||||
def __init__(
|
||||
@@ -196,7 +196,7 @@ class RateLimitException(WizamartException):
|
||||
)
|
||||
|
||||
|
||||
class ServiceUnavailableException(WizamartException):
|
||||
class ServiceUnavailableException(OrionException):
|
||||
"""Raised when service is unavailable."""
|
||||
|
||||
def __init__(self, message: str = "Service temporarily unavailable"):
|
||||
|
||||
@@ -19,7 +19,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from app.modules.enums import FrontendType
|
||||
from middleware.frontend_type import get_frontend_type
|
||||
|
||||
from .base import WizamartException
|
||||
from .base import OrionException
|
||||
from .error_renderer import ErrorPageRenderer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -28,8 +28,8 @@ logger = logging.getLogger(__name__)
|
||||
def setup_exception_handlers(app):
|
||||
"""Setup exception handlers for the FastAPI app."""
|
||||
|
||||
@app.exception_handler(WizamartException)
|
||||
async def custom_exception_handler(request: Request, exc: WizamartException):
|
||||
@app.exception_handler(OrionException)
|
||||
async def custom_exception_handler(request: Request, exc: OrionException):
|
||||
"""Handle custom exceptions with context-aware rendering."""
|
||||
|
||||
# Special handling for auth errors on HTML page requests (redirect to login)
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.exceptions.base import (
|
||||
)
|
||||
|
||||
|
||||
class ReportGenerationException(BusinessLogicException):
|
||||
class ReportGenerationException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when report generation fails."""
|
||||
|
||||
def __init__(self, report_type: str, reason: str):
|
||||
@@ -21,7 +21,7 @@ class ReportGenerationException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class InvalidDateRangeException(ValidationException):
|
||||
class InvalidDateRangeException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when an invalid date range is provided."""
|
||||
|
||||
def __init__(self, start_date: str, end_date: str):
|
||||
|
||||
@@ -16,6 +16,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.catalog.models import Product # IMPORT-002
|
||||
@@ -174,7 +175,7 @@ class StatsService:
|
||||
|
||||
except StoreNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(
|
||||
f"Failed to retrieve store statistics for store {store_id}: {str(e)}"
|
||||
)
|
||||
@@ -253,7 +254,7 @@ class StatsService:
|
||||
|
||||
except StoreNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(
|
||||
f"Failed to retrieve store analytics for store {store_id}: {str(e)}"
|
||||
)
|
||||
@@ -300,7 +301,7 @@ class StatsService:
|
||||
(verified_stores / total_stores * 100) if total_stores > 0 else 0
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to get store statistics: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_store_statistics", reason="Database query failed"
|
||||
@@ -353,7 +354,7 @@ class StatsService:
|
||||
"total_inventory_quantity": inventory_stats.get("total_quantity", 0),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to retrieve comprehensive statistics: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_comprehensive_stats",
|
||||
@@ -400,7 +401,7 @@ class StatsService:
|
||||
for stat in marketplace_stats
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(
|
||||
f"Failed to retrieve marketplace breakdown statistics: {str(e)}"
|
||||
)
|
||||
@@ -437,7 +438,7 @@ class StatsService:
|
||||
(active_users / total_users * 100) if total_users > 0 else 0
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to get user statistics: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_user_statistics", reason="Database query failed"
|
||||
@@ -496,7 +497,7 @@ class StatsService:
|
||||
"failed_imports": failed,
|
||||
"success_rate": (completed / total * 100) if total > 0 else 0,
|
||||
}
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to get import statistics: {str(e)}")
|
||||
return {
|
||||
"total": 0,
|
||||
|
||||
@@ -38,7 +38,6 @@ __all__ = [
|
||||
"WebhookVerificationException",
|
||||
# Feature exceptions
|
||||
"FeatureNotFoundException",
|
||||
"FeatureNotAvailableException",
|
||||
"InvalidFeatureCodesError",
|
||||
]
|
||||
|
||||
@@ -91,7 +90,7 @@ class SubscriptionNotCancelledException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class SubscriptionAlreadyCancelledException(BusinessLogicException):
|
||||
class SubscriptionAlreadyCancelledException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when trying to cancel an already cancelled subscription."""
|
||||
|
||||
def __init__(self):
|
||||
@@ -171,7 +170,7 @@ class StripePriceNotConfiguredException(BusinessLogicException):
|
||||
self.tier_code = tier_code
|
||||
|
||||
|
||||
class PaymentFailedException(BillingException):
|
||||
class PaymentFailedException(BillingException): # noqa: MOD-025
|
||||
"""Raised when a payment fails."""
|
||||
|
||||
def __init__(self, message: str, stripe_error: str | None = None):
|
||||
@@ -238,25 +237,6 @@ class FeatureNotFoundException(ResourceNotFoundException):
|
||||
self.feature_code = feature_code
|
||||
|
||||
|
||||
class FeatureNotAvailableException(BillingException):
|
||||
"""Raised when a feature is not available in current tier."""
|
||||
|
||||
def __init__(self, feature: str, current_tier: str, required_tier: str):
|
||||
message = f"Feature '{feature}' requires {required_tier} tier (current: {current_tier})"
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="FEATURE_NOT_AVAILABLE",
|
||||
details={
|
||||
"feature": feature,
|
||||
"current_tier": current_tier,
|
||||
"required_tier": required_tier,
|
||||
},
|
||||
)
|
||||
self.feature = feature
|
||||
self.current_tier = current_tier
|
||||
self.required_tier = required_tier
|
||||
|
||||
|
||||
class InvalidFeatureCodesError(ValidationException):
|
||||
"""Invalid feature codes provided."""
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class MerchantSubscription(Base, TimestampMixin):
|
||||
|
||||
Example:
|
||||
Merchant "Boucherie Luxembourg" subscribes to:
|
||||
- Wizamart OMS (Professional tier)
|
||||
- Orion OMS (Professional tier)
|
||||
- Loyalty+ (Essential tier)
|
||||
|
||||
Their stores inherit features from the merchant's subscription.
|
||||
|
||||
@@ -119,7 +119,7 @@ async def signup_success_page(
|
||||
Shown after successful account creation.
|
||||
"""
|
||||
context = get_platform_context(request, db)
|
||||
context["page_title"] = "Welcome to Wizamart!"
|
||||
context["page_title"] = "Welcome to Orion!"
|
||||
context["store_code"] = store_code
|
||||
|
||||
return templates.TemplateResponse(
|
||||
|
||||
@@ -542,7 +542,7 @@ class BillingService:
|
||||
if stripe_service.is_configured and store_addon.stripe_subscription_item_id:
|
||||
try:
|
||||
stripe_service.cancel_subscription_item(store_addon.stripe_subscription_item_id)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.warning(f"Failed to cancel addon in Stripe: {e}")
|
||||
|
||||
# Mark as cancelled
|
||||
|
||||
@@ -324,7 +324,7 @@ function storeInvoices() {
|
||||
|
||||
try {
|
||||
// Get the token for authentication
|
||||
const token = localStorage.getItem('wizamart_token') || localStorage.getItem('store_token');
|
||||
const token = localStorage.getItem('orion_token') || localStorage.getItem('store_token');
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{# Standalone Pricing Page #}
|
||||
{% extends "platform/base.html" %}
|
||||
|
||||
{% block title %}{{ _("cms.platform.pricing.title") }} - Wizamart{% endblock %}
|
||||
{% block title %}{{ _("cms.platform.pricing.title") }} - Orion{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="{ annual: false }" class="py-16 lg:py-24">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{# Multi-step Signup Wizard #}
|
||||
{% extends "platform/base.html" %}
|
||||
|
||||
{% block title %}Start Your Free Trial - Wizamart{% endblock %}
|
||||
{% block title %}Start Your Free Trial - Orion{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{# Stripe.js for payment #}
|
||||
|
||||
18
app/modules/billing/tests/unit/test_feature_service.py
Normal file
18
app/modules/billing/tests/unit/test_feature_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for FeatureService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.billing.services.feature_service import FeatureService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.billing
|
||||
class TestFeatureService:
|
||||
"""Test suite for FeatureService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = FeatureService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for PlatformPricingService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.billing.services.platform_pricing_service import PlatformPricingService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.billing
|
||||
class TestPlatformPricingService:
|
||||
"""Test suite for PlatformPricingService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = PlatformPricingService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
18
app/modules/billing/tests/unit/test_stripe_service.py
Normal file
18
app/modules/billing/tests/unit/test_stripe_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for StripeService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.billing.services.stripe_service import StripeService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.billing
|
||||
class TestStripeService:
|
||||
"""Test suite for StripeService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = StripeService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
18
app/modules/billing/tests/unit/test_usage_service.py
Normal file
18
app/modules/billing/tests/unit/test_usage_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for UsageService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.billing.services.usage_service import UsageService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.billing
|
||||
class TestUsageService:
|
||||
"""Test suite for UsageService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = UsageService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -37,7 +37,7 @@ class CartItemNotFoundException(ResourceNotFoundException):
|
||||
self.details.update({"product_id": product_id, "session_id": session_id})
|
||||
|
||||
|
||||
class EmptyCartException(ValidationException):
|
||||
class EmptyCartException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when trying to perform operations on an empty cart."""
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
@@ -45,7 +45,7 @@ class EmptyCartException(ValidationException):
|
||||
self.error_code = "CART_EMPTY"
|
||||
|
||||
|
||||
class CartValidationException(ValidationException):
|
||||
class CartValidationException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when cart data validation fails."""
|
||||
|
||||
def __init__(
|
||||
@@ -113,7 +113,7 @@ class InvalidCartQuantityException(ValidationException):
|
||||
self.error_code = "INVALID_CART_QUANTITY"
|
||||
|
||||
|
||||
class ProductNotAvailableForCartException(BusinessLogicException):
|
||||
class ProductNotAvailableForCartException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when product is not available for adding to cart."""
|
||||
|
||||
def __init__(self, product_id: int, reason: str):
|
||||
@@ -125,5 +125,3 @@ class ProductNotAvailableForCartException(BusinessLogicException):
|
||||
"reason": reason,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
0
app/modules/cart/tests/__init__.py
Normal file
0
app/modules/cart/tests/__init__.py
Normal file
0
app/modules/cart/tests/unit/__init__.py
Normal file
0
app/modules/cart/tests/unit/__init__.py
Normal file
18
app/modules/cart/tests/unit/test_cart_service.py
Normal file
18
app/modules/cart/tests/unit/test_cart_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for CartService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.cart.services.cart_service import CartService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.cart
|
||||
class TestCartService:
|
||||
"""Test suite for CartService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = CartService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -56,7 +56,7 @@ catalog_module = ModuleDefinition(
|
||||
description="Product catalog browsing and search for storefronts",
|
||||
version="1.0.0",
|
||||
is_self_contained=True,
|
||||
requires=["inventory"],
|
||||
requires=[],
|
||||
migrations_path="migrations",
|
||||
features=[
|
||||
"product_catalog", # Core product catalog functionality
|
||||
|
||||
@@ -63,7 +63,7 @@ class ProductAlreadyExistsException(ConflictException):
|
||||
)
|
||||
|
||||
|
||||
class ProductNotInCatalogException(ResourceNotFoundException):
|
||||
class ProductNotInCatalogException(ResourceNotFoundException): # noqa: MOD-025
|
||||
"""Raised when trying to access a product that's not in store's catalog."""
|
||||
|
||||
def __init__(self, product_id: int, store_id: int):
|
||||
@@ -129,7 +129,7 @@ class ProductValidationException(ValidationException):
|
||||
self.error_code = "PRODUCT_VALIDATION_FAILED"
|
||||
|
||||
|
||||
class CannotDeleteProductException(BusinessLogicException):
|
||||
class CannotDeleteProductException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when a product cannot be deleted due to dependencies."""
|
||||
|
||||
def __init__(self, product_id: int, reason: str, details: dict | None = None):
|
||||
@@ -140,7 +140,7 @@ class CannotDeleteProductException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class CannotDeleteProductWithInventoryException(BusinessLogicException):
|
||||
class CannotDeleteProductWithInventoryException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when trying to delete a product that has inventory."""
|
||||
|
||||
def __init__(self, product_id: int, inventory_count: int):
|
||||
@@ -154,7 +154,7 @@ class CannotDeleteProductWithInventoryException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class CannotDeleteProductWithOrdersException(BusinessLogicException):
|
||||
class CannotDeleteProductWithOrdersException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when trying to delete a product that has been ordered."""
|
||||
|
||||
def __init__(self, product_id: int, order_count: int):
|
||||
|
||||
@@ -10,7 +10,7 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.modules.inventory.schemas import InventoryLocationResponse
|
||||
from app.modules.inventory.schemas import InventoryLocationResponse # noqa: IMPORT-002
|
||||
from app.modules.marketplace.schemas import MarketplaceProductResponse # IMPORT-002
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.modules.inventory.schemas import InventoryLocationResponse
|
||||
from app.modules.inventory.schemas import InventoryLocationResponse # noqa: IMPORT-002
|
||||
from app.modules.marketplace.schemas import MarketplaceProductResponse # IMPORT-002
|
||||
|
||||
|
||||
|
||||
@@ -15,10 +15,13 @@ storefront operations only.
|
||||
import logging
|
||||
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.exceptions import ValidationException
|
||||
from app.modules.catalog.exceptions import ProductNotFoundException
|
||||
from app.modules.catalog.exceptions import (
|
||||
ProductNotFoundException,
|
||||
ProductValidationException,
|
||||
)
|
||||
from app.modules.catalog.models import Product, ProductTranslation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -91,9 +94,9 @@ class CatalogService:
|
||||
|
||||
return products, total
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error getting catalog products: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve products")
|
||||
raise ProductValidationException("Failed to retrieve products")
|
||||
|
||||
def search_products(
|
||||
self,
|
||||
@@ -174,9 +177,9 @@ class CatalogService:
|
||||
)
|
||||
return products, total
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error searching products: {str(e)}")
|
||||
raise ValidationException("Failed to search products")
|
||||
raise ProductValidationException("Failed to search products")
|
||||
|
||||
|
||||
# Create service instance
|
||||
|
||||
@@ -15,6 +15,7 @@ import logging
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.catalog.exceptions import ProductMediaException
|
||||
from app.modules.catalog.models import Product, ProductMedia
|
||||
from app.modules.cms.models import MediaFile
|
||||
|
||||
@@ -48,7 +49,7 @@ class ProductMediaService:
|
||||
Created or updated ProductMedia association
|
||||
|
||||
Raises:
|
||||
ValueError: If product or media doesn't belong to store
|
||||
ProductMediaException: If product or media doesn't belong to store
|
||||
"""
|
||||
# Verify product belongs to store
|
||||
product = (
|
||||
@@ -57,7 +58,10 @@ class ProductMediaService:
|
||||
.first()
|
||||
)
|
||||
if not product:
|
||||
raise ValueError(f"Product {product_id} not found for store {store_id}")
|
||||
raise ProductMediaException(
|
||||
product_id=product_id,
|
||||
message=f"Product {product_id} not found for store {store_id}",
|
||||
)
|
||||
|
||||
# Verify media belongs to store
|
||||
media = (
|
||||
@@ -66,7 +70,10 @@ class ProductMediaService:
|
||||
.first()
|
||||
)
|
||||
if not media:
|
||||
raise ValueError(f"Media {media_id} not found for store {store_id}")
|
||||
raise ProductMediaException(
|
||||
product_id=product_id,
|
||||
message=f"Media {media_id} not found for store {store_id}",
|
||||
)
|
||||
|
||||
# Check if already attached with same usage type
|
||||
existing = (
|
||||
@@ -128,7 +135,7 @@ class ProductMediaService:
|
||||
Number of associations removed
|
||||
|
||||
Raises:
|
||||
ValueError: If product doesn't belong to store
|
||||
ProductMediaException: If product doesn't belong to store
|
||||
"""
|
||||
# Verify product belongs to store
|
||||
product = (
|
||||
@@ -137,7 +144,10 @@ class ProductMediaService:
|
||||
.first()
|
||||
)
|
||||
if not product:
|
||||
raise ValueError(f"Product {product_id} not found for store {store_id}")
|
||||
raise ProductMediaException(
|
||||
product_id=product_id,
|
||||
message=f"Product {product_id} not found for store {store_id}",
|
||||
)
|
||||
|
||||
# Build query
|
||||
query = db.query(ProductMedia).filter(
|
||||
|
||||
@@ -11,12 +11,14 @@ This module provides:
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import ValidationException
|
||||
from app.modules.catalog.exceptions import (
|
||||
InvalidProductDataException,
|
||||
ProductAlreadyExistsException,
|
||||
ProductNotFoundException,
|
||||
ProductValidationException,
|
||||
)
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.catalog.schemas import ProductCreate, ProductUpdate
|
||||
@@ -57,9 +59,9 @@ class ProductService:
|
||||
|
||||
except ProductNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error getting product: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve product")
|
||||
raise ProductValidationException("Failed to retrieve product")
|
||||
|
||||
def create_product(
|
||||
self, db: Session, store_id: int, product_data: ProductCreate
|
||||
@@ -77,7 +79,7 @@ class ProductService:
|
||||
|
||||
Raises:
|
||||
ProductAlreadyExistsException: If product already in catalog
|
||||
ValidationException: If marketplace product not found
|
||||
InvalidProductDataException: If marketplace product not found
|
||||
"""
|
||||
try:
|
||||
# Verify marketplace product exists
|
||||
@@ -88,7 +90,7 @@ class ProductService:
|
||||
)
|
||||
|
||||
if not marketplace_product:
|
||||
raise ValidationException(
|
||||
raise InvalidProductDataException(
|
||||
f"Marketplace product {product_data.marketplace_product_id} not found"
|
||||
)
|
||||
|
||||
@@ -129,11 +131,11 @@ class ProductService:
|
||||
logger.info(f"Added product {product.id} to store {store_id} catalog")
|
||||
return product
|
||||
|
||||
except (ProductAlreadyExistsException, ValidationException):
|
||||
except (ProductAlreadyExistsException, InvalidProductDataException):
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error creating product: {str(e)}")
|
||||
raise ValidationException("Failed to create product")
|
||||
raise ProductValidationException("Failed to create product")
|
||||
|
||||
def update_product(
|
||||
self,
|
||||
@@ -171,9 +173,9 @@ class ProductService:
|
||||
|
||||
except ProductNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error updating product: {str(e)}")
|
||||
raise ValidationException("Failed to update product")
|
||||
raise ProductValidationException("Failed to update product")
|
||||
|
||||
def delete_product(self, db: Session, store_id: int, product_id: int) -> bool:
|
||||
"""
|
||||
@@ -197,9 +199,9 @@ class ProductService:
|
||||
|
||||
except ProductNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error deleting product: {str(e)}")
|
||||
raise ValidationException("Failed to delete product")
|
||||
raise ProductValidationException("Failed to delete product")
|
||||
|
||||
def get_store_products(
|
||||
self,
|
||||
@@ -238,9 +240,9 @@ class ProductService:
|
||||
|
||||
return products, total
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error getting store products: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve products")
|
||||
raise ProductValidationException("Failed to retrieve products")
|
||||
|
||||
def search_products(
|
||||
self,
|
||||
@@ -326,9 +328,9 @@ class ProductService:
|
||||
)
|
||||
return products, total
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error searching products: {str(e)}")
|
||||
raise ValidationException("Failed to search products")
|
||||
raise ProductValidationException("Failed to search products")
|
||||
|
||||
|
||||
# Create service instance
|
||||
|
||||
18
app/modules/catalog/tests/unit/test_catalog_service.py
Normal file
18
app/modules/catalog/tests/unit/test_catalog_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for CatalogService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.catalog.services.catalog_service import CatalogService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.catalog
|
||||
class TestCatalogService:
|
||||
"""Test suite for CatalogService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = CatalogService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
18
app/modules/catalog/tests/unit/test_product_media_service.py
Normal file
18
app/modules/catalog/tests/unit/test_product_media_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for ProductMediaService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.catalog.services.product_media_service import ProductMediaService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.catalog
|
||||
class TestProductMediaService:
|
||||
"""Test suite for ProductMediaService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = ProductMediaService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -306,7 +306,7 @@ class TestProductInventoryProperties:
|
||||
|
||||
def test_physical_product_with_inventory(self, db, test_store):
|
||||
"""Test physical product calculates inventory from entries."""
|
||||
from app.modules.inventory.models import Inventory
|
||||
from app.modules.inventory.models import Inventory # noqa: IMPORT-002
|
||||
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
@@ -364,7 +364,7 @@ class TestProductInventoryProperties:
|
||||
|
||||
def test_digital_product_ignores_inventory_entries(self, db, test_store):
|
||||
"""Test digital product returns unlimited even with inventory entries."""
|
||||
from app.modules.inventory.models import Inventory
|
||||
from app.modules.inventory.models import Inventory # noqa: IMPORT-002
|
||||
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.exceptions.base import (
|
||||
)
|
||||
|
||||
|
||||
class CheckoutValidationException(ValidationException):
|
||||
class CheckoutValidationException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when checkout data validation fails."""
|
||||
|
||||
def __init__(
|
||||
@@ -29,7 +29,7 @@ class CheckoutValidationException(ValidationException):
|
||||
self.error_code = "CHECKOUT_VALIDATION_FAILED"
|
||||
|
||||
|
||||
class CheckoutSessionNotFoundException(ResourceNotFoundException):
|
||||
class CheckoutSessionNotFoundException(ResourceNotFoundException): # noqa: MOD-025
|
||||
"""Raised when checkout session is not found."""
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
@@ -41,7 +41,7 @@ class CheckoutSessionNotFoundException(ResourceNotFoundException):
|
||||
)
|
||||
|
||||
|
||||
class CheckoutSessionExpiredException(BusinessLogicException):
|
||||
class CheckoutSessionExpiredException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when checkout session has expired."""
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
@@ -52,7 +52,7 @@ class CheckoutSessionExpiredException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class EmptyCheckoutException(ValidationException):
|
||||
class EmptyCheckoutException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when trying to checkout with empty cart."""
|
||||
|
||||
def __init__(self):
|
||||
@@ -63,7 +63,7 @@ class EmptyCheckoutException(ValidationException):
|
||||
self.error_code = "EMPTY_CHECKOUT"
|
||||
|
||||
|
||||
class PaymentRequiredException(BusinessLogicException):
|
||||
class PaymentRequiredException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when payment is required but not provided."""
|
||||
|
||||
def __init__(self, order_total: float):
|
||||
@@ -74,7 +74,7 @@ class PaymentRequiredException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class PaymentFailedException(BusinessLogicException):
|
||||
class PaymentFailedException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when payment processing fails."""
|
||||
|
||||
def __init__(self, reason: str, details: dict | None = None):
|
||||
@@ -85,7 +85,7 @@ class PaymentFailedException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class InvalidShippingAddressException(ValidationException):
|
||||
class InvalidShippingAddressException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when shipping address is invalid or missing."""
|
||||
|
||||
def __init__(self, message: str = "Invalid shipping address", details: dict | None = None):
|
||||
@@ -97,7 +97,7 @@ class InvalidShippingAddressException(ValidationException):
|
||||
self.error_code = "INVALID_SHIPPING_ADDRESS"
|
||||
|
||||
|
||||
class ShippingMethodNotAvailableException(BusinessLogicException):
|
||||
class ShippingMethodNotAvailableException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when selected shipping method is not available."""
|
||||
|
||||
def __init__(self, shipping_method: str, reason: str | None = None):
|
||||
@@ -111,7 +111,7 @@ class ShippingMethodNotAvailableException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class CheckoutInventoryException(BusinessLogicException):
|
||||
class CheckoutInventoryException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when inventory check fails during checkout."""
|
||||
|
||||
def __init__(self, product_id: int, available: int, requested: int):
|
||||
|
||||
0
app/modules/checkout/tests/__init__.py
Normal file
0
app/modules/checkout/tests/__init__.py
Normal file
0
app/modules/checkout/tests/unit/__init__.py
Normal file
0
app/modules/checkout/tests/unit/__init__.py
Normal file
18
app/modules/checkout/tests/unit/test_checkout_service.py
Normal file
18
app/modules/checkout/tests/unit/test_checkout_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for CheckoutService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.checkout.services.checkout_service import CheckoutService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.checkout
|
||||
class TestCheckoutService:
|
||||
"""Test suite for CheckoutService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = CheckoutService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -69,7 +69,7 @@ class ContentPageNotFoundException(ResourceNotFoundException):
|
||||
)
|
||||
|
||||
|
||||
class ContentPageAlreadyExistsException(ConflictException):
|
||||
class ContentPageAlreadyExistsException(ConflictException): # noqa: MOD-025
|
||||
"""Raised when a content page with the same slug already exists."""
|
||||
|
||||
def __init__(self, slug: str, store_id: int | None = None):
|
||||
@@ -84,7 +84,7 @@ class ContentPageAlreadyExistsException(ConflictException):
|
||||
)
|
||||
|
||||
|
||||
class ContentPageSlugReservedException(ValidationException):
|
||||
class ContentPageSlugReservedException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when trying to use a reserved slug."""
|
||||
|
||||
def __init__(self, slug: str):
|
||||
@@ -96,7 +96,7 @@ class ContentPageSlugReservedException(ValidationException):
|
||||
self.error_code = "CONTENT_PAGE_SLUG_RESERVED"
|
||||
|
||||
|
||||
class ContentPageNotPublishedException(BusinessLogicException):
|
||||
class ContentPageNotPublishedException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when trying to access an unpublished content page."""
|
||||
|
||||
def __init__(self, slug: str):
|
||||
@@ -118,7 +118,7 @@ class UnauthorizedContentPageAccessException(AuthorizationException):
|
||||
)
|
||||
|
||||
|
||||
class StoreNotAssociatedException(AuthorizationException):
|
||||
class StoreNotAssociatedException(AuthorizationException): # noqa: MOD-025
|
||||
"""Raised when a user is not associated with a store."""
|
||||
|
||||
def __init__(self):
|
||||
@@ -143,7 +143,7 @@ class NoPlatformSubscriptionException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class ContentPageValidationException(ValidationException):
|
||||
class ContentPageValidationException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when content page data validation fails."""
|
||||
|
||||
def __init__(self, field: str, message: str, value: str | None = None):
|
||||
@@ -175,7 +175,7 @@ class MediaNotFoundException(ResourceNotFoundException):
|
||||
)
|
||||
|
||||
|
||||
class MediaUploadException(BusinessLogicException):
|
||||
class MediaUploadException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when media upload fails."""
|
||||
|
||||
def __init__(self, message: str = "Media upload failed", details: dict[str, Any] | None = None):
|
||||
@@ -241,7 +241,7 @@ class MediaOptimizationException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class MediaDeleteException(BusinessLogicException):
|
||||
class MediaDeleteException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when media deletion fails."""
|
||||
|
||||
def __init__(self, message: str, details: dict[str, Any] | None = None):
|
||||
@@ -269,7 +269,7 @@ class StoreThemeNotFoundException(ResourceNotFoundException):
|
||||
)
|
||||
|
||||
|
||||
class InvalidThemeDataException(ValidationException):
|
||||
class InvalidThemeDataException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when theme data is invalid."""
|
||||
|
||||
def __init__(
|
||||
@@ -321,7 +321,7 @@ class ThemeValidationException(ValidationException):
|
||||
self.error_code = "THEME_VALIDATION_FAILED"
|
||||
|
||||
|
||||
class ThemePresetAlreadyAppliedException(BusinessLogicException):
|
||||
class ThemePresetAlreadyAppliedException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when trying to apply the same preset that's already active."""
|
||||
|
||||
def __init__(self, preset_name: str, store_code: str):
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
"creating_account": "Erstelle Ihr Konto..."
|
||||
},
|
||||
"success": {
|
||||
"title": "Willkommen bei Wizamart!",
|
||||
"title": "Willkommen bei Orion!",
|
||||
"subtitle": "Ihr Konto wurde erstellt und Ihre {trial_days}-tägige kostenlose Testphase hat begonnen.",
|
||||
"what_next": "Was kommt als Nächstes?",
|
||||
"step_connect": "Letzshop verbinden:",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"cta": {
|
||||
"title": "Bereit, Ihre Bestellungen zu optimieren?",
|
||||
"subtitle": "Schließen Sie sich Letzshop-Händlern an, die Wizamart für ihre Bestellverwaltung vertrauen. Starten Sie heute Ihre {trial_days}-tägige kostenlose Testversion.",
|
||||
"subtitle": "Schließen Sie sich Letzshop-Händlern an, die Orion für ihre Bestellverwaltung vertrauen. Starten Sie heute Ihre {trial_days}-tägige kostenlose Testversion.",
|
||||
"button": "Kostenlos testen"
|
||||
},
|
||||
"footer": {
|
||||
@@ -157,7 +157,7 @@
|
||||
"quick_links": "Schnelllinks",
|
||||
"platform": "Plattform",
|
||||
"contact": "Kontakt",
|
||||
"copyright": "© {year} Wizamart. Entwickelt für den luxemburgischen E-Commerce.",
|
||||
"copyright": "© {year} Orion. Entwickelt für den luxemburgischen E-Commerce.",
|
||||
"privacy": "Datenschutzerklärung",
|
||||
"terms": "Nutzungsbedingungen",
|
||||
"about": "Über uns",
|
||||
@@ -188,7 +188,7 @@
|
||||
"how_step1": "Letzshop verbinden",
|
||||
"how_step1_desc": "Geben Sie Ihre Letzshop-API-Zugangsdaten ein. In 2 Minuten erledigt, keine technischen Kenntnisse erforderlich.",
|
||||
"how_step2": "Bestellungen kommen rein",
|
||||
"how_step2_desc": "Bestellungen werden automatisch synchronisiert. Bestätigen und Tracking direkt von Wizamart hinzufügen.",
|
||||
"how_step2_desc": "Bestellungen werden automatisch synchronisiert. Bestätigen und Tracking direkt von Orion hinzufügen.",
|
||||
"how_step3": "Rechnungen erstellen",
|
||||
"how_step3_desc": "Ein Klick, um konforme PDF-Rechnungen mit korrekter MwSt für jedes EU-Land zu erstellen.",
|
||||
"how_step4": "Ihr Geschäft ausbauen",
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
"creating_account": "Creating your account..."
|
||||
},
|
||||
"success": {
|
||||
"title": "Welcome to Wizamart!",
|
||||
"title": "Welcome to Orion!",
|
||||
"subtitle": "Your account has been created and your {trial_days}-day free trial has started.",
|
||||
"what_next": "What's Next?",
|
||||
"step_connect": "Connect Letzshop:",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"cta": {
|
||||
"title": "Ready to Streamline Your Orders?",
|
||||
"subtitle": "Join Letzshop stores who trust Wizamart for their order management. Start your {trial_days}-day free trial today.",
|
||||
"subtitle": "Join Letzshop stores who trust Orion for their order management. Start your {trial_days}-day free trial today.",
|
||||
"button": "Start Free Trial"
|
||||
},
|
||||
"footer": {
|
||||
@@ -157,7 +157,7 @@
|
||||
"quick_links": "Quick Links",
|
||||
"platform": "Platform",
|
||||
"contact": "Contact",
|
||||
"copyright": "© {year} Wizamart. Built for Luxembourg e-commerce.",
|
||||
"copyright": "© {year} Orion. Built for Luxembourg e-commerce.",
|
||||
"privacy": "Privacy Policy",
|
||||
"terms": "Terms of Service",
|
||||
"about": "About Us",
|
||||
@@ -188,7 +188,7 @@
|
||||
"how_step1": "Connect Letzshop",
|
||||
"how_step1_desc": "Enter your Letzshop API credentials. Done in 2 minutes, no technical skills needed.",
|
||||
"how_step2": "Orders Flow In",
|
||||
"how_step2_desc": "Orders sync automatically. Confirm and add tracking directly from Wizamart.",
|
||||
"how_step2_desc": "Orders sync automatically. Confirm and add tracking directly from Orion.",
|
||||
"how_step3": "Generate Invoices",
|
||||
"how_step3_desc": "One click to create compliant PDF invoices with correct VAT for any EU country.",
|
||||
"how_step4": "Grow Your Business",
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
"creating_account": "Création de votre compte..."
|
||||
},
|
||||
"success": {
|
||||
"title": "Bienvenue sur Wizamart !",
|
||||
"title": "Bienvenue sur Orion !",
|
||||
"subtitle": "Votre compte a été créé et votre essai gratuit de {trial_days} jours a commencé.",
|
||||
"what_next": "Et maintenant ?",
|
||||
"step_connect": "Connecter Letzshop :",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"cta": {
|
||||
"title": "Prêt à optimiser vos commandes ?",
|
||||
"subtitle": "Rejoignez les vendeurs Letzshop qui font confiance à Wizamart pour leur gestion de commandes. Commencez votre essai gratuit de {trial_days} jours aujourd'hui.",
|
||||
"subtitle": "Rejoignez les vendeurs Letzshop qui font confiance à Orion pour leur gestion de commandes. Commencez votre essai gratuit de {trial_days} jours aujourd'hui.",
|
||||
"button": "Essai gratuit"
|
||||
},
|
||||
"footer": {
|
||||
@@ -157,7 +157,7 @@
|
||||
"quick_links": "Liens rapides",
|
||||
"platform": "Plateforme",
|
||||
"contact": "Contact",
|
||||
"copyright": "© {year} Wizamart. Conçu pour le e-commerce luxembourgeois.",
|
||||
"copyright": "© {year} Orion. Conçu pour le e-commerce luxembourgeois.",
|
||||
"privacy": "Politique de confidentialité",
|
||||
"terms": "Conditions d'utilisation",
|
||||
"about": "À propos",
|
||||
@@ -188,7 +188,7 @@
|
||||
"how_step1": "Connecter Letzshop",
|
||||
"how_step1_desc": "Entrez vos identifiants API Letzshop. Fait en 2 minutes, aucune compétence technique requise.",
|
||||
"how_step2": "Les commandes arrivent",
|
||||
"how_step2_desc": "Les commandes se synchronisent automatiquement. Confirmez et ajoutez le suivi directement depuis Wizamart.",
|
||||
"how_step2_desc": "Les commandes se synchronisent automatiquement. Confirmez et ajoutez le suivi directement depuis Orion.",
|
||||
"how_step3": "Générer des factures",
|
||||
"how_step3_desc": "Un clic pour créer des factures PDF conformes avec la TVA correcte pour tout pays UE.",
|
||||
"how_step4": "Développez votre entreprise",
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
"creating_account": "Erstellt Äre Kont..."
|
||||
},
|
||||
"success": {
|
||||
"title": "Wëllkomm bei Wizamart!",
|
||||
"title": "Wëllkomm bei Orion!",
|
||||
"subtitle": "Äre Kont gouf erstallt an Är {trial_days}-Deeg gratis Testversioun huet ugefaang.",
|
||||
"what_next": "Wat kënnt duerno?",
|
||||
"step_connect": "Letzshop verbannen:",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"cta": {
|
||||
"title": "Prett fir Är Bestellungen ze optiméieren?",
|
||||
"subtitle": "Schléisst Iech Letzshop Händler un déi Wizamart fir hir Bestellungsverwaltung vertrauen. Fänkt haut Är {trial_days}-Deeg gratis Testversioun un.",
|
||||
"subtitle": "Schléisst Iech Letzshop Händler un déi Orion fir hir Bestellungsverwaltung vertrauen. Fänkt haut Är {trial_days}-Deeg gratis Testversioun un.",
|
||||
"button": "Gratis Testen"
|
||||
},
|
||||
"footer": {
|
||||
@@ -157,7 +157,7 @@
|
||||
"quick_links": "Séier Linken",
|
||||
"platform": "Plattform",
|
||||
"contact": "Kontakt",
|
||||
"copyright": "© {year} Wizamart. Gemaach fir de lëtzebuergeschen E-Commerce.",
|
||||
"copyright": "© {year} Orion. Gemaach fir de lëtzebuergeschen E-Commerce.",
|
||||
"privacy": "Dateschutzrichtlinn",
|
||||
"terms": "Notzungsbedéngungen",
|
||||
"about": "Iwwer eis",
|
||||
@@ -188,7 +188,7 @@
|
||||
"how_step1": "Letzshop verbannen",
|
||||
"how_step1_desc": "Gitt Är Letzshop API Zougangsdaten an. An 2 Minutte fäerdeg, keng technesch Kenntnisser néideg.",
|
||||
"how_step2": "Bestellunge kommen eran",
|
||||
"how_step2_desc": "Bestellunge ginn automatesch synchroniséiert. Confirméiert an Tracking direkt vu Wizamart derbäisetzen.",
|
||||
"how_step2_desc": "Bestellunge ginn automatesch synchroniséiert. Confirméiert an Tracking direkt vu Orion derbäisetzen.",
|
||||
"how_step3": "Rechnunge generéieren",
|
||||
"how_step3_desc": "Ee Klick fir konform PDF Rechnunge mat korrekter TVA fir all EU Land ze erstellen.",
|
||||
"how_step4": "Äert Geschäft ausbauen",
|
||||
|
||||
@@ -165,8 +165,8 @@ async def homepage(
|
||||
logger.info(f"[HOMEPAGE] Rendering CMS homepage with template: {template_path}")
|
||||
return templates.TemplateResponse(template_path, context)
|
||||
|
||||
# Fallback: Default wizamart homepage (no CMS content)
|
||||
logger.info("[HOMEPAGE] No CMS homepage found, using default wizamart template")
|
||||
# Fallback: Default orion homepage (no CMS content)
|
||||
logger.info("[HOMEPAGE] No CMS homepage found, using default orion template")
|
||||
context = get_platform_context(request, db)
|
||||
context["tiers"] = _get_tiers_data(db)
|
||||
|
||||
@@ -204,7 +204,7 @@ async def homepage(
|
||||
]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"cms/platform/homepage-wizamart.html",
|
||||
"cms/platform/homepage-orion.html",
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ async def store_content_page(
|
||||
Generic content page handler for store shop (CMS).
|
||||
|
||||
Handles dynamic content pages like:
|
||||
- /stores/wizamart/about, /stores/wizamart/faq, /stores/wizamart/contact, etc.
|
||||
- /stores/orion/about, /stores/orion/faq, /stores/orion/contact, etc.
|
||||
|
||||
Features:
|
||||
- Two-tier system: Store overrides take priority, fallback to platform defaults
|
||||
|
||||
@@ -13,10 +13,6 @@ from app.modules.cms.services.media_service import (
|
||||
MediaService,
|
||||
media_service,
|
||||
)
|
||||
from app.modules.cms.services.store_email_settings_service import (
|
||||
StoreEmailSettingsService,
|
||||
store_email_settings_service,
|
||||
)
|
||||
from app.modules.cms.services.store_theme_service import (
|
||||
StoreThemeService,
|
||||
store_theme_service,
|
||||
@@ -29,6 +25,4 @@ __all__ = [
|
||||
"media_service",
|
||||
"StoreThemeService",
|
||||
"store_theme_service",
|
||||
"StoreEmailSettingsService",
|
||||
"store_email_settings_service",
|
||||
]
|
||||
|
||||
@@ -141,7 +141,7 @@ class MediaService:
|
||||
except ImportError:
|
||||
logger.debug("PIL not available, skipping image dimension detection")
|
||||
return None
|
||||
except Exception as e:
|
||||
except OSError as e:
|
||||
logger.warning(f"Could not get image dimensions: {e}")
|
||||
return None
|
||||
|
||||
@@ -216,7 +216,7 @@ class MediaService:
|
||||
except ImportError:
|
||||
logger.debug("PIL not available, skipping variant generation")
|
||||
return {}
|
||||
except Exception as e:
|
||||
except OSError as e:
|
||||
logger.warning(f"Could not generate image variants: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ Handles theme CRUD operations, preset application, and validation.
|
||||
import logging
|
||||
import re
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.cms.exceptions import (
|
||||
@@ -205,7 +206,7 @@ class StoreThemeService:
|
||||
# Re-raise custom exceptions
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
self.logger.error(f"Failed to update theme for store {store_code}: {e}")
|
||||
raise ThemeOperationException(
|
||||
operation="update", store_code=store_code, reason=str(e)
|
||||
@@ -324,7 +325,7 @@ class StoreThemeService:
|
||||
# Re-raise custom exceptions
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
self.logger.error(f"Failed to apply preset to store {store_code}: {e}")
|
||||
raise ThemeOperationException(
|
||||
operation="apply_preset", store_code=store_code, reason=str(e)
|
||||
@@ -394,7 +395,7 @@ class StoreThemeService:
|
||||
# Re-raise custom exceptions
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
self.logger.error(f"Failed to delete theme for store {store_code}: {e}")
|
||||
raise ThemeOperationException(
|
||||
operation="delete", store_code=store_code, reason=str(e)
|
||||
|
||||
@@ -201,49 +201,3 @@ def get_preset_preview(preset_name: str) -> dict:
|
||||
"body_font": preset["fonts"]["body"],
|
||||
"layout_style": preset["layout"]["style"],
|
||||
}
|
||||
|
||||
|
||||
def create_custom_preset(
|
||||
colors: dict, fonts: dict, layout: dict, name: str = "custom"
|
||||
) -> dict:
|
||||
"""
|
||||
Create a custom preset from provided settings.
|
||||
|
||||
Args:
|
||||
colors: Dict with primary, secondary, accent, background, text, border
|
||||
fonts: Dict with heading and body fonts
|
||||
layout: Dict with style, header, product_card
|
||||
name: Name for the custom preset
|
||||
|
||||
Returns:
|
||||
dict: Custom preset configuration
|
||||
|
||||
Example:
|
||||
custom = create_custom_preset(
|
||||
colors={"primary": "#ff0000", "secondary": "#00ff00", ...},
|
||||
fonts={"heading": "Arial", "body": "Arial"},
|
||||
layout={"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
name="my_custom_theme"
|
||||
)
|
||||
"""
|
||||
# Validate colors
|
||||
required_colors = ["primary", "secondary", "accent", "background", "text", "border"]
|
||||
for color_key in required_colors:
|
||||
if color_key not in colors:
|
||||
colors[color_key] = THEME_PRESETS["default"]["colors"][color_key]
|
||||
|
||||
# Validate fonts
|
||||
if "heading" not in fonts:
|
||||
fonts["heading"] = "Inter, sans-serif"
|
||||
if "body" not in fonts:
|
||||
fonts["body"] = "Inter, sans-serif"
|
||||
|
||||
# Validate layout
|
||||
if "style" not in layout:
|
||||
layout["style"] = "grid"
|
||||
if "header" not in layout:
|
||||
layout["header"] = "fixed"
|
||||
if "product_card" not in layout:
|
||||
layout["product_card"] = "modern"
|
||||
|
||||
return {"colors": colors, "fonts": fonts, "layout": layout}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{# app/templates/platform/homepage-modern.html #}
|
||||
{# Wizamart OMS - Luxembourg-focused homepage inspired by Veeqo #}
|
||||
{# Orion OMS - Luxembourg-focused homepage inspired by Veeqo #}
|
||||
{% extends "platform/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Wizamart - The Back-Office for Letzshop Sellers
|
||||
Orion - The Back-Office for Letzshop Sellers
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
<span class="ml-4 text-gray-400 text-sm">Wizamart Dashboard</span>
|
||||
<span class="ml-4 text-gray-400 text-sm">Orion Dashboard</span>
|
||||
</div>
|
||||
{# Mock dashboard content #}
|
||||
<div class="p-6 space-y-4">
|
||||
@@ -219,7 +219,7 @@
|
||||
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-lg h-full">
|
||||
<div class="w-12 h-12 rounded-full bg-blue-500 text-white flex items-center justify-center font-bold text-xl mb-6">2</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">Orders Flow In</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">Orders sync automatically. Confirm and add tracking directly from Wizamart.</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">Orders sync automatically. Confirm and add tracking directly from Orion.</p>
|
||||
</div>
|
||||
<div class="hidden lg:block absolute top-1/2 -right-4 w-8 h-0.5 bg-blue-300"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{# app/templates/platform/homepage-wizamart.html #}
|
||||
{# Wizamart Marketing Homepage - Letzshop OMS Platform #}
|
||||
{# app/templates/platform/homepage-orion.html #}
|
||||
{# Orion Marketing Homepage - Letzshop OMS Platform #}
|
||||
{% extends "platform/base.html" %}
|
||||
{% from 'shared/macros/inputs.html' import toggle_switch %}
|
||||
|
||||
{% block title %}Wizamart - Order Management for Letzshop Sellers{% endblock %}
|
||||
{% block title %}Orion - Order Management for Letzshop Sellers{% endblock %}
|
||||
{% block meta_description %}Lightweight OMS for Letzshop stores. Manage orders, inventory, and invoicing. Start your 30-day free trial today.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -212,7 +212,7 @@
|
||||
|
||||
{# CTA Button #}
|
||||
{% if tier.is_enterprise %}
|
||||
<a href="mailto:sales@wizamart.com?subject=Enterprise%20Plan%20Inquiry"
|
||||
<a href="mailto:sales@orion.lu?subject=Enterprise%20Plan%20Inquiry"
|
||||
class="block w-full py-3 px-4 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white font-semibold rounded-xl text-center hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
{{ _("cms.platform.pricing.contact_sales") }}
|
||||
</a>
|
||||
0
app/modules/cms/tests/__init__.py
Normal file
0
app/modules/cms/tests/__init__.py
Normal file
0
app/modules/cms/tests/unit/__init__.py
Normal file
0
app/modules/cms/tests/unit/__init__.py
Normal file
18
app/modules/cms/tests/unit/test_content_page_service.py
Normal file
18
app/modules/cms/tests/unit/test_content_page_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for ContentPageService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.cms.services.content_page_service import ContentPageService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.cms
|
||||
class TestContentPageService:
|
||||
"""Test suite for ContentPageService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = ContentPageService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
18
app/modules/cms/tests/unit/test_media_service.py
Normal file
18
app/modules/cms/tests/unit/test_media_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for MediaService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.cms.services.media_service import MediaService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.cms
|
||||
class TestMediaService:
|
||||
"""Test suite for MediaService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = MediaService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
18
app/modules/cms/tests/unit/test_store_theme_service.py
Normal file
18
app/modules/cms/tests/unit/test_store_theme_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for StoreThemeService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.cms.services.store_theme_service import StoreThemeService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.cms
|
||||
class TestStoreThemeService:
|
||||
"""Test suite for StoreThemeService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = StoreThemeService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
23
app/modules/cms/tests/unit/test_theme_presets.py
Normal file
23
app/modules/cms/tests/unit/test_theme_presets.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Unit tests for theme_presets."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.cms.services.theme_presets import get_available_presets, get_preset
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.cms
|
||||
class TestThemePresets:
|
||||
"""Test suite for theme preset functions."""
|
||||
|
||||
def test_get_available_presets(self):
|
||||
"""Available presets returns a list."""
|
||||
presets = get_available_presets()
|
||||
assert isinstance(presets, list)
|
||||
|
||||
def test_get_preset_default(self):
|
||||
"""Default preset can be retrieved."""
|
||||
presets = get_available_presets()
|
||||
if presets:
|
||||
preset = get_preset(presets[0])
|
||||
assert isinstance(preset, dict)
|
||||
@@ -7,15 +7,15 @@ Exceptions for core platform functionality including:
|
||||
- Settings management
|
||||
"""
|
||||
|
||||
from app.exceptions import WizamartException
|
||||
from app.exceptions import OrionException
|
||||
|
||||
|
||||
class CoreException(WizamartException):
|
||||
class CoreException(OrionException): # noqa: MOD-025
|
||||
"""Base exception for core module."""
|
||||
|
||||
|
||||
|
||||
class MenuConfigurationError(CoreException):
|
||||
class MenuConfigurationError(CoreException): # noqa: MOD-025
|
||||
"""Error in menu configuration."""
|
||||
|
||||
|
||||
@@ -25,6 +25,5 @@ class SettingsError(CoreException):
|
||||
|
||||
|
||||
|
||||
class DashboardError(CoreException):
|
||||
class DashboardError(CoreException): # noqa: MOD-025
|
||||
"""Error in dashboard operations."""
|
||||
|
||||
|
||||
@@ -663,11 +663,11 @@ def send_test_email(
|
||||
email_log = email_service.send_raw(
|
||||
to_email=request.to_email,
|
||||
to_name=None,
|
||||
subject="Wizamart Platform - Test Email",
|
||||
subject="Orion Platform - Test Email",
|
||||
body_html=f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Test Email from Wizamart</h2>
|
||||
<h2 style="color: #6b46c1;">Test Email from Orion</h2>
|
||||
<p>This is a test email to verify your platform email configuration.</p>
|
||||
<p>If you received this email, your email settings are working correctly!</p>
|
||||
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;">
|
||||
@@ -678,7 +678,7 @@ def send_test_email(
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
body_text=f"Test email from Wizamart platform.\n\nProvider: {app_settings.email_provider}\nFrom: {app_settings.email_from_address}",
|
||||
body_text=f"Test email from Orion platform.\n\nProvider: {app_settings.email_provider}\nFrom: {app_settings.email_from_address}",
|
||||
is_platform_email=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
@@ -42,7 +43,7 @@ class AdminSettingsService:
|
||||
.filter(func.lower(AdminSetting.key) == key.lower())
|
||||
.first()
|
||||
)
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to get setting {key}: {str(e)}")
|
||||
return None
|
||||
|
||||
@@ -73,7 +74,7 @@ class AdminSettingsService:
|
||||
if setting.value_type == "json":
|
||||
return json.loads(setting.value)
|
||||
return setting.value
|
||||
except Exception as e:
|
||||
except (ValueError, TypeError, KeyError) as e:
|
||||
logger.error(f"Failed to convert setting {key} value: {str(e)}")
|
||||
return default
|
||||
|
||||
@@ -99,7 +100,7 @@ class AdminSettingsService:
|
||||
AdminSettingResponse.model_validate(setting) for setting in settings
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to get settings: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_all_settings", reason="Database query failed"
|
||||
@@ -172,7 +173,7 @@ class AdminSettingsService:
|
||||
|
||||
except ValidationException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to create setting: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="create_setting", reason="Database operation failed"
|
||||
@@ -212,7 +213,7 @@ class AdminSettingsService:
|
||||
|
||||
except ValidationException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to update setting {key}: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="update_setting", reason="Database operation failed"
|
||||
@@ -245,7 +246,7 @@ class AdminSettingsService:
|
||||
|
||||
return f"Setting '{key}' successfully deleted"
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Failed to delete setting {key}: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="delete_setting", reason="Database operation failed"
|
||||
@@ -267,7 +268,7 @@ class AdminSettingsService:
|
||||
raise ValueError("Invalid boolean value")
|
||||
elif value_type == "json":
|
||||
json.loads(value)
|
||||
except Exception as e:
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValidationException(
|
||||
f"Value '{value}' is not valid for type '{value_type}': {str(e)}"
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@ import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -195,7 +197,7 @@ class R2StorageBackend(StorageBackend):
|
||||
|
||||
return self.get_url(file_path)
|
||||
|
||||
except Exception as e:
|
||||
except ClientError as e:
|
||||
logger.error(f"R2 upload failed for {file_path}: {e}")
|
||||
raise
|
||||
|
||||
@@ -214,7 +216,7 @@ class R2StorageBackend(StorageBackend):
|
||||
logger.debug(f"Deleted from R2: {file_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
except ClientError as e:
|
||||
logger.error(f"R2 delete failed for {file_path}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
0
app/modules/core/tests/__init__.py
Normal file
0
app/modules/core/tests/__init__.py
Normal file
0
app/modules/core/tests/unit/__init__.py
Normal file
0
app/modules/core/tests/unit/__init__.py
Normal file
18
app/modules/core/tests/unit/test_admin_settings_service.py
Normal file
18
app/modules/core/tests/unit/test_admin_settings_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for AdminSettingsService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.core.services.admin_settings_service import AdminSettingsService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.core
|
||||
class TestAdminSettingsService:
|
||||
"""Test suite for AdminSettingsService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = AdminSettingsService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
18
app/modules/core/tests/unit/test_menu_discovery_service.py
Normal file
18
app/modules/core/tests/unit/test_menu_discovery_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for MenuDiscoveryService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.core.services.menu_discovery_service import MenuDiscoveryService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.core
|
||||
class TestMenuDiscoveryService:
|
||||
"""Test suite for MenuDiscoveryService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = MenuDiscoveryService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
18
app/modules/core/tests/unit/test_menu_service.py
Normal file
18
app/modules/core/tests/unit/test_menu_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for MenuService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.core.services.menu_service import MenuService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.core
|
||||
class TestMenuService:
|
||||
"""Test suite for MenuService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = MenuService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for PlatformSettingsService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.core.services.platform_settings_service import PlatformSettingsService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.core
|
||||
class TestPlatformSettingsService:
|
||||
"""Test suite for PlatformSettingsService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = PlatformSettingsService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
16
app/modules/core/tests/unit/test_storage_service.py
Normal file
16
app/modules/core/tests/unit/test_storage_service.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Unit tests for StorageService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.core.services.storage_service import get_storage_backend
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.core
|
||||
class TestStorageService:
|
||||
"""Test suite for storage service."""
|
||||
|
||||
def test_get_storage_backend(self):
|
||||
"""Storage backend can be retrieved."""
|
||||
backend = get_storage_backend()
|
||||
assert backend is not None
|
||||
@@ -53,7 +53,7 @@ class CustomerNotFoundException(ResourceNotFoundException):
|
||||
)
|
||||
|
||||
|
||||
class CustomerAlreadyExistsException(ConflictException):
|
||||
class CustomerAlreadyExistsException(ConflictException): # noqa: MOD-025
|
||||
"""Raised when trying to create a customer that already exists."""
|
||||
|
||||
def __init__(self, email: str):
|
||||
@@ -109,7 +109,7 @@ class CustomerValidationException(ValidationException):
|
||||
self.error_code = "CUSTOMER_VALIDATION_FAILED"
|
||||
|
||||
|
||||
class CustomerAuthorizationException(BusinessLogicException):
|
||||
class CustomerAuthorizationException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when customer is not authorized for operation."""
|
||||
|
||||
def __init__(self, customer_email: str, operation: str):
|
||||
@@ -170,7 +170,7 @@ class AddressLimitExceededException(BusinessLogicException):
|
||||
)
|
||||
|
||||
|
||||
class InvalidAddressTypeException(BusinessLogicException):
|
||||
class InvalidAddressTypeException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when an invalid address type is provided."""
|
||||
|
||||
def __init__(self, address_type: str):
|
||||
|
||||
@@ -11,6 +11,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.core.services.auth_service import AuthService
|
||||
@@ -123,7 +124,7 @@ class CustomerService:
|
||||
|
||||
return customer
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error registering customer: {str(e)}")
|
||||
raise CustomerValidationException(
|
||||
message="Failed to register customer", details={"error": str(e)}
|
||||
@@ -397,7 +398,7 @@ class CustomerService:
|
||||
|
||||
return customer
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error updating customer: {str(e)}")
|
||||
raise CustomerValidationException(
|
||||
message="Failed to update customer", details={"error": str(e)}
|
||||
|
||||
18
app/modules/customers/tests/unit/test_customer_service.py
Normal file
18
app/modules/customers/tests/unit/test_customer_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for CustomerService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.customers.services.customer_service import CustomerService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.customers
|
||||
class TestCustomerService:
|
||||
"""Test suite for CustomerService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = CustomerService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -28,7 +28,7 @@ from app.modules.monitoring.exceptions import (
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestRunNotFoundException(ResourceNotFoundException):
|
||||
class TestRunNotFoundException(ResourceNotFoundException): # noqa: MOD-025
|
||||
"""Raised when a test run is not found."""
|
||||
|
||||
def __init__(self, run_id: int):
|
||||
@@ -39,7 +39,7 @@ class TestRunNotFoundException(ResourceNotFoundException):
|
||||
)
|
||||
|
||||
|
||||
class TestExecutionException(ExternalServiceException):
|
||||
class TestExecutionException(ExternalServiceException): # noqa: MOD-025
|
||||
"""Raised when test execution fails."""
|
||||
|
||||
def __init__(self, reason: str):
|
||||
@@ -50,7 +50,7 @@ class TestExecutionException(ExternalServiceException):
|
||||
)
|
||||
|
||||
|
||||
class TestTimeoutException(ExternalServiceException):
|
||||
class TestTimeoutException(ExternalServiceException): # noqa: MOD-025
|
||||
"""Raised when test execution times out."""
|
||||
|
||||
def __init__(self, timeout_seconds: int = 3600):
|
||||
|
||||
@@ -186,7 +186,7 @@ class CodeQualityService:
|
||||
try:
|
||||
scan = self.run_scan(db, triggered_by, validator_type)
|
||||
results.append(scan)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to run {validator_type} scan: {e}")
|
||||
# Continue with other validators even if one fails
|
||||
return results
|
||||
@@ -802,7 +802,7 @@ class CodeQualityService:
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()[:40]
|
||||
except Exception:
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
0
app/modules/dev_tools/tests/__init__.py
Normal file
0
app/modules/dev_tools/tests/__init__.py
Normal file
0
app/modules/dev_tools/tests/unit/__init__.py
Normal file
0
app/modules/dev_tools/tests/unit/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for CodeQualityService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.dev_tools.services.code_quality_service import CodeQualityService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestCodeQualityService:
|
||||
"""Test suite for CodeQualityService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = CodeQualityService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
18
app/modules/dev_tools/tests/unit/test_test_runner_service.py
Normal file
18
app/modules/dev_tools/tests/unit/test_test_runner_service.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unit tests for TestRunnerService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.dev_tools.services.test_runner_service import TestRunnerService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.dev
|
||||
class TestTestRunnerService:
|
||||
"""Test suite for TestRunnerService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = TestRunnerService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -113,7 +113,7 @@ class InventoryValidationException(ValidationException):
|
||||
self.error_code = "INVENTORY_VALIDATION_FAILED"
|
||||
|
||||
|
||||
class NegativeInventoryException(BusinessLogicException):
|
||||
class NegativeInventoryException(BusinessLogicException): # noqa: MOD-025
|
||||
"""Raised when inventory quantity would become negative."""
|
||||
|
||||
def __init__(self, gtin: str, location: str, resulting_quantity: int):
|
||||
@@ -142,7 +142,7 @@ class InvalidQuantityException(ValidationException):
|
||||
self.error_code = "INVALID_QUANTITY"
|
||||
|
||||
|
||||
class LocationNotFoundException(ResourceNotFoundException):
|
||||
class LocationNotFoundException(ResourceNotFoundException): # noqa: MOD-025
|
||||
"""Raised when inventory location is not found."""
|
||||
|
||||
def __init__(self, location: str):
|
||||
|
||||
@@ -21,6 +21,7 @@ import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.catalog.models import Product
|
||||
@@ -198,7 +199,7 @@ class InventoryImportService:
|
||||
f"Import had {len(result.unmatched_gtins)} unmatched GTINs"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
except (SQLAlchemyError, ValueError) as e:
|
||||
logger.exception("Inventory import failed")
|
||||
result.success = False
|
||||
result.errors.append(str(e))
|
||||
@@ -229,7 +230,7 @@ class InventoryImportService:
|
||||
try:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
except OSError as e:
|
||||
return ImportResult(success=False, errors=[f"Failed to read file: {e}"])
|
||||
|
||||
# Detect delimiter
|
||||
|
||||
@@ -3,13 +3,14 @@ import logging
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import ValidationException
|
||||
from app.modules.catalog.exceptions import ProductNotFoundException
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.inventory.exceptions import (
|
||||
InsufficientInventoryException,
|
||||
InvalidInventoryOperationException,
|
||||
InvalidQuantityException,
|
||||
InventoryNotFoundException,
|
||||
InventoryValidationException,
|
||||
@@ -107,10 +108,12 @@ class InventoryService:
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error setting inventory: {str(e)}")
|
||||
raise ValidationException("Failed to set inventory")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to set inventory", operation="set_inventory"
|
||||
)
|
||||
|
||||
def adjust_inventory(
|
||||
self, db: Session, store_id: int, inventory_data: InventoryAdjust
|
||||
@@ -173,8 +176,10 @@ class InventoryService:
|
||||
# Validate resulting quantity
|
||||
if new_qty < 0:
|
||||
raise InsufficientInventoryException(
|
||||
f"Insufficient inventory. Available: {old_qty}, "
|
||||
f"Requested removal: {abs(inventory_data.quantity)}"
|
||||
gtin=getattr(product.marketplace_product, "gtin", str(inventory_data.product_id)),
|
||||
location=location,
|
||||
requested=abs(inventory_data.quantity),
|
||||
available=old_qty,
|
||||
)
|
||||
|
||||
existing.quantity = new_qty
|
||||
@@ -196,10 +201,12 @@ class InventoryService:
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error adjusting inventory: {str(e)}")
|
||||
raise ValidationException("Failed to adjust inventory")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to adjust inventory", operation="adjust_inventory"
|
||||
)
|
||||
|
||||
def reserve_inventory(
|
||||
self, db: Session, store_id: int, reserve_data: InventoryReserve
|
||||
@@ -234,8 +241,10 @@ class InventoryService:
|
||||
available = inventory.quantity - inventory.reserved_quantity
|
||||
if available < reserve_data.quantity:
|
||||
raise InsufficientInventoryException(
|
||||
f"Insufficient available inventory. Available: {available}, "
|
||||
f"Requested: {reserve_data.quantity}"
|
||||
gtin=getattr(inventory, "gtin", str(reserve_data.product_id)),
|
||||
location=location,
|
||||
requested=reserve_data.quantity,
|
||||
available=available,
|
||||
)
|
||||
|
||||
# Reserve inventory
|
||||
@@ -258,10 +267,12 @@ class InventoryService:
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error reserving inventory: {str(e)}")
|
||||
raise ValidationException("Failed to reserve inventory")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to reserve inventory", operation="reserve_inventory"
|
||||
)
|
||||
|
||||
def release_reservation(
|
||||
self, db: Session, store_id: int, reserve_data: InventoryReserve
|
||||
@@ -317,10 +328,12 @@ class InventoryService:
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error releasing reservation: {str(e)}")
|
||||
raise ValidationException("Failed to release reservation")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to release reservation", operation="release_reservation"
|
||||
)
|
||||
|
||||
def fulfill_reservation(
|
||||
self, db: Session, store_id: int, reserve_data: InventoryReserve
|
||||
@@ -351,8 +364,10 @@ class InventoryService:
|
||||
# Validate quantities
|
||||
if inventory.quantity < reserve_data.quantity:
|
||||
raise InsufficientInventoryException(
|
||||
f"Insufficient inventory. Available: {inventory.quantity}, "
|
||||
f"Requested: {reserve_data.quantity}"
|
||||
gtin=getattr(inventory, "gtin", str(reserve_data.product_id)),
|
||||
location=location,
|
||||
requested=reserve_data.quantity,
|
||||
available=inventory.quantity,
|
||||
)
|
||||
|
||||
if inventory.reserved_quantity < reserve_data.quantity:
|
||||
@@ -384,10 +399,12 @@ class InventoryService:
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error fulfilling reservation: {str(e)}")
|
||||
raise ValidationException("Failed to fulfill reservation")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to fulfill reservation", operation="fulfill_reservation"
|
||||
)
|
||||
|
||||
def get_product_inventory(
|
||||
self, db: Session, store_id: int, product_id: int
|
||||
@@ -449,9 +466,12 @@ class InventoryService:
|
||||
|
||||
except ProductNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error getting product inventory: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve product inventory")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to retrieve product inventory",
|
||||
operation="get_product_inventory",
|
||||
)
|
||||
|
||||
def get_store_inventory(
|
||||
self,
|
||||
@@ -487,9 +507,12 @@ class InventoryService:
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Error getting store inventory: {str(e)}")
|
||||
raise ValidationException("Failed to retrieve store inventory")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to retrieve store inventory",
|
||||
operation="get_store_inventory",
|
||||
)
|
||||
|
||||
def update_inventory(
|
||||
self,
|
||||
@@ -534,10 +557,12 @@ class InventoryService:
|
||||
):
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating inventory: {str(e)}")
|
||||
raise ValidationException("Failed to update inventory")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to update inventory", operation="update_inventory"
|
||||
)
|
||||
|
||||
def delete_inventory(self, db: Session, store_id: int, inventory_id: int) -> bool:
|
||||
"""Delete inventory entry."""
|
||||
@@ -556,10 +581,12 @@ class InventoryService:
|
||||
|
||||
except InventoryNotFoundException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except SQLAlchemyError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting inventory: {str(e)}")
|
||||
raise ValidationException("Failed to delete inventory")
|
||||
raise InvalidInventoryOperationException(
|
||||
"Failed to delete inventory", operation="delete_inventory"
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Admin Methods (cross-store operations)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Unit tests for InventoryImportService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.inventory.services.inventory_import_service import (
|
||||
InventoryImportService,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.inventory
|
||||
class TestInventoryImportService:
|
||||
"""Test suite for InventoryImportService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = InventoryImportService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Unit tests for InventoryTransactionService."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.inventory.services.inventory_transaction_service import (
|
||||
InventoryTransactionService,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.inventory
|
||||
class TestInventoryTransactionService:
|
||||
"""Test suite for InventoryTransactionService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = InventoryTransactionService()
|
||||
|
||||
def test_service_instantiation(self):
|
||||
"""Service can be instantiated."""
|
||||
assert self.service is not None
|
||||
@@ -336,7 +336,7 @@ class OrderReferenceRequiredException(LoyaltyException):
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class LoyaltyValidationException(ValidationException):
|
||||
class LoyaltyValidationException(ValidationException): # noqa: MOD-025
|
||||
"""Raised when loyalty data validation fails."""
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -167,7 +167,7 @@ class AppleWalletService:
|
||||
"""
|
||||
try:
|
||||
self.register_device(db, card, device_id, push_token)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to register device: {e}")
|
||||
raise DeviceRegistrationException(device_id, "register")
|
||||
|
||||
@@ -190,7 +190,7 @@ class AppleWalletService:
|
||||
"""
|
||||
try:
|
||||
self.unregister_device(db, card, device_id)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to unregister device: {e}")
|
||||
raise DeviceRegistrationException(device_id, "unregister")
|
||||
|
||||
@@ -251,7 +251,7 @@ class AppleWalletService:
|
||||
try:
|
||||
signature = self._sign_manifest(pass_files["manifest.json"])
|
||||
pass_files["signature"] = signature
|
||||
except Exception as e:
|
||||
except (OSError, ValueError) as e:
|
||||
logger.error(f"Failed to sign pass: {e}")
|
||||
raise WalletIntegrationException("apple", f"Failed to sign pass: {e}")
|
||||
|
||||
@@ -428,7 +428,7 @@ class AppleWalletService:
|
||||
return signature
|
||||
except FileNotFoundError as e:
|
||||
raise WalletIntegrationException("apple", f"Certificate file not found: {e}")
|
||||
except Exception as e:
|
||||
except (OSError, ValueError) as e:
|
||||
raise WalletIntegrationException("apple", f"Failed to sign manifest: {e}")
|
||||
|
||||
# =========================================================================
|
||||
@@ -521,7 +521,7 @@ class AppleWalletService:
|
||||
for registration in registrations:
|
||||
try:
|
||||
self._send_push(registration.push_token)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.warning(
|
||||
f"Failed to send push to device {registration.device_library_identifier[:8]}...: {e}"
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ class GoogleWalletService:
|
||||
scopes=scopes,
|
||||
)
|
||||
return self._credentials
|
||||
except Exception as e:
|
||||
except (ValueError, OSError) as e:
|
||||
logger.error(f"Failed to load Google credentials: {e}")
|
||||
raise WalletIntegrationException("google", str(e))
|
||||
|
||||
@@ -70,7 +70,7 @@ class GoogleWalletService:
|
||||
credentials = self._get_credentials()
|
||||
self._http_client = AuthorizedSession(credentials)
|
||||
return self._http_client
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to create Google HTTP client: {e}")
|
||||
raise WalletIntegrationException("google", str(e))
|
||||
|
||||
@@ -146,7 +146,7 @@ class GoogleWalletService:
|
||||
)
|
||||
except WalletIntegrationException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to create Google Wallet class: {e}")
|
||||
raise WalletIntegrationException("google", str(e))
|
||||
|
||||
@@ -177,7 +177,7 @@ class GoogleWalletService:
|
||||
f"Failed to update Google Wallet class {program.google_class_id}: "
|
||||
f"{response.status_code}"
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to update Google Wallet class: {e}")
|
||||
|
||||
# =========================================================================
|
||||
@@ -233,7 +233,7 @@ class GoogleWalletService:
|
||||
)
|
||||
except WalletIntegrationException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to create Google Wallet object: {e}")
|
||||
raise WalletIntegrationException("google", str(e))
|
||||
|
||||
@@ -258,7 +258,7 @@ class GoogleWalletService:
|
||||
f"Failed to update Google Wallet object {card.google_object_id}: "
|
||||
f"{response.status_code}"
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to update Google Wallet object: {e}")
|
||||
|
||||
def _build_object_data(self, card: LoyaltyCard, object_id: str) -> dict[str, Any]:
|
||||
@@ -356,7 +356,7 @@ class GoogleWalletService:
|
||||
db.commit()
|
||||
|
||||
return f"https://pay.google.com/gp/v/save/{token}"
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: EXC-003
|
||||
logger.error(f"Failed to generate Google Wallet save URL: {e}")
|
||||
raise WalletIntegrationException("google", str(e))
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user