fix(db): Добавлены новые таблицы для хранения информации о новых и старых ценах при использовании промокодов на скидку
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Telegram bot for selling and managing Remnawave VPN subscriptions. Built with Aiogram 3.x (async), SQLAlchemy 2.x (async ORM), and PostgreSQL. Supports multiple payment providers (YooKassa, CryptoPay, FreeKassa, Platega, SeverPay, Telegram Stars) and integrates with the Remnawave panel API.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Running the Bot
|
||||
|
||||
```bash
|
||||
# Using Docker Compose (recommended)
|
||||
docker compose up -d
|
||||
|
||||
# View logs
|
||||
docker compose logs -f remnawave-tg-shop
|
||||
|
||||
# Stop
|
||||
docker compose down
|
||||
|
||||
# Local development (requires PostgreSQL running)
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
```bash
|
||||
# Copy example environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env with your configuration
|
||||
# Required: BOT_TOKEN, ADMIN_IDS, WEBHOOK_BASE_URL, POSTGRES_*, PANEL_API_URL, PANEL_API_KEY
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
|
||||
The bot auto-creates tables on startup via `init_db()` in `db/database_setup.py`. No manual migrations needed for initial setup.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
```
|
||||
Handlers (routing, input validation)
|
||||
↓
|
||||
Services (business logic, orchestration)
|
||||
↓
|
||||
DAL (data access layer - pure async functions)
|
||||
↓
|
||||
Models (SQLAlchemy ORM)
|
||||
↓
|
||||
PostgreSQL
|
||||
```
|
||||
|
||||
### Core Components
|
||||
|
||||
**Entry Point**: `main.py` → `bot/main_bot.py::run_bot()`
|
||||
|
||||
**Initialization Flow**:
|
||||
1. Load `.env` via `config/settings.py` (Pydantic)
|
||||
2. Initialize database connection (`db/database_setup.py`)
|
||||
3. Create dispatcher with middlewares (`bot/app/controllers/dispatcher_controller.py`)
|
||||
4. Build all services via factory (`bot/app/factories/build_services.py`)
|
||||
5. Register routers (`bot/routers.py`)
|
||||
6. Start AIOHTTP web server for webhooks (`bot/app/web/web_server.py`)
|
||||
|
||||
### Middleware Pipeline (Execution Order)
|
||||
|
||||
All middlewares are outer middlewares applied at dispatcher level:
|
||||
|
||||
1. **DBSessionMiddleware** - Provides `session` to handlers, auto-commits/rollbacks
|
||||
2. **I18nMiddleware** - Sets `current_language` and `i18n_instance` in handler data
|
||||
3. **ProfileSyncMiddleware** - Syncs Telegram user profile to local DB
|
||||
4. **BanCheckMiddleware** - Blocks banned users
|
||||
5. **ChannelSubscriptionMiddleware** - Enforces required channel subscription (if configured)
|
||||
6. **ActionLoggerMiddleware** - Logs all user actions to `message_logs` table
|
||||
|
||||
### Router Hierarchy
|
||||
|
||||
```
|
||||
root_router (Private chat filter)
|
||||
├── user_router_aggregate
|
||||
│ ├── start_router (CommandStart)
|
||||
│ ├── payment_router (payment flow callbacks)
|
||||
│ ├── subscription_router (my_subscription callbacks)
|
||||
│ ├── trial_router
|
||||
│ ├── referral_router
|
||||
│ ├── promo_user_router
|
||||
│ └── payment method routers (yookassa, stars, crypto, etc.)
|
||||
├── inline_mode.router
|
||||
└── admin_main_filtered_router (AdminFilter)
|
||||
├── admin_router_aggregate
|
||||
├── admin_common_router
|
||||
├── admin_payments_router
|
||||
├── admin_promo_routers
|
||||
├── admin_stats_router
|
||||
├── admin_logs_router
|
||||
├── admin_broadcast_router
|
||||
├── admin_ads_router
|
||||
├── admin_sync_router
|
||||
└── admin_user_management_router
|
||||
```
|
||||
|
||||
## Key Subsystems
|
||||
|
||||
### 1. Service Layer (`bot/services/`)
|
||||
|
||||
All services are created once at startup in `build_core_services()` and injected into dispatcher. Handlers access them via `data["service_name"]`.
|
||||
|
||||
**Core Services**:
|
||||
- **PanelApiService** - REST client for Remnawave panel API (user CRUD, device management, stats)
|
||||
- **SubscriptionService** - Subscription lifecycle management (trial, paid, renewals, traffic packages)
|
||||
- **YooKassaService** - Primary payment provider with auto-renewal support
|
||||
- **StarsService**, **CryptoPayService**, **FreeKassaService**, **PlategaService**, **SeverPayService** - Alternative payment providers
|
||||
- **PanelWebhookService** - Handles panel subscription events (expiry notifications, auto-renew triggers)
|
||||
- **ReferralService** - Referral tracking and bonus distribution
|
||||
- **PromoCodeService** - Promo code validation and activation
|
||||
- **LknpdService** - Tax receipt generation (nalog.ru integration)
|
||||
|
||||
**Service Wiring Pattern**:
|
||||
```python
|
||||
# Services are cross-injected after creation
|
||||
subscription_service.yookassa_service = yookassa_service
|
||||
panel_webhook_service.subscription_service = subscription_service
|
||||
```
|
||||
|
||||
### 2. Data Access Layer (`db/dal/`)
|
||||
|
||||
Pure async functions (no classes). Each module provides CRUD operations for specific entities.
|
||||
|
||||
**Key DAL Modules**:
|
||||
- `user_dal.py` - User CRUD, statistics
|
||||
- `subscription_dal.py` - Subscription upsert, status tracking, expiry queries
|
||||
- `payment_dal.py` - Payment records, financial statistics
|
||||
- `promo_code_dal.py` - Promo code management, activation tracking
|
||||
- `user_billing_dal.py` - Saved payment methods for auto-renewal
|
||||
|
||||
**Common Pattern**:
|
||||
```python
|
||||
async def get_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]:
|
||||
stmt = select(User).where(User.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
```
|
||||
|
||||
### 3. Database Models (`db/models.py`)
|
||||
|
||||
**Key Entities**:
|
||||
- **User** - Telegram user profile, panel linkage, referral tree
|
||||
- **Subscription** - Panel-synced subscription state (active/inactive, expiry, traffic, auto-renew flag)
|
||||
- **Payment** - Payment transaction records (status, provider, amount, promo code linkage)
|
||||
- **PromoCode** / **PromoCodeActivation** - Promo codes with usage limits
|
||||
- **UserBilling** / **UserPaymentMethod** - Saved payment methods for auto-renewal
|
||||
- **MessageLog** - Audit trail of all user/admin actions
|
||||
- **PanelSyncStatus** - Tracks automatic panel sync state
|
||||
- **AdCampaign** / **AdAttribution** - Ad tracking
|
||||
|
||||
**Important Relationships**:
|
||||
- User → Subscription (1:Many)
|
||||
- User → Payment (1:Many)
|
||||
- User → User (self-referencing for referral tree via `referred_by_id`)
|
||||
- Subscription has `panel_user_uuid` and `panel_subscription_uuid` for sync
|
||||
|
||||
### 4. Webhook Handling (`bot/app/web/web_server.py`)
|
||||
|
||||
Single AIOHTTP application hosts multiple webhook routes:
|
||||
|
||||
| Path | Handler | Purpose |
|
||||
|------|---------|---------|
|
||||
| `/{BOT_TOKEN}` | SimpleRequestHandler | Telegram updates |
|
||||
| `/webhook/yookassa` | yookassa_webhook_route | YooKassa payment status |
|
||||
| `/webhook/cryptopay` | cryptopay_webhook_route | CryptoPay transactions |
|
||||
| `/webhook/freekassa` | freekassa_webhook_route | FreeKassa payments |
|
||||
| `/webhook/platega` | platega_webhook_route | Platega payments |
|
||||
| `/webhook/severpay` | severpay_webhook_route | SeverPay payments |
|
||||
| `/webhook/panel` | panel_webhook_route | Panel subscription events |
|
||||
|
||||
**Security**: All webhooks verify signatures (HMAC-SHA256 for panel, provider-specific for payment systems).
|
||||
|
||||
### 5. Panel Integration
|
||||
|
||||
**Remnawave API** (`bot/services/panel_api_service.py`):
|
||||
- REST API client with Bearer token auth
|
||||
- User CRUD operations
|
||||
- Device management (HWID disconnect)
|
||||
- Subscription link generation
|
||||
- System/bandwidth stats
|
||||
|
||||
**User Linking Logic**:
|
||||
When activating subscription, the bot ensures a panel user exists:
|
||||
1. Check local DB for `panel_user_uuid`
|
||||
2. If missing, search panel by `telegramId`
|
||||
3. If not found, search by username pattern `tg_{telegram_id}`
|
||||
4. If still missing, create new panel user with configured settings
|
||||
5. Save returned UUID to local DB
|
||||
|
||||
**Panel Webhook Events** (`bot/services/panel_webhook_service.py`):
|
||||
- `user.expires_in_72_hours` → Send notification
|
||||
- `user.expires_in_48_hours` → Send notification (special if auto-renew enabled)
|
||||
- `user.expires_in_24_hours` → **Trigger auto-renewal** if YooKassa + saved card
|
||||
- `user.expired` → Send expiration notice
|
||||
- `user.expired_24_hours_ago` → Follow-up reminder
|
||||
|
||||
### 6. Payment Flow
|
||||
|
||||
**Standard Payment Flow**:
|
||||
1. User selects subscription duration
|
||||
2. Handler creates `Payment` record (status: `pending_{provider}`)
|
||||
3. Service creates payment with provider API
|
||||
4. User redirected to payment page
|
||||
5. Provider sends webhook on completion
|
||||
6. Webhook handler verifies signature, updates Payment status
|
||||
7. If succeeded: call `subscription_service.activate_subscription()`
|
||||
- Ensures panel user exists/linked
|
||||
- Creates/updates Subscription record
|
||||
- Updates panel user expiry and traffic
|
||||
- Enables auto-renew if payment method saved
|
||||
8. Send success notification with config link
|
||||
|
||||
**Auto-Renewal Flow** (YooKassa only):
|
||||
1. Panel webhook triggers 24h before expiry
|
||||
2. `PanelWebhookService` checks if auto-renew enabled
|
||||
3. Calls `subscription_service.charge_subscription_renewal()`
|
||||
4. Uses saved `payment_method_id` for off-session charge
|
||||
5. If successful: suppress 24h notification, create new Payment
|
||||
6. If failed: send notification to user
|
||||
|
||||
## Important Architectural Patterns
|
||||
|
||||
### 1. Dependency Injection via Factory
|
||||
All services created in `build_core_services()`, stored in dispatcher:
|
||||
```python
|
||||
dp["panel_service"] = panel_service
|
||||
# Handlers access:
|
||||
panel_service = data["panel_service"]
|
||||
```
|
||||
|
||||
### 2. Upsert Pattern for Panel Sync
|
||||
Uses `panel_subscription_uuid` as idempotency key to prevent duplicate subscriptions:
|
||||
```python
|
||||
async def upsert_subscription(session, payload):
|
||||
existing = await get_subscription_by_panel_subscription_uuid(...)
|
||||
if existing:
|
||||
# Update fields
|
||||
else:
|
||||
# Create new
|
||||
```
|
||||
|
||||
### 3. FSM for Multi-Step Flows
|
||||
State machine for complex interactions (defined in `bot/states/`):
|
||||
```python
|
||||
# Set state
|
||||
await state.set_state(UserPromoStates.waiting_for_promo_code)
|
||||
|
||||
# Next handler checks state, processes, clears
|
||||
if await state.get_state() == UserPromoStates.waiting_for_promo_code:
|
||||
# Process promo code
|
||||
await state.clear()
|
||||
```
|
||||
|
||||
### 4. Notification Suppression
|
||||
Track `last_notification_sent` per subscription to avoid spam:
|
||||
```python
|
||||
if last_notification_sent is None or date(last_notification_sent) < date(now):
|
||||
# Send notification
|
||||
await update_last_notification_sent(subscription)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings loaded via Pydantic from `.env` (see `.env.example` for full reference).
|
||||
|
||||
**Critical Settings**:
|
||||
- `BOT_TOKEN` - Telegram bot token
|
||||
- `ADMIN_IDS` - Comma-separated admin Telegram IDs
|
||||
- `WEBHOOK_BASE_URL` - External URL for webhooks (HTTPS required)
|
||||
- `PANEL_API_URL`, `PANEL_API_KEY` - Remnawave panel access
|
||||
- `PANEL_WEBHOOK_SECRET` - HMAC signature verification
|
||||
- Payment provider credentials (YooKassa, CryptoPay, etc.)
|
||||
- Pricing: `RUB_PRICE_1_MONTH`, `STARS_PRICE_1_MONTH`, etc.
|
||||
|
||||
**Sales Modes**:
|
||||
- **Time-based** (default): User buys N months subscription
|
||||
- **Traffic-based** (`traffic_sale_mode`): User buys X GB with far-future expiry
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Adding New Payment Provider
|
||||
|
||||
1. Create service in `bot/services/{provider}_service.py` implementing:
|
||||
- `create_payment()` - Generate payment URL/data
|
||||
- Webhook handler function
|
||||
2. Add webhook route in `bot/app/web/web_server.py`
|
||||
3. Add service to `build_core_services()` factory
|
||||
4. Create handler router in `bot/handlers/user/subscription/payments_{provider}.py`
|
||||
5. Register router in `bot/routers.py`
|
||||
6. Add pricing settings to `config/settings.py`
|
||||
7. Update payment method selection in `bot/handlers/user/subscription/payment_methods.py`
|
||||
|
||||
### Adding New Admin Feature
|
||||
|
||||
1. Create handler in `bot/handlers/admin/{feature}.py`
|
||||
2. Add keyboard buttons in `bot/keyboards/inline/admin_keyboards.py`
|
||||
3. Register router in `bot/routers.py` under `admin_main_filtered_router`
|
||||
4. Use `AdminFilter()` to protect routes
|
||||
5. Access services via `data["service_name"]`
|
||||
|
||||
### Database Changes
|
||||
|
||||
The project uses SQLAlchemy models without formal migrations. To add fields:
|
||||
1. Update model in `db/models.py`
|
||||
2. Add corresponding DAL functions in `db/dal/`
|
||||
3. Drop and recreate tables in dev (bot auto-creates on startup)
|
||||
4. For production, manually ALTER tables or use `db/migrator.py` as template
|
||||
|
||||
### Localization
|
||||
|
||||
Translations stored in `locales/{lang}/LC_MESSAGES/messages.json`. To add strings:
|
||||
1. Add key-value to both `ru` and `en` files
|
||||
2. Access in handlers via: `i18n.get("key_name")`
|
||||
3. I18n instance available in handler data: `data["i18n_instance"]`
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
When making changes, verify:
|
||||
- [ ] Webhooks are reachable (use ngrok for local testing)
|
||||
- [ ] Payment flow completes end-to-end
|
||||
- [ ] Panel user creation/linking works
|
||||
- [ ] Subscription activation updates both DB and panel
|
||||
- [ ] Auto-renewal triggers correctly
|
||||
- [ ] Notifications send at proper times
|
||||
- [ ] Admin panel functions accessible only to admins
|
||||
- [ ] Banned users cannot access bot
|
||||
- [ ] Referral bonuses apply correctly
|
||||
- [ ] Promo codes validate and activate
|
||||
|
||||
## Deployment
|
||||
|
||||
**Production Setup**:
|
||||
1. Configure reverse proxy (Nginx/HAProxy) to route HTTPS webhooks to container
|
||||
2. Set `WEBHOOK_BASE_URL` to external domain
|
||||
3. Run `docker compose up -d`
|
||||
4. Verify webhook registration in logs
|
||||
5. Add bot as admin to required channel (if using `REQUIRED_CHANNEL_ID`)
|
||||
6. Test payment flow with small amount
|
||||
|
||||
**Webhook Paths**:
|
||||
- Telegram: `https://yourdomain.com/{BOT_TOKEN}`
|
||||
- YooKassa: `https://yourdomain.com/webhook/yookassa`
|
||||
- Panel: `https://yourdomain.com/webhook/panel`
|
||||
- Other providers: `https://yourdomain.com/webhook/{provider}`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Bot not responding**:
|
||||
- Check webhook is set correctly (logs show webhook URL on startup)
|
||||
- Verify HTTPS is working for `WEBHOOK_BASE_URL`
|
||||
- Check `docker compose logs` for errors
|
||||
|
||||
**Payment not completing**:
|
||||
- Verify webhook route is accessible from provider
|
||||
- Check signature verification is passing
|
||||
- Look for errors in payment webhook handler logs
|
||||
- Ensure `provider_payment_id` is unique (duplicate payments are rejected)
|
||||
|
||||
**Panel sync issues**:
|
||||
- Check `PANEL_API_KEY` is valid
|
||||
- Verify panel user exists with matching UUID
|
||||
- Check panel webhook secret matches
|
||||
- Review `panel_sync_status` table for last sync time
|
||||
|
||||
**Auto-renewal not working**:
|
||||
- Only works with YooKassa
|
||||
- User must have saved payment method (`user_billing` or `user_payment_methods` table)
|
||||
- Subscription must have `auto_renew_enabled = true`
|
||||
- Panel webhook must trigger 24h before expiry
|
||||
@@ -112,6 +112,62 @@ def _migration_0003_normalize_referral_codes(connection: Connection) -> None:
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _migration_0004_add_discount_promo_codes(connection: Connection) -> None:
|
||||
inspector = inspect(connection)
|
||||
|
||||
# 1. Добавить поля в payments
|
||||
payment_columns: Set[str] = {col["name"] for col in inspector.get_columns("payments")}
|
||||
if "original_amount" not in payment_columns:
|
||||
connection.execute(text("ALTER TABLE payments ADD COLUMN original_amount FLOAT"))
|
||||
if "discount_applied" not in payment_columns:
|
||||
connection.execute(text("ALTER TABLE payments ADD COLUMN discount_applied FLOAT"))
|
||||
|
||||
# 2. Модифицировать promo_codes
|
||||
promo_columns: Set[str] = {col["name"] for col in inspector.get_columns("promo_codes")}
|
||||
|
||||
if "promo_type" not in promo_columns:
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE promo_codes ADD COLUMN promo_type VARCHAR NOT NULL DEFAULT 'bonus_days'"
|
||||
)
|
||||
)
|
||||
|
||||
if "discount_percentage" not in promo_columns:
|
||||
connection.execute(
|
||||
text("ALTER TABLE promo_codes ADD COLUMN discount_percentage INTEGER")
|
||||
)
|
||||
|
||||
# Изменить bonus_days на nullable (если еще не nullable)
|
||||
connection.execute(
|
||||
text("ALTER TABLE promo_codes ALTER COLUMN bonus_days DROP NOT NULL")
|
||||
)
|
||||
|
||||
# Создать индекс на promo_type
|
||||
connection.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_promo_codes_promo_type ON promo_codes (promo_type)"
|
||||
)
|
||||
)
|
||||
|
||||
# 3. Создать таблицу active_discounts
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS active_discounts (
|
||||
user_id BIGINT PRIMARY KEY,
|
||||
promo_code_id INTEGER NOT NULL,
|
||||
discount_percentage INTEGER NOT NULL,
|
||||
activated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT fk_active_discounts_user
|
||||
FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_active_discounts_promo_code
|
||||
FOREIGN KEY (promo_code_id) REFERENCES promo_codes (promo_code_id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
MIGRATIONS: List[Migration] = [
|
||||
Migration(
|
||||
id="0001_add_channel_subscription_fields",
|
||||
@@ -128,6 +184,11 @@ MIGRATIONS: List[Migration] = [
|
||||
description="Normalize referral codes to uppercase for consistent lookups",
|
||||
upgrade=_migration_0003_normalize_referral_codes,
|
||||
),
|
||||
Migration(
|
||||
id="0004_add_discount_promo_codes",
|
||||
description="Add support for percentage discount promo codes",
|
||||
upgrade=_migration_0004_add_discount_promo_codes,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user