diff --git a/.env.example b/.env.example
index 271b9d1..5a98439 100644
--- a/.env.example
+++ b/.env.example
@@ -50,6 +50,7 @@ FREEKASSA_MERCHANT_ID=your_shop_id #
FREEKASSA_API_KEY=your_api_key # API key for REST requests
FREEKASSA_SECOND_SECRET=your_second_secret # Secret word #2 (used to verify notifications)
FREEKASSA_PAYMENT_IP= # Public IP address reported to FreeKassa
+FREEKASSA_PAYMENT_METHOD_ID=44 # Payment method ID, you can get it from https://merchant.freekassa.net/settings/currencies
# CryptoBot Payment Gateway Configuration
CRYPTOPAY_TOKEN= # API token for CryptoPay
diff --git a/.github/workflows/dev-docker-build.yml b/.github/workflows/dev-docker-build.yml
index 96a0358..36cb4c0 100644
--- a/.github/workflows/dev-docker-build.yml
+++ b/.github/workflows/dev-docker-build.yml
@@ -9,8 +9,9 @@ on:
- dev
env:
- REGISTRY: ghcr.io
+ GHCR_REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
+ DOCKERHUB_IMAGE: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
jobs:
build:
@@ -26,18 +27,26 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- - name: Log in to Container Registry
+ - name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ images: |
+ ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
+ ${{ env.DOCKERHUB_IMAGE }}
tags: |
type=ref,event=branch
type=ref,event=pr
@@ -56,4 +65,4 @@ jobs:
cache-to: type=gha,mode=max
- name: Image digest
- run: echo ${{ steps.meta.outputs.digest }}
\ No newline at end of file
+ run: echo ${{ steps.meta.outputs.digest }}
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index c9a6be4..e1a8a28 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -9,6 +9,10 @@ on:
paths-ignore:
- 'README.md'
+env:
+ GHCR_IMAGE: ghcr.io/${{ github.repository }}
+ DOCKERHUB_IMAGE: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
+
jobs:
build-and-push:
runs-on: ubuntu-latest
@@ -27,6 +31,12 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
- name: Set up QEMU (для эмуляции arm64 на x86)
uses: docker/setup-qemu-action@v3
@@ -40,5 +50,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: |
- ghcr.io/${{ github.repository }}:latest
- ghcr.io/${{ github.repository }}:${{ github.ref_name }}
+ ${{ env.GHCR_IMAGE }}:latest
+ ${{ env.GHCR_IMAGE }}:${{ github.ref_name }}
+ ${{ env.DOCKERHUB_IMAGE }}:latest
+ ${{ env.DOCKERHUB_IMAGE }}:${{ github.ref_name }}
diff --git a/README.md b/README.md
index 5f2a2e0..1849156 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@
| `FREEKASSA_SECOND_SECRET` | Секретное слово №2 — используется для проверки уведомлений от FreeKassa. |
| `FREEKASSA_PAYMENT_URL` | (Опционально, legacy SCI) Базовый URL платёжной формы FreeKassa. По умолчанию `https://pay.freekassa.ru/`. |
| `FREEKASSA_PAYMENT_IP` | Внешний IP вашего сервера, который будет передаваться в запрос оплаты. |
+ | `FREEKASSA_PAYMENT_METHOD_ID` | ID метода оплаты через магазин FreeKassa. По умолчанию `44`. |
| `STARS_ENABLED` | Включить/выключить Telegram Stars (`true`/`false`). |
| `TRIBUTE_ENABLED`| Включить/выключить Tribute (`true`/`false`). |
@@ -163,6 +164,8 @@
Файлы `Dockerfile` и `docker-compose.yml` уже настроены для сборки и запуска проекта. `docker-compose.yml` использует готовый образ с GitHub Container Registry, но вы можете раскомментировать `build: .` для локальной сборки.
+Для автоматической публикации образов настроены GitHub Actions (`.github/workflows`). По умолчанию образы пушатся в GitHub Container Registry и Docker Hub. Добавьте в Secrets репозитория значения `DOCKERHUB_USERNAME` и `DOCKERHUB_TOKEN` (персональный access token или пароль для Docker Hub), чтобы загрузка в Docker Hub работала корректно.
+
## 📁 Структура проекта
```
diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py
index 548bfd9..db0e602 100644
--- a/bot/handlers/admin/user_management.py
+++ b/bot/handlers/admin/user_management.py
@@ -192,6 +192,20 @@ async def format_user_card(user: User, session: AsyncSession,
trial_status = _("admin_user_trial_used", default="Использовал") if had_subscriptions else _("admin_user_trial_not_used", default="Не использовал")
card_parts.append(f"{_('admin_user_trial_label', default='🏡 Триал:')} {hcode(trial_status)}")
+ # Financial analytics (admin-only)
+ try:
+ from db.dal import payment_dal
+
+ # Total amount paid by this user
+ total_paid = await payment_dal.get_user_total_paid(session, user.user_id)
+ card_parts.append(f"{_('admin_user_total_paid_label', default='💰 Всего оплачено:')} {hcode(f'{total_paid:.2f} RUB')}")
+
+ # Total revenue from referrals
+ referral_revenue = await payment_dal.get_referral_revenue(session, user.user_id)
+ card_parts.append(f"{_('admin_user_referral_revenue_label', default='💸 Доход по рефералам:')} {hcode(f'{referral_revenue:.2f} RUB')}")
+ except Exception as e_fin:
+ logging.error(f"Failed to build financial analytics for admin card {user.user_id}: {e_fin}")
+
# Referral stats
if referral_service is not None:
try:
diff --git a/bot/handlers/user/subscription/payments.py b/bot/handlers/user/subscription/payments.py
index d9815cb..1c21005 100644
--- a/bot/handlers/user/subscription/payments.py
+++ b/bot/handlers/user/subscription/payments.py
@@ -881,10 +881,10 @@ async def pay_fk_callback_handler(
months=months,
amount=price_rub,
currency=freekassa_service.default_currency,
- method_code=44,
+ payment_method_id=freekassa_service.payment_method_id,
ip_address=freekassa_service.server_ip,
extra_params={
- "us_method": 44,
+ "us_method": freekassa_service.payment_method_id,
},
)
diff --git a/bot/middlewares/channel_subscription.py b/bot/middlewares/channel_subscription.py
index 3af434b..902c061 100644
--- a/bot/middlewares/channel_subscription.py
+++ b/bot/middlewares/channel_subscription.py
@@ -40,6 +40,14 @@ class ChannelSubscriptionMiddleware(BaseMiddleware):
if not event_user or event_user.id in self.settings.ADMIN_IDS:
return await handler(event, data)
+ callback_query = event.callback_query
+ if (
+ callback_query
+ and callback_query.data
+ and callback_query.data == "channel_subscription:verify"
+ ):
+ return await handler(event, data)
+
# Allow /start to reach the handler so the check can be re-run.
message_object: Optional[Message] = event.message
if (
diff --git a/bot/services/freekassa_service.py b/bot/services/freekassa_service.py
index 662b379..23f266d 100644
--- a/bot/services/freekassa_service.py
+++ b/bot/services/freekassa_service.py
@@ -45,6 +45,7 @@ class FreeKassaService:
self.second_secret: Optional[str] = settings.FREEKASSA_SECOND_SECRET
self.default_currency: str = (settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
self.server_ip: Optional[str] = settings.FREEKASSA_PAYMENT_IP
+ self.payment_method_id: Optional[int] = settings.FREEKASSA_PAYMENT_METHOD_ID
self.api_base_url: str = "https://api.fk.life/v1"
self._timeout = ClientTimeout(total=15)
@@ -72,9 +73,9 @@ class FreeKassaService:
months: int,
amount: float,
currency: Optional[str],
- method_code: int,
email: Optional[str] = None,
ip_address: Optional[str] = None,
+ payment_method_id: Optional[int] = None,
extra_params: Optional[Dict[str, Any]] = None,
) -> Tuple[bool, Dict[str, Any]]:
if not self.configured:
@@ -94,7 +95,7 @@ class FreeKassaService:
"shopId": int(self.shop_id),
"nonce": await self._generate_nonce(),
"paymentId": str(payment_db_id),
- "i": int(method_code),
+ "i": int(payment_method_id),
"amount": amount_str,
"currency": currency_code,
"email": email,
diff --git a/config/settings.py b/config/settings.py
index f7a86a9..561f66c 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -57,6 +57,7 @@ class Settings(BaseSettings):
FREEKASSA_PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/")
FREEKASSA_API_KEY: Optional[str] = None
FREEKASSA_PAYMENT_IP: Optional[str] = None
+ FREEKASSA_PAYMENT_METHOD_ID: Optional[int] = None
YOOKASSA_ENABLED: bool = Field(default=True)
STARS_ENABLED: bool = Field(default=True)
diff --git a/db/dal/payment_dal.py b/db/dal/payment_dal.py
index 939ff2b..cfd63de 100644
--- a/db/dal/payment_dal.py
+++ b/db/dal/payment_dal.py
@@ -262,3 +262,37 @@ async def get_last_tribute_payment(
Payment.created_at.desc()).limit(1))
result = await session.execute(stmt)
return result.scalar_one_or_none()
+
+
+async def get_user_total_paid(session: AsyncSession, user_id: int) -> float:
+ """Get total amount paid by a specific user (sum of all succeeded payments)."""
+ stmt = select(func.sum(Payment.amount)).where(
+ and_(
+ Payment.user_id == user_id,
+ Payment.status == 'succeeded'
+ )
+ )
+ result = await session.execute(stmt)
+ total = result.scalar()
+ return float(total or 0)
+
+
+async def get_referral_revenue(session: AsyncSession, referrer_id: int) -> float:
+ """Get total revenue generated from referred users' payments.
+
+ This calculates the sum of all succeeded payments made by users
+ where referred_by_id equals the referrer_id.
+ """
+ from db.models import User
+
+ stmt = select(func.sum(Payment.amount)).join(
+ User, Payment.user_id == User.user_id
+ ).where(
+ and_(
+ User.referred_by_id == referrer_id,
+ Payment.status == 'succeeded'
+ )
+ )
+ result = await session.execute(stmt)
+ total = result.scalar()
+ return float(total or 0)
\ No newline at end of file
diff --git a/docker-compose-removed-server.yml b/docker-compose-removed-server.yml
new file mode 100644
index 0000000..67e07ae
--- /dev/null
+++ b/docker-compose-removed-server.yml
@@ -0,0 +1,41 @@
+services:
+ remnawave-tg-shop:
+ image: ghcr.io/machka-pasla/remnawave-tg-shop:latest
+ container_name: remnawave-tg-shop
+ hostname: remnawave-tg-shop
+ env_file:
+ - .env
+ ports:
+ - 8080:8080
+ networks:
+ - remnawave-network
+ environment:
+ - TZ=UTC
+ volumes:
+ - ./locales:/app/locales
+ restart: unless-stopped
+ depends_on:
+ - remnawave-tg-shop-db
+ remnawave-tg-shop-db:
+ image: postgres:17
+ container_name: remnawave-tg-shop-db
+ hostname: remnawave-tg-shop-db
+ env_file:
+ - .env
+ environment:
+ - TZ=UTC
+ volumes:
+ - remnawave-tg-shop-db-data:/var/lib/postgresql/data
+ networks:
+ - remnawave-network
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
+ interval: 5s
+ timeout: 5s
+ retries: 20
+networks:
+ remnawave-network: null
+volumes:
+ remnawave-tg-shop-db-data:
+ name: remnawave-tg-shop-db-data
diff --git a/locales/en.json b/locales/en.json
index d74e821..cc2437e 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -337,6 +337,8 @@
"admin_user_na_value": "N/A",
"admin_user_subscription_none": "No active subscription",
"admin_user_actions_count_label": "📜 Total actions:",
+ "admin_user_total_paid_label": "💰 Total Paid:",
+ "admin_user_referral_revenue_label": "💸 Referral Revenue:",
"admin_user_invited_friends_label": "👥 Friends invited:",
"admin_user_ref_purchased_label": "💳 Purchased subscription:",
"admin_user_subscription_active_until": "⏰ Active until:",
diff --git a/locales/ru.json b/locales/ru.json
index c20d53c..ad106ae 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -337,6 +337,8 @@
"admin_user_na_value": "N/A",
"admin_user_subscription_none": "Нет активной подписки",
"admin_user_actions_count_label": "📜 Всего действий:",
+ "admin_user_total_paid_label": "💰 Всего оплачено:",
+ "admin_user_referral_revenue_label": "💸 Доход по рефералам:",
"admin_user_invited_friends_label": "👥 Приглашено друзей:",
"admin_user_ref_purchased_label": "💳 Купили подписку:",
"admin_user_subscription_active_until": "⏰ Действует до:",