docs: migrate module documentation to single source of truth
Move 39 documentation files from top-level docs/ into each module's docs/ folder, accessible via symlinks from docs/modules/. Create data-model.md files for 10 modules with full schema documentation. Replace originals with redirect stubs. Remove empty guide stubs. Modules migrated: tenancy, billing, loyalty, marketplace, orders, messaging, cms, catalog, inventory, hosting, prospecting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
docs/modules/analytics
Symbolic link
1
docs/modules/analytics
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/analytics/docs
|
||||
1
docs/modules/billing
Symbolic link
1
docs/modules/billing
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/billing/docs
|
||||
1
docs/modules/cart
Symbolic link
1
docs/modules/cart
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/cart/docs
|
||||
1
docs/modules/catalog
Symbolic link
1
docs/modules/catalog
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/catalog/docs
|
||||
1
docs/modules/checkout
Symbolic link
1
docs/modules/checkout
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/checkout/docs
|
||||
1
docs/modules/cms
Symbolic link
1
docs/modules/cms
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/cms/docs
|
||||
1
docs/modules/contracts
Symbolic link
1
docs/modules/contracts
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/contracts/docs
|
||||
1
docs/modules/core
Symbolic link
1
docs/modules/core
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/core/docs
|
||||
1
docs/modules/customers
Symbolic link
1
docs/modules/customers
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/customers/docs
|
||||
1
docs/modules/dev_tools
Symbolic link
1
docs/modules/dev_tools
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/dev_tools/docs
|
||||
1
docs/modules/hosting
Symbolic link
1
docs/modules/hosting
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/hosting/docs
|
||||
1
docs/modules/inventory
Symbolic link
1
docs/modules/inventory
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/inventory/docs
|
||||
1
docs/modules/loyalty
Symbolic link
1
docs/modules/loyalty
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/loyalty/docs
|
||||
@@ -1,321 +0,0 @@
|
||||
# Loyalty Module
|
||||
|
||||
The Loyalty Module provides stamp-based and points-based loyalty programs for Orion stores with Google Wallet and Apple Wallet integration.
|
||||
|
||||
## Overview
|
||||
|
||||
| Aspect | Description |
|
||||
|--------|-------------|
|
||||
| Module Code | `loyalty` |
|
||||
| Dependencies | `customers` |
|
||||
| Status | Phase 2 Complete |
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Stamp-based loyalty**: Collect N stamps, get a reward (e.g., "Buy 10 coffees, get 1 free")
|
||||
- **Points-based loyalty**: Earn points per euro spent, redeem for rewards
|
||||
- **Hybrid programs**: Support both stamps and points simultaneously
|
||||
- **Anti-fraud system**: Staff PINs, cooldown periods, daily limits, lockout protection
|
||||
- **Wallet integration**: Google Wallet and Apple Wallet pass generation
|
||||
- **Full audit trail**: Transaction logging with IP, user agent, and staff attribution
|
||||
|
||||
## Entity Model
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Merchant │───────│ LoyaltyProgram │
|
||||
└─────────────────┘ 1:1 └─────────────────┘
|
||||
│ │
|
||||
▼ ┌────────┼──────────┐
|
||||
┌──────────────────┐ │ │ │
|
||||
│ MerchantLoyalty │ ▼ ▼ ▼
|
||||
│ Settings │┌──────────┐┌──────────┐┌────────┐
|
||||
└──────────────────┘│ StaffPin ││LoyaltyCard││(config)│
|
||||
└──────────┘└──────────┘└────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
└──▶│ Transaction │
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│AppleDeviceRegistration│
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Database Tables
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `loyalty_programs` | Merchant's program configuration (type, targets, branding) |
|
||||
| `loyalty_cards` | Customer cards with stamp/point balances (merchant-scoped) |
|
||||
| `loyalty_transactions` | Immutable audit log of all operations |
|
||||
| `staff_pins` | Hashed PINs for fraud prevention |
|
||||
| `apple_device_registrations` | Apple Wallet push notification tokens |
|
||||
| `merchant_loyalty_settings` | Admin-controlled per-merchant settings |
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables (prefix: `LOYALTY_`):
|
||||
|
||||
```bash
|
||||
# Anti-fraud defaults
|
||||
LOYALTY_DEFAULT_COOLDOWN_MINUTES=15
|
||||
LOYALTY_MAX_DAILY_STAMPS=5
|
||||
LOYALTY_PIN_MAX_FAILED_ATTEMPTS=5
|
||||
LOYALTY_PIN_LOCKOUT_MINUTES=30
|
||||
|
||||
# Points
|
||||
LOYALTY_DEFAULT_POINTS_PER_EURO=10
|
||||
|
||||
# Google Wallet
|
||||
LOYALTY_GOOGLE_ISSUER_ID=3388000000012345678
|
||||
LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON=/path/to/service-account.json
|
||||
|
||||
# Apple Wallet
|
||||
LOYALTY_APPLE_PASS_TYPE_ID=pass.com.example.loyalty
|
||||
LOYALTY_APPLE_TEAM_ID=ABCD1234
|
||||
LOYALTY_APPLE_WWDR_CERT_PATH=/path/to/wwdr.pem
|
||||
LOYALTY_APPLE_SIGNER_CERT_PATH=/path/to/signer.pem
|
||||
LOYALTY_APPLE_SIGNER_KEY_PATH=/path/to/signer.key
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Store Endpoints (`/api/v1/store/loyalty/`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/program` | Get store's loyalty program |
|
||||
| `POST` | `/program` | Create loyalty program |
|
||||
| `PATCH` | `/program` | Update loyalty program |
|
||||
| `GET` | `/stats` | Get program statistics |
|
||||
| `GET` | `/cards` | List customer cards |
|
||||
| `POST` | `/cards/enroll` | Enroll customer in program |
|
||||
| `POST` | `/cards/lookup` | Look up card by QR/number |
|
||||
| `POST` | `/stamp` | Add stamp to card |
|
||||
| `POST` | `/stamp/redeem` | Redeem stamps for reward |
|
||||
| `POST` | `/points` | Earn points from purchase |
|
||||
| `POST` | `/points/redeem` | Redeem points for reward |
|
||||
| `GET` | `/pins` | List staff PINs |
|
||||
| `POST` | `/pins` | Create staff PIN |
|
||||
| `PATCH` | `/pins/{id}` | Update staff PIN |
|
||||
| `DELETE` | `/pins/{id}` | Delete staff PIN |
|
||||
| `POST` | `/pins/{id}/unlock` | Unlock locked PIN |
|
||||
|
||||
### Admin Endpoints (`/api/v1/admin/loyalty/`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/programs` | List all loyalty programs |
|
||||
| `GET` | `/programs/{id}` | Get specific program |
|
||||
| `GET` | `/programs/{id}/stats` | Get program statistics |
|
||||
| `GET` | `/stats` | Platform-wide statistics |
|
||||
|
||||
### Storefront Endpoints (`/api/v1/storefront/loyalty/`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/card` | Get customer's loyalty card and balance |
|
||||
| `GET` | `/transactions` | Get customer's transaction history |
|
||||
| `POST` | `/enroll` | Self-enrollment in loyalty program |
|
||||
|
||||
### Public Endpoints (`/api/v1/loyalty/`)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/programs/{store_code}` | Get program info for enrollment |
|
||||
| `GET` | `/passes/apple/{serial}.pkpass` | Download Apple Wallet pass |
|
||||
| `POST` | `/apple/v1/devices/...` | Apple Web Service: register device |
|
||||
| `DELETE` | `/apple/v1/devices/...` | Apple Web Service: unregister |
|
||||
| `GET` | `/apple/v1/devices/...` | Apple Web Service: get updates |
|
||||
| `GET` | `/apple/v1/passes/...` | Apple Web Service: get pass |
|
||||
|
||||
## Anti-Fraud System
|
||||
|
||||
### Staff PIN Verification
|
||||
|
||||
All stamp and points operations require staff PIN verification (configurable per program).
|
||||
|
||||
```python
|
||||
# PIN is hashed with bcrypt
|
||||
pin.set_pin("1234") # Stores bcrypt hash
|
||||
pin.verify_pin("1234") # Returns True/False
|
||||
```
|
||||
|
||||
### Lockout Protection
|
||||
|
||||
- **Max failed attempts**: 5 (configurable)
|
||||
- **Lockout duration**: 30 minutes (configurable)
|
||||
- After lockout expires, PIN can be used again
|
||||
- Admin can manually unlock via API
|
||||
|
||||
### Cooldown Period
|
||||
|
||||
Prevents rapid stamp collection (fraud prevention):
|
||||
|
||||
```
|
||||
Customer scans card → Gets stamp → Must wait 15 minutes → Can get next stamp
|
||||
```
|
||||
|
||||
### Daily Limits
|
||||
|
||||
Maximum stamps per card per day (default: 5).
|
||||
|
||||
## Wallet Integration
|
||||
|
||||
### Google Wallet
|
||||
|
||||
Architecture: **Server-side storage with automatic API updates**
|
||||
|
||||
All wallet operations are triggered automatically — no manual API calls needed:
|
||||
|
||||
| Event | Wallet Action | Trigger |
|
||||
|-------|---------------|---------|
|
||||
| Customer enrolls | Create `LoyaltyClass` (first time) + `LoyaltyObject` | `card_service.enroll_customer()` → `wallet_service.create_wallet_objects()` |
|
||||
| Stamp/points change | `PATCH` the object with new balance | `stamp_service`/`points_service` → `wallet_service.sync_card_to_wallets()` |
|
||||
| Customer views dashboard | Generate JWT "Add to Wallet" URL (1h expiry) | `GET /storefront/loyalty/card` → `wallet_service.get_add_to_wallet_urls()` |
|
||||
|
||||
**Setup:** Configure `LOYALTY_GOOGLE_ISSUER_ID` and `LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON` env vars. This is a platform-wide setting — all merchants automatically get Google Wallet support. See [Google Wallet Setup](../deployment/hetzner-server-setup.md#step-25-google-wallet-integration) for full instructions.
|
||||
|
||||
No device registration needed — Google syncs automatically.
|
||||
|
||||
### Apple Wallet
|
||||
|
||||
Architecture: **Push notification model**
|
||||
|
||||
1. Customer adds pass → Device registers with our server
|
||||
2. Stamp/points change → Send push notification to APNs
|
||||
3. Device receives push → Fetches updated pass from our server
|
||||
|
||||
Requires `apple_device_registrations` table for push tokens.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create a Loyalty Program
|
||||
|
||||
```python
|
||||
from app.modules.loyalty.services import program_service
|
||||
from app.modules.loyalty.schemas import ProgramCreate
|
||||
|
||||
data = ProgramCreate(
|
||||
loyalty_type="stamps",
|
||||
stamps_target=10,
|
||||
stamps_reward_description="Free coffee",
|
||||
cooldown_minutes=15,
|
||||
max_daily_stamps=5,
|
||||
require_staff_pin=True,
|
||||
card_color="#4F46E5",
|
||||
)
|
||||
|
||||
program = program_service.create_program(db, store_id=1, data=data)
|
||||
```
|
||||
|
||||
### Enroll a Customer
|
||||
|
||||
```python
|
||||
from app.modules.loyalty.services import card_service
|
||||
|
||||
card = card_service.enroll_customer(db, customer_id=123, store_id=1)
|
||||
# Returns LoyaltyCard with unique card_number and qr_code_data
|
||||
```
|
||||
|
||||
### Add a Stamp
|
||||
|
||||
```python
|
||||
from app.modules.loyalty.services import stamp_service
|
||||
|
||||
result = stamp_service.add_stamp(
|
||||
db,
|
||||
qr_code="abc123xyz",
|
||||
staff_pin="1234",
|
||||
ip_address="192.168.1.1",
|
||||
)
|
||||
# Returns dict with stamp_count, reward_earned, next_stamp_available_at, etc.
|
||||
```
|
||||
|
||||
### Earn Points from Purchase
|
||||
|
||||
```python
|
||||
from app.modules.loyalty.services import points_service
|
||||
|
||||
result = points_service.earn_points(
|
||||
db,
|
||||
card_number="123456789012",
|
||||
purchase_amount_cents=2500, # €25.00
|
||||
order_reference="ORD-12345",
|
||||
staff_pin="1234",
|
||||
)
|
||||
# Returns dict with points_earned (250 at 10pts/€), points_balance, etc.
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Purpose |
|
||||
|---------|---------|
|
||||
| `program_service` | Program CRUD and statistics |
|
||||
| `card_service` | Card enrollment, lookup, management |
|
||||
| `stamp_service` | Stamp operations with anti-fraud |
|
||||
| `points_service` | Points operations and redemption |
|
||||
| `pin_service` | Staff PIN CRUD and verification |
|
||||
| `wallet_service` | Unified wallet abstraction |
|
||||
| `google_wallet_service` | Google Wallet API integration |
|
||||
| `apple_wallet_service` | Apple Wallet pass generation |
|
||||
|
||||
## Scheduled Tasks
|
||||
|
||||
| Task | Schedule | Description |
|
||||
|------|----------|-------------|
|
||||
| `loyalty.sync_wallet_passes` | Hourly | Sync cards that missed real-time updates |
|
||||
| `loyalty.expire_points` | Daily 02:00 | Expire points for inactive cards (based on `points_expiration_days`) |
|
||||
|
||||
## UI Pages
|
||||
|
||||
### Admin Pages
|
||||
|
||||
| Page | Path | Description |
|
||||
|------|------|-------------|
|
||||
| Programs Dashboard | `/admin/loyalty/programs` | List all loyalty programs with stats |
|
||||
| Merchant Detail | `/admin/loyalty/merchants/{id}` | Detailed view of a merchant's program |
|
||||
| Merchant Settings | `/admin/loyalty/merchants/{id}/settings` | Admin-controlled merchant settings |
|
||||
| Analytics | `/admin/loyalty/analytics` | Platform-wide analytics |
|
||||
|
||||
### Store Pages
|
||||
|
||||
| Page | Path | Description |
|
||||
|------|------|-------------|
|
||||
| Terminal | `/store/loyalty/terminal` | Scan card, add stamps/points, redeem |
|
||||
| Cards List | `/store/loyalty/cards` | Browse customer cards |
|
||||
| Card Detail | `/store/loyalty/cards/{id}` | Individual card detail |
|
||||
| Enroll | `/store/loyalty/enroll` | Enroll new customer |
|
||||
| Settings | `/store/loyalty/settings` | Program settings |
|
||||
| Stats | `/store/loyalty/stats` | Store-level statistics |
|
||||
|
||||
### Storefront Pages
|
||||
|
||||
| Page | Path | Description |
|
||||
|------|------|-------------|
|
||||
| Dashboard | `/loyalty/dashboard` | Customer's card and balance |
|
||||
| History | `/loyalty/history` | Transaction history |
|
||||
| Enroll | `/loyalty/enroll` | Self-enrollment page |
|
||||
|
||||
## Localization
|
||||
|
||||
Available in 5 languages:
|
||||
- English (`en.json`)
|
||||
- French (`fr.json`)
|
||||
- German (`de.json`)
|
||||
- Luxembourgish (`lu.json`, `lb.json`)
|
||||
|
||||
## Future Enhancements (Phase 3+)
|
||||
|
||||
- Rewards catalog with configurable tiers
|
||||
- Customer tiers (Bronze/Silver/Gold)
|
||||
- Promotions engine (bonus points, discounts, free items)
|
||||
- Referral program
|
||||
- Gamification (spin wheel, scratch cards)
|
||||
- POS integration
|
||||
- Batch import of existing loyalty cards
|
||||
- Real-time WebSocket updates
|
||||
- Receipt printing
|
||||
1
docs/modules/marketplace
Symbolic link
1
docs/modules/marketplace
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/marketplace/docs
|
||||
1
docs/modules/messaging
Symbolic link
1
docs/modules/messaging
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/messaging/docs
|
||||
1
docs/modules/monitoring
Symbolic link
1
docs/modules/monitoring
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/monitoring/docs
|
||||
1
docs/modules/orders
Symbolic link
1
docs/modules/orders
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/orders/docs
|
||||
1
docs/modules/payments
Symbolic link
1
docs/modules/payments
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/payments/docs
|
||||
1
docs/modules/prospecting
Symbolic link
1
docs/modules/prospecting
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/prospecting/docs
|
||||
@@ -1,171 +0,0 @@
|
||||
# Database Schema
|
||||
|
||||
## Entity Relationship Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌────────────────────────┐
|
||||
│ prospects │────<│ prospect_tech_profiles │
|
||||
├─────────────────────┤ ├────────────────────────┤
|
||||
│ id │ │ id │
|
||||
│ channel │ │ prospect_id (FK) │
|
||||
│ business_name │ │ cms, server │
|
||||
│ domain_name │ │ hosting_provider │
|
||||
│ status │ │ js_framework, cdn │
|
||||
│ source │ │ analytics │
|
||||
│ has_website │ │ ecommerce_platform │
|
||||
│ uses_https │ │ tech_stack_json (JSON) │
|
||||
│ ... │ └────────────────────────┘
|
||||
└─────────────────────┘
|
||||
│
|
||||
│ ┌──────────────────────────────┐
|
||||
└──────────────<│ prospect_performance_profiles │
|
||||
│ ├──────────────────────────────┤
|
||||
│ │ id │
|
||||
│ │ prospect_id (FK) │
|
||||
│ │ performance_score (0-100) │
|
||||
│ │ accessibility_score │
|
||||
│ │ seo_score │
|
||||
│ │ FCP, LCP, TBT, CLS │
|
||||
│ │ is_mobile_friendly │
|
||||
│ └──────────────────────────────┘
|
||||
│
|
||||
│ ┌───────────────────────┐
|
||||
└──────────────<│ prospect_scores │
|
||||
│ ├───────────────────────┤
|
||||
│ │ id │
|
||||
│ │ prospect_id (FK) │
|
||||
│ │ score (0-100) │
|
||||
│ │ technical_health_score│
|
||||
│ │ modernity_score │
|
||||
│ │ business_value_score │
|
||||
│ │ engagement_score │
|
||||
│ │ reason_flags (JSON) │
|
||||
│ │ lead_tier │
|
||||
│ └───────────────────────┘
|
||||
│
|
||||
│ ┌───────────────────────┐
|
||||
└──────────────<│ prospect_contacts │
|
||||
│ ├───────────────────────┤
|
||||
│ │ id │
|
||||
│ │ prospect_id (FK) │
|
||||
│ │ contact_type │
|
||||
│ │ value │
|
||||
│ │ source_url │
|
||||
│ │ is_primary │
|
||||
│ └───────────────────────┘
|
||||
│
|
||||
│ ┌───────────────────────┐
|
||||
└──────────────<│ prospect_interactions │
|
||||
│ ├───────────────────────┤
|
||||
│ │ id │
|
||||
│ │ prospect_id (FK) │
|
||||
│ │ interaction_type │
|
||||
│ │ subject, notes │
|
||||
│ │ outcome │
|
||||
│ │ next_action │
|
||||
│ │ next_action_date │
|
||||
│ │ created_by_user_id │
|
||||
│ └───────────────────────┘
|
||||
│
|
||||
│ ┌───────────────────────┐
|
||||
└──────────────<│ prospect_scan_jobs │
|
||||
├───────────────────────┤
|
||||
│ id │
|
||||
│ job_type │
|
||||
│ status │
|
||||
│ total_items │
|
||||
│ processed_items │
|
||||
│ celery_task_id │
|
||||
└───────────────────────┘
|
||||
|
||||
┌──────────────────────┐ ┌──────────────────┐
|
||||
│ campaign_templates │────<│ campaign_sends │
|
||||
├──────────────────────┤ ├──────────────────┤
|
||||
│ id │ │ id │
|
||||
│ name │ │ template_id (FK) │
|
||||
│ lead_type │ │ prospect_id (FK) │
|
||||
│ channel │ │ channel │
|
||||
│ language │ │ rendered_subject │
|
||||
│ subject_template │ │ rendered_body │
|
||||
│ body_template │ │ status │
|
||||
│ is_active │ │ sent_at │
|
||||
└──────────────────────┘ │ sent_by_user_id │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
### prospects
|
||||
|
||||
Central table for all leads — both digital (domain-based) and offline (in-person).
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| id | INTEGER PK | Auto-increment |
|
||||
| channel | ENUM(digital, offline) | How the lead was discovered |
|
||||
| business_name | VARCHAR(255) | Required for offline |
|
||||
| domain_name | VARCHAR(255) | Required for digital, unique |
|
||||
| status | ENUM | pending, active, inactive, parked, error, contacted, converted |
|
||||
| source | VARCHAR(100) | e.g. "domain_scan", "networking_event", "street" |
|
||||
| has_website | BOOLEAN | Determined by HTTP check |
|
||||
| uses_https | BOOLEAN | SSL status |
|
||||
| http_status_code | INTEGER | Last HTTP response |
|
||||
| address | VARCHAR(500) | Physical address (offline) |
|
||||
| city | VARCHAR(100) | City |
|
||||
| postal_code | VARCHAR(10) | Postal code |
|
||||
| country | VARCHAR(2) | Default "LU" |
|
||||
| notes | TEXT | Free-form notes |
|
||||
| tags | JSON | Flexible tagging |
|
||||
| captured_by_user_id | INTEGER FK | Who captured this lead |
|
||||
| location_lat / location_lng | FLOAT | GPS from mobile capture |
|
||||
| last_*_at | DATETIME | Timestamps for each scan type |
|
||||
|
||||
### prospect_tech_profiles
|
||||
|
||||
Technology stack detection results. One per prospect.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| cms | VARCHAR(100) | WordPress, Drupal, Joomla, etc. |
|
||||
| server | VARCHAR(100) | Nginx, Apache |
|
||||
| hosting_provider | VARCHAR(100) | Hosting company |
|
||||
| cdn | VARCHAR(100) | CDN provider |
|
||||
| js_framework | VARCHAR(100) | React, Vue, Angular, jQuery |
|
||||
| analytics | VARCHAR(200) | Google Analytics, Matomo, etc. |
|
||||
| ecommerce_platform | VARCHAR(100) | Shopify, WooCommerce, etc. |
|
||||
| tech_stack_json | JSON | Full detection results |
|
||||
|
||||
### prospect_performance_profiles
|
||||
|
||||
Lighthouse audit results. One per prospect.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| performance_score | INTEGER | 0-100 |
|
||||
| accessibility_score | INTEGER | 0-100 |
|
||||
| seo_score | INTEGER | 0-100 |
|
||||
| first_contentful_paint_ms | INTEGER | FCP |
|
||||
| largest_contentful_paint_ms | INTEGER | LCP |
|
||||
| total_blocking_time_ms | INTEGER | TBT |
|
||||
| cumulative_layout_shift | FLOAT | CLS |
|
||||
| is_mobile_friendly | BOOLEAN | Mobile test |
|
||||
|
||||
### prospect_scores
|
||||
|
||||
Calculated opportunity scores. One per prospect. See [scoring.md](scoring.md) for algorithm details.
|
||||
|
||||
### prospect_contacts
|
||||
|
||||
Scraped or manually entered contact info. Many per prospect.
|
||||
|
||||
### prospect_interactions
|
||||
|
||||
CRM-style interaction log. Many per prospect. Types: note, call, email_sent, email_received, meeting, visit, sms, proposal_sent.
|
||||
|
||||
### prospect_scan_jobs
|
||||
|
||||
Background job tracking for batch operations.
|
||||
|
||||
### campaign_templates / campaign_sends
|
||||
|
||||
Marketing campaign templates and send tracking. Templates support placeholders like `{business_name}`, `{domain}`, `{score}`, `{issues}`.
|
||||
@@ -1,80 +0,0 @@
|
||||
# .lu Domain Lead Generation — Research Findings
|
||||
|
||||
Research on data sources, APIs, legal requirements, and cost analysis for the prospecting module.
|
||||
|
||||
## 1. Data Sources for .lu Domains
|
||||
|
||||
The official .lu registry (DNS-LU / RESTENA) does **not** publish zone files. All providers use web crawling to discover domains, so no list is 100% complete. Expect 70-80% coverage.
|
||||
|
||||
### Providers
|
||||
|
||||
| Provider | Domains | Price | Format | Notes |
|
||||
|----------|---------|-------|--------|-------|
|
||||
| NetworksDB | ~70,000 | $5 | Zipped text | Best value, one-time purchase |
|
||||
| DomainMetaData | Varies | $9.90/mo | CSV | Daily updates |
|
||||
| Webatla | ~75,000 | Unknown | CSV | Good coverage |
|
||||
|
||||
## 2. Technical APIs — Cost Analysis
|
||||
|
||||
### Technology Detection
|
||||
|
||||
| Service | Free Tier | Notes |
|
||||
|---------|-----------|-------|
|
||||
| CRFT Lookup | Unlimited | Budget option, includes Lighthouse |
|
||||
| Wappalyzer | 50/month | Most accurate |
|
||||
| WhatCMS | Free lookups | CMS-only |
|
||||
|
||||
**Approach used**: Custom HTML parsing for CMS, JS framework, analytics, and server detection (no external API dependency).
|
||||
|
||||
### Performance Audits
|
||||
|
||||
PageSpeed Insights API — **free**, 25,000 queries/day, 400/100 seconds.
|
||||
|
||||
### SSL Checks
|
||||
|
||||
Simple HTTPS connectivity check (fast). SSL Labs API available for deep analysis of high-priority leads.
|
||||
|
||||
### WHOIS
|
||||
|
||||
Due to GDPR, .lu WHOIS data for private individuals is hidden. Only owner type and country visible. Contact info scraped from websites instead.
|
||||
|
||||
## 3. Legal — Luxembourg & GDPR
|
||||
|
||||
### B2B Cold Email Rules
|
||||
|
||||
Luxembourg has **no specific B2B cold email restrictions** per Article 11(1) of the Electronic Privacy Act (applies only to natural persons).
|
||||
|
||||
**Requirements**:
|
||||
1. Identify yourself clearly (company name, address)
|
||||
2. Provide opt-out mechanism in every email
|
||||
3. Message must relate to recipient's business
|
||||
4. Store contact data securely
|
||||
5. Only contact businesses, not private individuals
|
||||
|
||||
**Legal basis**: Legitimate interest (GDPR Art. 6(1)(f))
|
||||
|
||||
### GDPR Penalties
|
||||
|
||||
Fines up to EUR 20 million or 4% of global revenue for violations.
|
||||
|
||||
**Key violations to avoid**:
|
||||
- Emailing private individuals without consent
|
||||
- No opt-out mechanism
|
||||
- Holding personal data longer than necessary
|
||||
|
||||
### Recommendation
|
||||
|
||||
- Focus on `info@`, `contact@`, and business role emails
|
||||
- Always include unsubscribe link
|
||||
- Document legitimate interest basis
|
||||
|
||||
## 4. Cost Summary
|
||||
|
||||
| Item | Cost | Type |
|
||||
|------|------|------|
|
||||
| Domain list (NetworksDB) | $5 | One-time |
|
||||
| PageSpeed API | Free | Ongoing |
|
||||
| Contact scraping | Free | Self-hosted |
|
||||
| Tech detection | Free | Self-hosted |
|
||||
|
||||
Working MVP costs under $25 total.
|
||||
@@ -1,110 +0,0 @@
|
||||
# Opportunity Scoring Model
|
||||
|
||||
## Overview
|
||||
|
||||
The scoring model assigns each prospect a score from 0-100 based on the opportunity potential for offering web services. Higher scores indicate better leads. The model supports two channels: **digital** (domain-based) and **offline** (in-person discovery).
|
||||
|
||||
## Score Components — Digital Channel
|
||||
|
||||
### Technical Health (Max 40 points)
|
||||
|
||||
Issues that indicate immediate opportunities:
|
||||
|
||||
| Issue | Points | Condition |
|
||||
|-------|--------|-----------|
|
||||
| No SSL | 15 | `uses_https = false` |
|
||||
| Very Slow | 15 | `performance_score < 30` |
|
||||
| Slow | 10 | `performance_score < 50` |
|
||||
| Moderate Speed | 5 | `performance_score < 70` |
|
||||
| Not Mobile Friendly | 10 | `is_mobile_friendly = false` |
|
||||
|
||||
### Modernity / Stack (Max 25 points)
|
||||
|
||||
Outdated technology stack:
|
||||
|
||||
| Issue | Points | Condition |
|
||||
|-------|--------|-----------|
|
||||
| Outdated CMS | 15 | CMS is Drupal, Joomla, or TYPO3 |
|
||||
| Unknown CMS | 5 | No CMS detected but has website |
|
||||
| Legacy JavaScript | 5 | Uses jQuery without modern framework |
|
||||
| No Analytics | 5 | No Google Analytics or similar |
|
||||
|
||||
### Business Value (Max 25 points)
|
||||
|
||||
Indicators of business potential:
|
||||
|
||||
| Factor | Points | Condition |
|
||||
|--------|--------|-----------|
|
||||
| Has Website | 10 | Active website exists |
|
||||
| Has E-commerce | 10 | E-commerce platform detected |
|
||||
| Short Domain | 5 | Domain name <= 15 characters |
|
||||
|
||||
### Engagement Potential (Max 10 points)
|
||||
|
||||
Ability to contact the business:
|
||||
|
||||
| Factor | Points | Condition |
|
||||
|--------|--------|-----------|
|
||||
| Has Contacts | 5 | Any contact info found |
|
||||
| Has Email | 3 | Email address found |
|
||||
| Has Phone | 2 | Phone number found |
|
||||
|
||||
## Score Components — Offline Channel
|
||||
|
||||
Offline leads have a simplified scoring model based on the information captured during in-person encounters:
|
||||
|
||||
| Scenario | Technical Health | Modernity | Business Value | Engagement | Total |
|
||||
|----------|-----------------|-----------|----------------|------------|-------|
|
||||
| No website at all | 30 | 20 | 20 | 0 | **70** (top_priority) |
|
||||
| Uses gmail/free email | +0 | +10 | +0 | +0 | +10 |
|
||||
| Met in person | +0 | +0 | +0 | +5 | +5 |
|
||||
| Has email contact | +0 | +0 | +0 | +3 | +3 |
|
||||
| Has phone contact | +0 | +0 | +0 | +2 | +2 |
|
||||
|
||||
A business with no website met in person with contact info scores: 70 + 5 + 3 + 2 = **80** (top_priority).
|
||||
|
||||
## Lead Tiers
|
||||
|
||||
Based on the total score:
|
||||
|
||||
| Tier | Score Range | Description |
|
||||
|------|-------------|-------------|
|
||||
| `top_priority` | 70-100 | Best leads, multiple issues or no website at all |
|
||||
| `quick_win` | 50-69 | Good leads, 1-2 easy fixes |
|
||||
| `strategic` | 30-49 | Moderate potential |
|
||||
| `low_priority` | 0-29 | Low opportunity |
|
||||
|
||||
## Reason Flags
|
||||
|
||||
Each score includes `reason_flags` that explain why points were awarded:
|
||||
|
||||
```json
|
||||
{
|
||||
"score": 78,
|
||||
"reason_flags": ["no_ssl", "slow", "outdated_cms"],
|
||||
"lead_tier": "top_priority"
|
||||
}
|
||||
```
|
||||
|
||||
Common flags (digital):
|
||||
- `no_ssl` — Missing HTTPS
|
||||
- `very_slow` — Performance score < 30
|
||||
- `slow` — Performance score < 50
|
||||
- `not_mobile_friendly` — Fails mobile tests
|
||||
- `outdated_cms` — Using old CMS
|
||||
- `legacy_js` — Using jQuery only
|
||||
- `no_analytics` — No tracking installed
|
||||
|
||||
Offline-specific flags:
|
||||
- `no_website` — Business has no website
|
||||
- `uses_gmail` — Uses free email provider
|
||||
- `met_in_person` — Lead captured in person (warm lead)
|
||||
|
||||
## Customizing the Model
|
||||
|
||||
The scoring logic is in `app/modules/prospecting/services/scoring_service.py`. You can adjust:
|
||||
|
||||
1. **Point values** — Change weights for different issues
|
||||
2. **Thresholds** — Adjust performance score cutoffs
|
||||
3. **Conditions** — Add new scoring criteria
|
||||
4. **Tier boundaries** — Change score ranges for tiers
|
||||
1
docs/modules/tenancy
Symbolic link
1
docs/modules/tenancy
Symbolic link
@@ -0,0 +1 @@
|
||||
../../app/modules/tenancy/docs
|
||||
Reference in New Issue
Block a user