feat: balance and reward
This commit is contained in:
1
app.py
1
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:
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
from database.models import *
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
from .models import *
|
||||
|
||||
@@ -8,3 +8,9 @@ class AssemblyState(IntEnum):
|
||||
ALL_PRODUCTS_ASSEMBLED = 2,
|
||||
CONFIRMED = 3,
|
||||
ENDED = 4
|
||||
|
||||
|
||||
@unique
|
||||
class BalanceTransactionType(IntEnum):
|
||||
TOP_UP = 0
|
||||
WITHDRAW = 1
|
||||
|
||||
2
database/models/__init__.py
Normal file
2
database/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .basic import *
|
||||
from .balance import *
|
||||
40
database/models/balance.py
Normal file
40
database/models/balance.py
Normal file
@@ -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)
|
||||
@@ -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):
|
||||
0
queries/balance/__init__.py
Normal file
0
queries/balance/__init__.py
Normal file
4
queries/balance/api/__init__.py
Normal file
4
queries/balance/api/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .requests import (
|
||||
get_balance_transactions,
|
||||
get_balance_info
|
||||
)
|
||||
29
queries/balance/api/requests.py
Normal file
29
queries/balance/api/requests.py
Normal file
@@ -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()
|
||||
0
queries/balance/schemas/__init__.py
Normal file
0
queries/balance/schemas/__init__.py
Normal file
13
queries/balance/schemas/entities.py
Normal file
13
queries/balance/schemas/entities.py
Normal file
@@ -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
|
||||
10
queries/balance/schemas/requests.py
Normal file
10
queries/balance/schemas/requests.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from schemas.base import CommonModel
|
||||
|
||||
|
||||
class GetBalanceTransactionsRequest(CommonModel):
|
||||
page: int
|
||||
user_id: int
|
||||
|
||||
|
||||
class GetBalanceInfoRequest(CommonModel):
|
||||
user_id: int
|
||||
12
queries/balance/schemas/responses.py
Normal file
12
queries/balance/schemas/responses.py
Normal file
@@ -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
|
||||
@@ -14,4 +14,5 @@ mariadb
|
||||
|
||||
# Other stuff
|
||||
requests
|
||||
python-dotenv
|
||||
python-dotenv
|
||||
pydantic
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
23
routes/balance.py
Normal file
23
routes/balance.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
0
schemas/__init__.py
Normal file
0
schemas/__init__.py
Normal file
20
schemas/base.py
Normal file
20
schemas/base.py
Normal file
@@ -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)
|
||||
23
utils/balance.py
Normal file
23
utils/balance.py
Normal file
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user