Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

better statistics #30

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 90 additions & 19 deletions handlers/admin/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder

import config
from bot import bot
from handlers.common.common import add_pagination_buttons
from services.buy import BuyService
Expand Down Expand Up @@ -50,8 +50,9 @@ class AdminConstants:
callback_data=create_admin_callback(level=0))

@staticmethod
async def get_back_button(current_level: int) -> types.InlineKeyboardButton:
return types.InlineKeyboardButton(text="Back", callback_data=create_admin_callback(level=current_level - 1))
async def get_back_button(unpacked_callback: AdminCallback) -> types.InlineKeyboardButton:
new_callback = unpacked_callback.model_copy(update={"level": unpacked_callback.level - 1})
return types.InlineKeyboardButton(text="Back", callback_data=new_callback.pack())


@admin_router.message(Command("admin"), AdminIdFilter())
Expand All @@ -71,10 +72,10 @@ async def admin(message: Union[Message, CallbackQuery]):
callback_data=create_admin_callback(
level=5,
action="send_to_everyone"))
admin_menu_builder.button(text="Get new users",
admin_menu_builder.button(text="Get database file",
callback_data=create_admin_callback(
level=6,
action="get_new_users"
action="get_db_file"
))
admin_menu_builder.button(text="Delete category",
callback_data=create_admin_callback(
Expand All @@ -88,6 +89,8 @@ async def admin(message: Union[Message, CallbackQuery]):
callback_data=create_admin_callback(
level=11
))
admin_menu_builder.button(text="Statistics",
callback_data=create_admin_callback(level=14))
admin_menu_builder.adjust(2)
if isinstance(message, Message):
await message.answer("<b>Admin Menu:</b>", parse_mode=ParseMode.HTML,
Expand Down Expand Up @@ -181,18 +184,6 @@ async def send_restocking_message(callback: CallbackQuery):
reply_markup=AdminConstants.confirmation_builder.as_markup())


async def get_new_users(callback: CallbackQuery):
users_builder = InlineKeyboardBuilder()
new_users = UserService.get_new_users()
for user in new_users:
if user.telegram_username:
user_button = types.InlineKeyboardButton(text=user.telegram_username, url=f"t.me/{user.telegram_username}")
users_builder.add(user_button)
users_builder.add(AdminConstants.back_to_main_button)
users_builder.adjust(1)
await callback.message.edit_text(text=f"{len(new_users)} new users:", reply_markup=users_builder.as_markup())


async def create_delete_entity_buttons(get_all_entities_function,
entity_name):
entities = get_all_entities_function
Expand Down Expand Up @@ -317,7 +308,7 @@ async def refund_confirmation(callback: CallbackQuery):
unpacked_callback = AdminCallback.unpack(callback.data)
current_level = unpacked_callback.level
buy_id = int(unpacked_callback.args_to_action)
back_button = await AdminConstants.get_back_button(current_level)
back_button = await AdminConstants.get_back_button(unpacked_callback)
confirm_button = types.InlineKeyboardButton(text="Confirm",
callback_data=create_admin_callback(level=current_level + 1,
action="confirm_refund",
Expand All @@ -340,6 +331,83 @@ async def refund_confirmation(callback: CallbackQuery):
reply_markup=confirmation_builder.as_markup())


async def pick_statistics_entity(callback: CallbackQuery):
unpacked_callback = AdminCallback.unpack(callback.data)
users_statistics_callback = create_admin_callback(unpacked_callback.level + 1, "users")
buys_statistics_callback = create_admin_callback(unpacked_callback.level + 1, "buys")
buttons_builder = InlineKeyboardBuilder()
buttons_builder.add(types.InlineKeyboardButton(text="📊Users statistics", callback_data=users_statistics_callback))
buttons_builder.add(types.InlineKeyboardButton(text="📊Buys statistics", callback_data=buys_statistics_callback))
buttons_builder.row(AdminConstants.back_to_main_button)
await callback.message.edit_text(text="<b>📊 Pick statistics entity</b>", reply_markup=buttons_builder.as_markup(),
parse_mode=ParseMode.HTML)


async def pick_statistics_timedelta(callback: CallbackQuery):
unpacked_callback = AdminCallback.unpack(callback.data)
one_day_cb = unpacked_callback.model_copy(
update={"args_to_action": '1', 'level': unpacked_callback.level + 1}).pack()
seven_days_cb = unpacked_callback.model_copy(
update={"args_to_action": '7', 'level': unpacked_callback.level + 1}).pack()
one_month_cb = unpacked_callback.model_copy(
update={"args_to_action": '30', 'level': unpacked_callback.level + 1}).pack()
timedelta_buttons_builder = InlineKeyboardBuilder()
timedelta_buttons_builder.add(types.InlineKeyboardButton(text="1 Day", callback_data=one_day_cb))
timedelta_buttons_builder.add(types.InlineKeyboardButton(text="7 Days", callback_data=seven_days_cb))
timedelta_buttons_builder.add(types.InlineKeyboardButton(text="30 Days", callback_data=one_month_cb))
timedelta_buttons_builder.row(await AdminConstants.get_back_button(unpacked_callback))
await callback.message.edit_text(text="<b>🗓 Pick timedelta to statistics</b>",
reply_markup=timedelta_buttons_builder.as_markup(), parse_mode=ParseMode.HTML)


async def get_statistics(callback: CallbackQuery):
unpacked_callback = AdminCallback.unpack(callback.data)
statistics_keyboard_builder = InlineKeyboardBuilder()
if unpacked_callback.action == "users":
users, users_count = UserService.get_new_users_by_timedelta(unpacked_callback.args_to_action,
unpacked_callback.page)
for user in users:
if user.telegram_username:
user_button = types.InlineKeyboardButton(text=user.telegram_username,
url=f"t.me/{user.telegram_username}")
statistics_keyboard_builder.add(user_button)
statistics_keyboard_builder.adjust(1)
statistics_keyboard_builder = await add_pagination_buttons(statistics_keyboard_builder, callback.data,
UserService.get_max_page_for_users_by_timedelta(
unpacked_callback.args_to_action),
AdminCallback.unpack, None)
statistics_keyboard_builder.row(
*[AdminConstants.back_to_main_button, await AdminConstants.get_back_button(unpacked_callback)])
await callback.message.edit_text(
text=f"<b>{users_count} new users in the last {unpacked_callback.args_to_action} days:</b>",
reply_markup=statistics_keyboard_builder.as_markup(), parse_mode=ParseMode.HTML)

elif unpacked_callback.action == "buys":
back_button = await AdminConstants.get_back_button(unpacked_callback)
buttons = [back_button,
AdminConstants.back_to_main_button]
statistics_keyboard_builder.add(*buttons)
buys = BuyService.get_new_buys_by_timedelta(unpacked_callback.args_to_action)
total_profit = 0
items_sold = 0
for buy in buys:
total_profit += buy.total_price
items_sold += buy.quantity
await callback.message.edit_text(
text=f"<b>📊 Sales statistics for the last {unpacked_callback.args_to_action} days.\n"
f"💰 Total profit: ${total_profit}\n"
f"🛍️ Items sold: {items_sold}\n"
f"💼 Total buys: {len(buys)}</b>", reply_markup=statistics_keyboard_builder.as_markup(),
parse_mode=ParseMode.HTML)


async def send_db_file(callback: CallbackQuery):
with open(f"./data/{config.DB_NAME}", "rb") as f:
await callback.message.bot.send_document(callback.from_user.id,
types.BufferedInputFile(file=f.read(), filename="database.db"))
await callback.answer()


async def make_refund(callback: CallbackQuery):
unpacked_callback = AdminCallback.unpack(callback.data)
buy_id = int(unpacked_callback.args_to_action)
Expand Down Expand Up @@ -371,14 +439,17 @@ async def admin_menu_navigation(callback: CallbackQuery, state: FSMContext, call
3: decline_action,
4: add_items,
5: send_restocking_message,
6: get_new_users,
6: send_db_file,
7: delete_category,
8: delete_subcategory,
9: delete_confirmation,
10: confirm_and_delete,
11: send_refund_menu,
12: refund_confirmation,
13: make_refund,
14: pick_statistics_entity,
15: pick_statistics_timedelta,
16: get_statistics
}

current_level_function = levels[current_level]
Expand Down
2 changes: 1 addition & 1 deletion handlers/common/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


async def add_pagination_buttons(keyboard_builder: InlineKeyboardBuilder, callback_str: str, max_page_function,
callback_unpack_function, back_button):
callback_unpack_function, back_button) -> InlineKeyboardBuilder:
unpacked_callback = callback_unpack_function(callback_str)
maximum_page = max_page_function
buttons = []
Expand Down
45 changes: 29 additions & 16 deletions handlers/user/my_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from crypto_api.CryptoApiManager import CryptoApiManager
from handlers.common.common import add_pagination_buttons
from handlers.user.all_categories import create_message_with_bought_items
from services.buy import BuyService
from services.buyItem import BuyItemService
Expand All @@ -22,10 +23,16 @@ class MyProfileCallback(CallbackData, prefix="my_profile"):
level: int
action: str
args_for_action: Union[int, str]
page: int


def create_callback_profile(level: int, action: str = "", args_for_action=""):
return MyProfileCallback(level=level, action=action, args_for_action=args_for_action).pack()
def create_callback_profile(level: int, action: str = "", args_for_action="", page=0):
return MyProfileCallback(level=level, action=action, args_for_action=args_for_action, page=page).pack()


class MyProfileConstants:
back_to_main_menu = types.InlineKeyboardButton(text="⤵️Back my profile",
callback_data=create_callback_profile(level=0))


@my_profile_router.message(F.text == "🎓 My profile", IsUserExistFilter())
Expand Down Expand Up @@ -100,35 +107,40 @@ async def top_up_balance(callback: CallbackQuery):
await callback.answer()


async def purchase_history(callback: CallbackQuery):
telegram_id = callback.message.chat.id
user = UserService.get_by_tgid(telegram_id)
current_level = 2
orders = BuyService.get_buys_by_buyer_id(user.id)
async def create_purchase_history_keyboard_builder(page: int, user_id: int):
orders_markup_builder = InlineKeyboardBuilder()
back_to_profile_button = types.InlineKeyboardButton(text='Back',
callback_data=create_callback_profile(current_level - 2))
orders = BuyService.get_buys_by_buyer_id(user_id, page)
for order in orders:
quantity = order.quantity
total_price = order.total_price
buy_id = order.id
buy_item = BuyItemService.get_buy_item_by_buy_id(buy_id)
item = ItemService.get_by_primary_key(buy_item.item_id)
item_from_history_callback = create_callback_profile(current_level + 2, action="get_order",
item_from_history_callback = create_callback_profile(4, action="get_order",
args_for_action=str(buy_id))
order_inline = types.InlineKeyboardButton(
text=f"{item.subcategory.name} | Total Price: {total_price}$ | Quantity: {quantity} pcs",
callback_data=item_from_history_callback
)
orders_markup_builder.add(order_inline)
orders_markup_builder.add(back_to_profile_button)
orders_markup_builder.adjust(1)
orders_markup = orders_markup_builder.as_markup()
if not orders:
await callback.message.edit_text("<b>You haven't had any purchases yet</b>", reply_markup=orders_markup,
return orders_markup_builder, len(orders)


async def purchase_history(callback: CallbackQuery):
unpacked_callback = MyProfileCallback.unpack(callback.data)
telegram_id = callback.message.chat.id
user = UserService.get_by_tgid(telegram_id)
orders_markup_builder, orders_num = await create_purchase_history_keyboard_builder(unpacked_callback.page, user.id)
orders_markup_builder = await add_pagination_buttons(orders_markup_builder, callback.data,
BuyService.get_max_page_purchase_history(user.id),
MyProfileCallback.unpack, MyProfileConstants.back_to_main_menu)
if orders_num == 0:
await callback.message.edit_text("<b>You haven't had any purchases yet</b>",
reply_markup=orders_markup_builder.as_markup(),
parse_mode=ParseMode.HTML)
else:
await callback.message.edit_text('<b>Your purchases:</b>', reply_markup=orders_markup,
await callback.message.edit_text('<b>Your purchases:</b>', reply_markup=orders_markup_builder.as_markup(),
parse_mode=ParseMode.HTML)
await callback.answer()

Expand All @@ -144,7 +156,8 @@ async def refresh_balance(callback: CallbackQuery):
crypto_prices = await CryptoApiManager.get_crypto_prices()
deposit_usd_amount = 0.0
if sum(new_crypto_balances.values()) > sum(old_crypto_balances.values()):
merged_deposit = {key: new_crypto_balances[key] - old_crypto_balances[key] for key in new_crypto_balances.keys()}
merged_deposit = {key: new_crypto_balances[key] - old_crypto_balances[key] for key in
new_crypto_balances.keys()}
for balance_key, balance in merged_deposit.items():
balance_key = balance_key.split('_')[0]
crypto_balance_in_usd = balance * crypto_prices[balance_key]
Expand Down
4 changes: 2 additions & 2 deletions models/buy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime

from sqlalchemy import Column, Integer, Float, DateTime, Boolean, ForeignKey
from sqlalchemy import Column, Integer, Float, DateTime, Boolean, ForeignKey, func
from sqlalchemy.orm import relationship

from models.base import Base
Expand All @@ -14,5 +14,5 @@ class Buy(Base):
buyer = relationship('User', backref='buys')
quantity = Column(Integer, nullable=False)
total_price = Column(Float, nullable=False)
buy_datetime = Column(DateTime, default=datetime.datetime.utcnow())
buy_datetime = Column(DateTime, default=func.now())
is_refunded = Column(Boolean, default=False)
6 changes: 2 additions & 4 deletions models/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import datetime

from sqlalchemy import Column, Integer, DateTime, String, Boolean, Float
from sqlalchemy import Column, Integer, DateTime, String, Boolean, Float, func

from models.base import Base

Expand All @@ -21,5 +19,5 @@ class User(Base):
ltc_balance = Column(Float, nullable=False, default=0.0)
usdt_balance = Column(Float, nullable=False, default=0.0)
is_new = Column(Boolean, default=True)
registered_at = Column(DateTime, default=datetime.datetime.utcnow())
registered_at = Column(DateTime, default=func.now())
seed = Column(String, nullable=False, unique=True)
35 changes: 31 additions & 4 deletions services/buy.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import math

from sqlalchemy import select, update, func
Expand All @@ -9,15 +10,27 @@


class BuyService:
buys_per_page = 25
buys_per_page = 20

@staticmethod
def get_buys_by_buyer_id(buyer_id: int):
def get_buys_by_buyer_id(buyer_id: int, page: int):
with session_maker() as session:
stmt = select(Buy).where(Buy.buyer_id == buyer_id)
stmt = select(Buy).where(Buy.buyer_id == buyer_id).limit(BuyService.buys_per_page).offset(
page * BuyService.buys_per_page)
buys = session.execute(stmt)
return buys.scalars().all()

@staticmethod
def get_max_page_purchase_history(buyer_id: int):
with session_maker() as session:
stmt = select(func.count(Buy.id)).where(Buy.buyer_id == buyer_id)
max_page = session.execute(stmt)
max_page = max_page.scalar_one()
if max_page % BuyService.buys_per_page == 0:
return max_page / BuyService.buys_per_page - 1
else:
return math.trunc(max_page / BuyService.buys_per_page)

@staticmethod
def insert_new(user: User, quantity: int, total_price: float) -> int:
with session_maker() as session:
Expand Down Expand Up @@ -48,4 +61,18 @@ def get_max_refund_pages():
with session_maker() as session:
stmt = select(func.count(Buy.id)).where(Buy.is_refunded == 0)
not_refunded_buys = session.execute(stmt)
return math.trunc(not_refunded_buys.scalar_one() / BuyService.buys_per_page)
not_refunded_buys = not_refunded_buys.scalar_one()
if not_refunded_buys % BuyService.buys_per_page == 0:
return not_refunded_buys/BuyService.buys_per_page - 1
else:
return math.trunc(not_refunded_buys / BuyService.buys_per_page)

@staticmethod
def get_new_buys_by_timedelta(timedelta_int):
with session_maker() as session:
current_time = datetime.datetime.now()
one_day_interval = datetime.timedelta(days=int(timedelta_int))
time_to_subtract = current_time - one_day_interval
stmt = select(Buy).where(Buy.buy_datetime >= time_to_subtract)
buys = session.execute(stmt)
return buys.scalars().all()
20 changes: 15 additions & 5 deletions services/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class CategoryService:
items_per_page = 25
items_per_page = 20

@staticmethod
def get_or_create_one(category_name: str) -> Category:
Expand Down Expand Up @@ -50,8 +50,18 @@ def get_unsold(page) -> list[Category]:

@staticmethod
def get_maximum_page():
#TODO(Pagination bug with get last page)
with session_maker() as session:
stmt = select(func.count(Category.id)).distinct()
subcategories = session.execute(stmt)
subcategories_count = subcategories.scalar_one()
return math.trunc(subcategories_count / CategoryService.items_per_page)
unique_categories_subquery = (
select(Category.id)
.join(Item, Item.category_id == Category.id)
.filter(Item.is_sold == 0)
.distinct()
).alias('unique_categories')
stmt = select(func.count()).select_from(unique_categories_subquery)
max_page = session.execute(stmt)
max_page = max_page.scalar_one()
if max_page % CategoryService.items_per_page == 0:
return max_page / CategoryService.items_per_page - 1
else:
return math.trunc(max_page / CategoryService.items_per_page)
Loading