From 7e853c48e71a8f348bc584ec5d38d8c429c138b8 Mon Sep 17 00:00:00 2001 From: fakz9 Date: Sat, 24 Feb 2024 15:38:48 +0300 Subject: [PATCH] feat: balance and reward --- app.py | 1 + database/__init__.py | 5 +++- database/enums.py | 6 ++++ database/models/__init__.py | 2 ++ database/models/balance.py | 40 +++++++++++++++++++++++++ database/{models.py => models/basic.py} | 5 ++-- {scanner => queries}/__init__.py | 0 queries/balance/__init__.py | 0 queries/balance/api/__init__.py | 4 +++ queries/balance/api/requests.py | 29 ++++++++++++++++++ queries/balance/schemas/__init__.py | 0 queries/balance/schemas/entities.py | 13 ++++++++ queries/balance/schemas/requests.py | 10 +++++++ queries/balance/schemas/responses.py | 12 ++++++++ requirements.txt | 3 +- routes/__init__.py | 19 ++++++------ routes/assembly.py | 14 +++++++-- routes/balance.py | 23 ++++++++++++++ routes/general.py | 2 +- routes/sipro.py | 20 +++++++++++++ scanner/enums.py | 13 -------- scanner/search.py | 11 ------- scanner/utils.py | 9 ------ schemas/__init__.py | 0 schemas/base.py | 20 +++++++++++++ utils/balance.py | 23 ++++++++++++++ utils/barcodes_synchronizer.py | 16 ---------- 27 files changed, 234 insertions(+), 66 deletions(-) create mode 100644 database/models/__init__.py create mode 100644 database/models/balance.py rename database/{models.py => models/basic.py} (93%) rename {scanner => queries}/__init__.py (100%) create mode 100644 queries/balance/__init__.py create mode 100644 queries/balance/api/__init__.py create mode 100644 queries/balance/api/requests.py create mode 100644 queries/balance/schemas/__init__.py create mode 100644 queries/balance/schemas/entities.py create mode 100644 queries/balance/schemas/requests.py create mode 100644 queries/balance/schemas/responses.py create mode 100644 routes/balance.py delete mode 100644 scanner/enums.py delete mode 100644 scanner/search.py delete mode 100644 scanner/utils.py create mode 100644 schemas/__init__.py create mode 100644 schemas/base.py create mode 100644 utils/balance.py delete mode 100644 utils/barcodes_synchronizer.py diff --git a/app.py b/app.py index 6d1bd5e..7b63791 100644 --- a/app.py +++ b/app.py @@ -32,6 +32,7 @@ blueprints = [ (routes.application_blueprint, '/application'), (routes.sipro_blueprint, '/sipro'), (routes.admin_blueprint, '/admin'), + (routes.balance_blueprint, '/balance'), ] for blueprint, url_prefix in blueprints: diff --git a/database/__init__.py b/database/__init__.py index 70cdde6..00f46f8 100644 --- a/database/__init__.py +++ b/database/__init__.py @@ -1 +1,4 @@ -from database.models import * +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() +from .models import * diff --git a/database/enums.py b/database/enums.py index e167a8d..da97cc8 100644 --- a/database/enums.py +++ b/database/enums.py @@ -8,3 +8,9 @@ class AssemblyState(IntEnum): ALL_PRODUCTS_ASSEMBLED = 2, CONFIRMED = 3, ENDED = 4 + + +@unique +class BalanceTransactionType(IntEnum): + TOP_UP = 0 + WITHDRAW = 1 diff --git a/database/models/__init__.py b/database/models/__init__.py new file mode 100644 index 0000000..c45db11 --- /dev/null +++ b/database/models/__init__.py @@ -0,0 +1,2 @@ +from .basic import * +from .balance import * \ No newline at end of file diff --git a/database/models/balance.py b/database/models/balance.py new file mode 100644 index 0000000..49baa3c --- /dev/null +++ b/database/models/balance.py @@ -0,0 +1,40 @@ +from sqlalchemy import Connection, func +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapper + +import database +from database import db + + +class BalanceTransaction(db.Model): + __tablename__ = 'balance_transactions' + + id = db.Column(db.Integer, primary_key=True, comment='ID транзакции') + type = db.Column(db.Integer, nullable=False, comment='Тип транзакции') + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + user = db.relationship('User', backref='transactions') + amount = db.Column(db.Double, nullable=False, comment='Сумма транзакции') + description = db.Column(db.Text, nullable=False, comment='Описание') + json_data = db.Column(JSONB, comment='JSON данные') + created_at = db.Column(db.DateTime, nullable=False, comment='Дата и время создания транзакции') + + +def recalculate_user_balance(user_id: int): + user_balance = (database.BalanceTransaction.query.filter_by(user_id=user_id) + .group_by(database.BalanceTransaction.user_id) + .with_entities(func.sum(database.BalanceTransaction.amount)) + .scalar()) or 0 + + (db.session.query(database.User) + .filter_by(id=user_id) + .update({'balance': user_balance}, synchronize_session='fetch')) + + +@db.event.listens_for(BalanceTransaction, 'after_insert') +def after_balance_transaction_insert(mapper: Mapper, connection: Connection, target: BalanceTransaction): + recalculate_user_balance(target.user_id) + + +@db.event.listens_for(BalanceTransaction, 'after_delete') +def after_balance_transaction_delete(mapper: Mapper, connection: Connection, target: BalanceTransaction): + recalculate_user_balance(target.user_id) diff --git a/database/models.py b/database/models/basic.py similarity index 93% rename from database/models.py rename to database/models/basic.py index 22399f1..7b5e366 100644 --- a/database/models.py +++ b/database/models/basic.py @@ -1,6 +1,4 @@ -from flask_sqlalchemy import SQLAlchemy - -db = SQLAlchemy() +from database import db class User(db.Model): @@ -14,6 +12,7 @@ class User(db.Model): is_admin = db.Column(db.Boolean, nullable=False, default=False, server_default='0', comment='Админ ли юзверь') city_id = db.Column(db.Integer, nullable=False, default='1', server_default='1', comment='ID страны') + balance = db.Column(db.Double, nullable=False, default=0, server_default='0', comment='Баланс пользователя') class Assembly(db.Model): diff --git a/scanner/__init__.py b/queries/__init__.py similarity index 100% rename from scanner/__init__.py rename to queries/__init__.py diff --git a/queries/balance/__init__.py b/queries/balance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/queries/balance/api/__init__.py b/queries/balance/api/__init__.py new file mode 100644 index 0000000..5ae4f41 --- /dev/null +++ b/queries/balance/api/__init__.py @@ -0,0 +1,4 @@ +from .requests import ( + get_balance_transactions, + get_balance_info +) diff --git a/queries/balance/api/requests.py b/queries/balance/api/requests.py new file mode 100644 index 0000000..1e4fa22 --- /dev/null +++ b/queries/balance/api/requests.py @@ -0,0 +1,29 @@ +import database +from ..schemas.requests import * +from ..schemas.responses import * +from ..schemas.entities import * + + +def get_balance_transactions(data: dict): + items_per_page = 5 + request = GetBalanceTransactionsRequest.model_validate(data) + page = max([request.page - 1, 0]) + query = (database.BalanceTransaction.query + .filter(database.BalanceTransaction.user_id == request.user_id) + .order_by(database.BalanceTransaction.created_at.desc()) + .offset(items_per_page * page) + .limit(items_per_page) + .all()) + balance_transactions = [BalanceTransaction.model_validate(balance_transaction) for balance_transaction in query] + response = GetBalanceTransactionsResponse(balance_transactions=balance_transactions) + return response.to_dict() + + +def get_balance_info(data: dict): + request = GetBalanceInfoRequest.model_validate(data) + balance = (database.User.query + .filter_by(id=request.user_id) + .with_entities(database.User.balance) + .scalar()) + response = GetBalanceInfoResponse(balance=balance) + return response.to_dict() diff --git a/queries/balance/schemas/__init__.py b/queries/balance/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/queries/balance/schemas/entities.py b/queries/balance/schemas/entities.py new file mode 100644 index 0000000..a907bb8 --- /dev/null +++ b/queries/balance/schemas/entities.py @@ -0,0 +1,13 @@ +from datetime import datetime + +from schemas.base import CommonModel + + +class BalanceTransaction(CommonModel): + id: int + type: int + user_id: int + amount: float + description: str + json_data: dict | None = None + created_at: datetime diff --git a/queries/balance/schemas/requests.py b/queries/balance/schemas/requests.py new file mode 100644 index 0000000..6bd7561 --- /dev/null +++ b/queries/balance/schemas/requests.py @@ -0,0 +1,10 @@ +from schemas.base import CommonModel + + +class GetBalanceTransactionsRequest(CommonModel): + page: int + user_id: int + + +class GetBalanceInfoRequest(CommonModel): + user_id: int diff --git a/queries/balance/schemas/responses.py b/queries/balance/schemas/responses.py new file mode 100644 index 0000000..9efc024 --- /dev/null +++ b/queries/balance/schemas/responses.py @@ -0,0 +1,12 @@ +from typing import List + +from schemas.base import CommonModel +from .entities import BalanceTransaction + + +class GetBalanceTransactionsResponse(CommonModel): + balance_transactions: List[BalanceTransaction] + + +class GetBalanceInfoResponse(CommonModel): + balance: float diff --git a/requirements.txt b/requirements.txt index f553fb3..af3d1fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ mariadb # Other stuff requests -python-dotenv \ No newline at end of file +python-dotenv +pydantic \ No newline at end of file diff --git a/routes/__init__.py b/routes/__init__.py index 7be1b8b..ab1d785 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -1,9 +1,10 @@ -from routes.auth import auth_blueprint -from routes.orders import orders_blueprint -from routes.barcode import barcode_blueprint -from routes.printing import printing_blueprint -from routes.assembly import assembly_blueprint -from routes.general import general_blueprint -from routes.application import application_blueprint -from routes.sipro import sipro_blueprint -from routes.admin import admin_blueprint +from .auth import auth_blueprint +from .orders import orders_blueprint +from .barcode import barcode_blueprint +from .printing import printing_blueprint +from .assembly import assembly_blueprint +from .general import general_blueprint +from .application import application_blueprint +from .sipro import sipro_blueprint +from .admin import admin_blueprint +from .balance import balance_blueprint diff --git a/routes/assembly.py b/routes/assembly.py index 7abd68c..bcc264a 100644 --- a/routes/assembly.py +++ b/routes/assembly.py @@ -8,6 +8,7 @@ import database from database.enums import AssemblyState from routes.utils import jwt_protect_blueprint import sipro.api.orders +import utils.balance assembly_blueprint = jwt_protect_blueprint(Blueprint('assembly', __name__)) @@ -86,7 +87,17 @@ def close_assembly(): assembly.ended_at = datetime.datetime.now() database.db.session.commit() order_id = assembly.order_id - return sipro.api.orders.close_order(order_id) + sipro_response = sipro.api.orders.close_order(order_id) + reward = sipro_response.get('reward') + ok = sipro_response.get('ok') + if ok: + utils.balance.add_top_up(user_id=assembly.user_id, + amount=reward, + description=f'Начисление за сборку заказа {order_id}', + json_data={'order_id': order_id}, + commit=True) + + return sipro_response @assembly_blueprint.post('/cancel') @@ -142,7 +153,6 @@ def cancel_assembly_by_id(): return jsonify(response) - @assembly_blueprint.get('/hasActive') def user_has_active_assembly(): user_id = get_jwt_identity() diff --git a/routes/balance.py b/routes/balance.py new file mode 100644 index 0000000..fe1a4d5 --- /dev/null +++ b/routes/balance.py @@ -0,0 +1,23 @@ +from flask import Blueprint, request +from flask_jwt_extended import get_jwt_identity +from routes.utils import jwt_protect_blueprint + +import queries.balance.api as api + +balance_blueprint = jwt_protect_blueprint(Blueprint('balance', __name__)) + + +@balance_blueprint.get('/transactions') +def get_transactions(): + data = dict(request.args) + if 'user_id' not in data: + data['user_id'] = get_jwt_identity() + response = api.get_balance_transactions(dict(data)) + return response + + +@balance_blueprint.get('/info') +def get_balance_info(): + data = {'user_id': get_jwt_identity()} + response = api.get_balance_info(data) + return response diff --git a/routes/general.py b/routes/general.py index a939ec7..5166b7b 100644 --- a/routes/general.py +++ b/routes/general.py @@ -1,4 +1,4 @@ -from flask import Blueprint, jsonify, request +from flask import Blueprint from routes.utils import jwt_protect_blueprint import sipro.api.general diff --git a/routes/sipro.py b/routes/sipro.py index 5ef1d34..d5b4517 100644 --- a/routes/sipro.py +++ b/routes/sipro.py @@ -1,3 +1,4 @@ +import datetime from enum import StrEnum from flask import Blueprint, request, jsonify from sqlalchemy import func @@ -127,3 +128,22 @@ def get_users(): .all()) response = [{'id': user.id, 'login': user.login} for user in query] return jsonify(response) + + +@sipro_blueprint.get('/statistics') +def get_users_statistics(): + data: dict = request.args + user_id = int(data['userId']) + date_from = datetime.datetime.fromisoformat(data['dateFrom']) + date_to = datetime.datetime.fromisoformat(data['dateTo']) + datetime.timedelta(hours=24, minutes=59, seconds=59) + + query = (database.BalanceTransaction.query + .filter(database.BalanceTransaction.user_id == user_id, + database.BalanceTransaction.created_at.between(date_from, date_to)) + .order_by(func.date_trunc('day', database.BalanceTransaction.created_at)) + .group_by(func.date_trunc('day', database.BalanceTransaction.created_at)) + .with_entities(func.date_trunc('day', database.BalanceTransaction.created_at).label('date'), + func.sum(database.BalanceTransaction.amount).label('value')) + .all()) + result = [{'date': row.date.isoformat(), 'value': row.value} for row in query] + return jsonify(result) diff --git a/scanner/enums.py b/scanner/enums.py deleted file mode 100644 index 79722af..0000000 --- a/scanner/enums.py +++ /dev/null @@ -1,13 +0,0 @@ -from enum import unique, IntEnum - - -@unique -class CodeType(IntEnum): - BARCODE = 0 - QRCODE = 1 - INVALID = 2 - - -@unique -class SearchType(IntEnum): - PRODUCT = 0 diff --git a/scanner/search.py b/scanner/search.py deleted file mode 100644 index 5b14dcb..0000000 --- a/scanner/search.py +++ /dev/null @@ -1,11 +0,0 @@ -from scanner.enums import CodeType -from scanner.utils import guess_code_type - - -class ScannerSearch: - def __init__(self, string_value: str): - self.string_value = string_value - self._code_type = guess_code_type(self.string_value) - - def get_code_type(self) -> CodeType: - return self._code_type diff --git a/scanner/utils.py b/scanner/utils.py deleted file mode 100644 index 9acef23..0000000 --- a/scanner/utils.py +++ /dev/null @@ -1,9 +0,0 @@ -from scanner.enums import CodeType - - -def guess_code_type(string_value: str) -> CodeType: - if string_value.isdigit(): - return CodeType.BARCODE - if string_value: - return CodeType.QRCODE - return CodeType.INVALID diff --git a/schemas/__init__.py b/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/schemas/base.py b/schemas/base.py new file mode 100644 index 0000000..1f23b31 --- /dev/null +++ b/schemas/base.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel +from pydantic.alias_generators import to_camel + + +class CommonConfig: + alias_generator = to_camel + populate_by_name = True + from_attributes = True + + +class CommonModel(BaseModel): + class Config(CommonConfig): + pass + + @classmethod + def json_serialize(cls, obj): + return cls.model_validate(obj).model_dump(by_alias=True) + + def to_dict(self, by_alias=True): + return self.model_dump(by_alias=by_alias) diff --git a/utils/balance.py b/utils/balance.py new file mode 100644 index 0000000..159d39e --- /dev/null +++ b/utils/balance.py @@ -0,0 +1,23 @@ +import datetime + +import database + +import database.enums + + +def add_top_up(user_id: int, + amount: float, + description: str, + json_data: dict, + commit=False): + transaction = database.BalanceTransaction( + user_id=user_id, + type=database.enums.BalanceTransactionType.TOP_UP, + amount=amount, + description=description, + json_data=json_data, + created_at=datetime.datetime.now() + ) + database.db.session.add(transaction) + if commit: + database.db.session.commit() diff --git a/utils/barcodes_synchronizer.py b/utils/barcodes_synchronizer.py deleted file mode 100644 index d79557d..0000000 --- a/utils/barcodes_synchronizer.py +++ /dev/null @@ -1,16 +0,0 @@ -import database -from database.mariadb import MariadbConnector - - -class BarcodesSynchronizer: - def __init__(self): - self.mariadb_connector = MariadbConnector() - - def synchronize(self): - existing_barcodes = database.Barcode.query.all() - denco_articles = list(set([barcode.denco_article for barcode in existing_barcodes])) - - query_string = 'SELECT product, barcode FROM modx_0_connections WHERE barcode != "" LIMIT 100' - for denco_article, barcodes_string in self.mariadb_connector.select(query_string): - barcodes = barcodes_string.split(',') - print(denco_article, barcodes)