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": "⏰ Действует до:",