init commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
@@ -0,0 +1,92 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
|
||||
from app.core.config import config as app_config
|
||||
import app.models
|
||||
|
||||
target_metadata = app.models.SQLModel.metadata
|
||||
config.set_main_option('sqlalchemy.url', app_config.DATABASE_URL)
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if config.cmd_opts.autogenerate:
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
print('No changes in schema detected.')
|
||||
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,30 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0eb89d53f0d4
|
||||
Revises: 2aac393edafe
|
||||
Create Date: 2024-11-09 21:06:11.679952
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0eb89d53f0d4'
|
||||
down_revision: Union[str, None] = '2aac393edafe'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('event', sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('event', 'name')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,78 @@
|
||||
"""init
|
||||
|
||||
Revision ID: 2aac393edafe
|
||||
Revises:
|
||||
Create Date: 2024-11-09 20:45:26.289233
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2aac393edafe'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('username', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('event',
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=False),
|
||||
sa.Column('invite', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('eventuserlink',
|
||||
sa.Column('event_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['event_id'], ['event.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('event_id', 'user_id')
|
||||
)
|
||||
op.create_table('transaction',
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('payer_id', sa.Integer(), nullable=False),
|
||||
sa.Column('event_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('closed', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['event_id'], ['event.id'], ),
|
||||
sa.ForeignKeyConstraint(['payer_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('item',
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('price', sa.Float(), nullable=False),
|
||||
sa.Column('transaction_id', sa.Uuid(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['transaction_id'], ['transaction.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('itemuserlink',
|
||||
sa.Column('item_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('item_id', 'user_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('itemuserlink')
|
||||
op.drop_table('item')
|
||||
op.drop_table('transaction')
|
||||
op.drop_table('eventuserlink')
|
||||
op.drop_table('event')
|
||||
op.drop_table('user')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,59 @@
|
||||
"""fix bigints
|
||||
|
||||
Revision ID: 3f6feff5972e
|
||||
Revises: 0eb89d53f0d4
|
||||
Create Date: 2024-11-10 11:49:31.092386
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3f6feff5972e'
|
||||
down_revision: Union[str, None] = '0eb89d53f0d4'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('event', 'owner_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
nullable=True)
|
||||
op.alter_column('eventuserlink', 'user_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
existing_nullable=False)
|
||||
op.alter_column('itemuserlink', 'user_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
existing_nullable=False)
|
||||
op.alter_column('transaction', 'payer_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
type_=sa.BigInteger(),
|
||||
nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('transaction', 'payer_id',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
nullable=False)
|
||||
op.alter_column('itemuserlink', 'user_id',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=False)
|
||||
op.alter_column('eventuserlink', 'user_id',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=False)
|
||||
op.alter_column('event', 'owner_id',
|
||||
existing_type=sa.BigInteger(),
|
||||
type_=sa.INTEGER(),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,25 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 4934e7406cd0
|
||||
Revises: 3f6feff5972e
|
||||
Create Date: 2024-11-10 11:50:17.038836
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '4934e7406cd0'
|
||||
down_revision: Union[str, None] = '3f6feff5972e'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,4 @@
|
||||
__all__: list[str] = []
|
||||
|
||||
# Initialize all routes
|
||||
from .routes import *
|
||||
@@ -0,0 +1,4 @@
|
||||
__all__: list[str] = []
|
||||
|
||||
# Initialize all routes
|
||||
from .routes import *
|
||||
@@ -0,0 +1,68 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
from fastapi.security import HTTPBearer
|
||||
import jwt
|
||||
|
||||
from app.core.config import config
|
||||
from app.models.user import User
|
||||
|
||||
from .utils import decode_jwt
|
||||
|
||||
|
||||
class BearerAuth(HTTPBearer):
|
||||
def __init__(self, auto_error: bool = True):
|
||||
super().__init__(auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request):
|
||||
credentials: HTTPAuthorizationCredentials = await super().__call__(
|
||||
request
|
||||
)
|
||||
|
||||
if credentials:
|
||||
if not credentials.scheme == 'Bearer':
|
||||
raise HTTPException(
|
||||
status_code=403, detail='Invalid authentication scheme.'
|
||||
)
|
||||
if not self.verify_jwt(credentials.credentials):
|
||||
raise HTTPException(
|
||||
status_code=403, detail='Invalid token or expired token.'
|
||||
)
|
||||
return credentials.credentials
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403, detail='Invalid authorization code.'
|
||||
)
|
||||
|
||||
def verify_jwt(self, token: str) -> bool:
|
||||
payload = decode_jwt(token)
|
||||
is_valid = bool(payload)
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
oauth2_scheme = BearerAuth()
|
||||
|
||||
|
||||
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, config.JWT_SECRET_KEY, algorithms=['HS256']
|
||||
)
|
||||
user = await User.get_or_create_user(
|
||||
User(
|
||||
id=payload['user_id'],
|
||||
username=payload['username'],
|
||||
events=[],
|
||||
items=[],
|
||||
transactions=[],
|
||||
)
|
||||
)
|
||||
return user
|
||||
except jwt.ExpiredSignatureError:
|
||||
return {'error': 'Token is expired'}
|
||||
except jwt.InvalidTokenError:
|
||||
return {'error': 'Invalid token'}
|
||||
@@ -0,0 +1,7 @@
|
||||
import fastapi
|
||||
|
||||
credentials_exception = fastapi.HTTPException(
|
||||
status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
|
||||
detail='Could not validate credentials',
|
||||
headers={'WWW-Authenticate': 'Bearer'},
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
import fastapi
|
||||
|
||||
auth_router = fastapi.APIRouter()
|
||||
@@ -0,0 +1,60 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import fastapi
|
||||
|
||||
from app.api.auth.routers import auth_router
|
||||
import app.core.security.tokens
|
||||
from app.models.base import BasicResponse
|
||||
from app.models.telegram import TelegramInputData
|
||||
from app.models.tokens import Token
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
@auth_router.post(
|
||||
'/token',
|
||||
responses={
|
||||
fastapi.status.HTTP_401_UNAUTHORIZED: {
|
||||
'description': 'Unauthorized',
|
||||
'model': BasicResponse,
|
||||
},
|
||||
},
|
||||
)
|
||||
async def authenticate(init_data: TelegramInputData) -> Token:
|
||||
# if not config.DEBUG:
|
||||
# fields = init_data.model_dump()
|
||||
# sorted_fields = sorted(fields.items())
|
||||
# formatted = [f'{key}={value}' for key, value in sorted_fields]
|
||||
# data_check_string = '\n'.join(formatted)
|
||||
|
||||
# secret_key = hmac.new(
|
||||
# config.TOKEN_TELEGRAM_API.encode(), b'WebAppData', sha256
|
||||
# ).digest()
|
||||
|
||||
# if (
|
||||
# hmac.new(
|
||||
# data_check_string.encode(), secret_key, sha256
|
||||
# ).hexdigest()
|
||||
# != init_data.hash
|
||||
# ):
|
||||
# print(hmac.new(
|
||||
# data_check_string.encode(), secret_key, sha256
|
||||
# ).hexdigest())
|
||||
# print(init_data.hash)
|
||||
# raise HTTPException(status_code=403, detail='Unauthorized')
|
||||
|
||||
user = await User.get_or_create_user(
|
||||
User(id=init_data.user.id, username=init_data.user.username)
|
||||
)
|
||||
|
||||
return Token(
|
||||
access_token=app.core.security.tokens.generate_token(
|
||||
{'user_id': user.id, 'username': user.username},
|
||||
expires_delta=timedelta(days=7),
|
||||
),
|
||||
token_type='bearer',
|
||||
)
|
||||
|
||||
|
||||
@auth_router.get('/ping')
|
||||
def ping() -> str:
|
||||
return 'pong'
|
||||
@@ -0,0 +1,14 @@
|
||||
import jwt
|
||||
|
||||
from app.core.config import config
|
||||
|
||||
|
||||
def decode_jwt(token: str) -> dict:
|
||||
try:
|
||||
decoded_token = jwt.decode(
|
||||
token, config.JWT_SECRET_KEY, algorithms=['HS256']
|
||||
)
|
||||
return decoded_token
|
||||
|
||||
except (jwt.InvalidTokenError, jwt.ExpiredSignatureError):
|
||||
return {}
|
||||
@@ -0,0 +1,4 @@
|
||||
__all__: list[str] = []
|
||||
|
||||
# Initialize all routes
|
||||
from .routes import *
|
||||
@@ -0,0 +1,3 @@
|
||||
import fastapi
|
||||
|
||||
calculate_debits_router = fastapi.APIRouter()
|
||||
@@ -0,0 +1,106 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
import pydantic
|
||||
import requests
|
||||
from sqlmodel import select
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.api.auth.deps import BearerAuth
|
||||
from app.api.auth.deps import get_current_user
|
||||
from app.core.db import engine
|
||||
from app.models.event import Event
|
||||
from app.models.user import User
|
||||
from app.utils.telegram import send_telegram_message
|
||||
|
||||
from .routers import calculate_debits_router
|
||||
|
||||
|
||||
class Debt(pydantic.BaseModel):
|
||||
from_user_id: int
|
||||
to_user_id: int
|
||||
amount: float
|
||||
|
||||
|
||||
@calculate_debits_router.post(
|
||||
'/events/{event_id}',
|
||||
description='Get debts smeta',
|
||||
dependencies=[Depends(BearerAuth())],
|
||||
)
|
||||
def calculate_event_debts(
|
||||
event_id: UUID, user: User = Depends(get_current_user)
|
||||
) -> list[Debt]:
|
||||
with Session(engine) as session:
|
||||
result = session.execute(select(Event).where(Event.id == event_id))
|
||||
event = result.scalars().first()
|
||||
|
||||
if not event:
|
||||
raise HTTPException(status_code=404, detail='Event not found')
|
||||
|
||||
if user.id != event.owner_id:
|
||||
raise HTTPException(status_code=403, detail='Permission denied')
|
||||
|
||||
transactions = []
|
||||
for transaction in event.transactions:
|
||||
if not transaction.closed:
|
||||
positions = [
|
||||
{
|
||||
'price': item.price,
|
||||
# 'assigned_to_ids': [
|
||||
# user.id for user in item.assigned_to
|
||||
# ],
|
||||
'assigned_to_ids': [user.id for user in event.users],
|
||||
}
|
||||
for item in transaction.items
|
||||
]
|
||||
transactions.append(
|
||||
{'owner_id': transaction.payer_id, 'positions': positions}
|
||||
)
|
||||
|
||||
transaction.closed = True
|
||||
|
||||
if not transactions:
|
||||
return []
|
||||
|
||||
participants = [{'id': user.id} for user in event.users]
|
||||
input_data = {
|
||||
'participants': participants,
|
||||
'transactions': transactions,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'http://optimizetka:8000/optimizetka/api/calculate-debts',
|
||||
json=input_data,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise HTTPException(
|
||||
status_code=response.status_code,
|
||||
detail=f'Failed to calculate debts: {response.text}',
|
||||
)
|
||||
|
||||
debts = response.json()
|
||||
|
||||
session.commit()
|
||||
|
||||
messages = {user.id: [] for user in event.users}
|
||||
print(debts)
|
||||
for debt in debts:
|
||||
from_user = next(
|
||||
u for u in event.users if u.id == debt['from_user_id']
|
||||
)
|
||||
to_user = next(
|
||||
u for u in event.users if u.id == debt['to_user_id']
|
||||
)
|
||||
print(from_user, to_user)
|
||||
amount = debt['amount']
|
||||
messages[from_user].append(f'You owe {amount} to {to_user.name}.')
|
||||
messages[to_user.id].append(f'{from_user.name} owes you {amount}.')
|
||||
|
||||
for user in event.users:
|
||||
message_text = '\n'.join(messages[user.id])
|
||||
if message_text:
|
||||
send_telegram_message(user.chat_id, message_text)
|
||||
|
||||
return debts
|
||||
@@ -0,0 +1,5 @@
|
||||
__all__: list[str] = []
|
||||
|
||||
# Initialize all routes
|
||||
from .invites import *
|
||||
from .routes import *
|
||||
@@ -0,0 +1,4 @@
|
||||
__all__: list[str] = []
|
||||
|
||||
# Initialize all routes
|
||||
from .routes import *
|
||||
@@ -0,0 +1,3 @@
|
||||
import fastapi
|
||||
|
||||
invites_router = fastapi.APIRouter()
|
||||
@@ -0,0 +1,44 @@
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
import fastapi
|
||||
from fastapi import HTTPException
|
||||
import sqlmodel
|
||||
|
||||
import app.api.auth.deps
|
||||
from app.api.auth.deps import BearerAuth
|
||||
from app.api.events.invites.routers import invites_router
|
||||
import app.core.db
|
||||
from app.models import Event
|
||||
import app.models.base
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
@invites_router.get(
|
||||
'/{invite_id}',
|
||||
responses={
|
||||
fastapi.status.HTTP_400_BAD_REQUEST: {
|
||||
'model': app.models.base.BasicResponse,
|
||||
'detail': 'Invalid invite',
|
||||
}
|
||||
},
|
||||
dependencies=[fastapi.Depends(BearerAuth())],
|
||||
)
|
||||
def join_by_invite(
|
||||
invite_id: uuid.UUID,
|
||||
user: typing.Annotated[
|
||||
User, fastapi.Depends(app.api.auth.deps.get_current_user)
|
||||
],
|
||||
) -> app.models.event.Event:
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
user = session.get(User, user.id)
|
||||
event = session.query(Event).filter_by(invite=invite_id).first()
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
fastapi.status.HTTP_404_NOT_FOUND, detail='Invite not found'
|
||||
)
|
||||
|
||||
event.users.append(user)
|
||||
session.commit()
|
||||
|
||||
return event
|
||||
@@ -0,0 +1,9 @@
|
||||
import fastapi
|
||||
|
||||
# from .invites.routers import invites_router
|
||||
|
||||
events_router = fastapi.APIRouter()
|
||||
|
||||
# events_router.include_router(
|
||||
# invites_router, prefix='/invites', tags=['invites']
|
||||
# )
|
||||
@@ -0,0 +1,151 @@
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
import fastapi
|
||||
import sqlmodel
|
||||
|
||||
import app.api.auth.deps
|
||||
from app.api.auth.deps import BearerAuth
|
||||
from app.api.events.routers import events_router
|
||||
import app.core.db
|
||||
from app.models import Event
|
||||
from app.models import OutputEvent
|
||||
import app.models.event
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
@events_router.get(
|
||||
'/',
|
||||
response_model=list[app.models.event.OutputEvent],
|
||||
description='Return events containing given user (by token)',
|
||||
dependencies=[fastapi.Depends(BearerAuth())],
|
||||
)
|
||||
def list_events(
|
||||
user: typing.Annotated[
|
||||
User, fastapi.Depends(app.api.auth.deps.get_current_user)
|
||||
],
|
||||
):
|
||||
output = []
|
||||
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
user = session.get(User, user.id)
|
||||
|
||||
for event in user.events:
|
||||
new_output = app.models.event.OutputEvent(
|
||||
id=event.id,
|
||||
owner=event.owner_id,
|
||||
users=event.users,
|
||||
invite=event.invite,
|
||||
name=event.name,
|
||||
)
|
||||
output.append(new_output)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
@events_router.post(
|
||||
'/add/{event_id}',
|
||||
response_model=Event,
|
||||
description='Add user to event',
|
||||
dependencies=[fastapi.Depends(BearerAuth())],
|
||||
responses={
|
||||
fastapi.status.HTTP_404_NOT_FOUND: {
|
||||
'model': app.models.base.BasicResponse,
|
||||
'detail': r'Event \ user not found',
|
||||
},
|
||||
},
|
||||
)
|
||||
async def add_to_event(
|
||||
user: typing.Annotated[
|
||||
User, fastapi.Depends(app.api.auth.deps.get_current_user)
|
||||
],
|
||||
event_id: uuid.UUID,
|
||||
):
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
event = session.get(Event, event_id)
|
||||
|
||||
if not event:
|
||||
raise fastapi.HTTPException(
|
||||
detail='Event not found', status_code=404
|
||||
)
|
||||
|
||||
user = session.get(User, user.id)
|
||||
|
||||
if not user:
|
||||
raise fastapi.HTTPException(
|
||||
detail='User not found', status_code=404
|
||||
)
|
||||
|
||||
event.users.append(user)
|
||||
|
||||
session.add(event)
|
||||
session.commit()
|
||||
session.refresh(event)
|
||||
|
||||
return event
|
||||
|
||||
|
||||
@events_router.post(
|
||||
'/',
|
||||
description='Create event',
|
||||
dependencies=[fastapi.Depends(BearerAuth())],
|
||||
)
|
||||
def create_event(
|
||||
event: app.models.event.BaseEvent,
|
||||
user: typing.Annotated[
|
||||
User, fastapi.Depends(app.api.auth.deps.get_current_user)
|
||||
],
|
||||
) -> app.models.event.Event:
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
new_event = app.models.event.Event(
|
||||
name=event.name, owner_id=user.id, users=[user]
|
||||
)
|
||||
session.add(new_event)
|
||||
session.commit()
|
||||
session.refresh(new_event)
|
||||
|
||||
return new_event
|
||||
|
||||
|
||||
# @events_router.delete(
|
||||
# '/{event_id}',
|
||||
# dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
|
||||
# description='Delete an event',
|
||||
# )
|
||||
# def delete_event(
|
||||
# event_id: uuid.UUID,
|
||||
# ):
|
||||
# with sqlmodel.Session(app.core.db.engine) as session:
|
||||
# session.delete(session.get(app.models.event.Event, event_id))
|
||||
# session.commit()
|
||||
|
||||
|
||||
@events_router.get(
|
||||
'/{event_id}',
|
||||
response_model=app.models.event.OutputEvent,
|
||||
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
|
||||
description='Get info for an event by the id',
|
||||
responses={
|
||||
fastapi.status.HTTP_404_NOT_FOUND: {
|
||||
'model': app.models.base.BasicResponse,
|
||||
'detail': r'Event \ user not found',
|
||||
},
|
||||
},
|
||||
)
|
||||
def event_by_id(event_id: uuid.UUID):
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
event = session.get(app.models.event.Event, event_id)
|
||||
|
||||
if not event:
|
||||
raise fastapi.HTTPException(
|
||||
detail='Event not found', status_code=404
|
||||
)
|
||||
|
||||
new_event = app.models.event.OutputEvent(
|
||||
id=event.id,
|
||||
owner=event.owner_id,
|
||||
users=event.users,
|
||||
invite=event.invite,
|
||||
name=event.name,
|
||||
)
|
||||
return new_event
|
||||
@@ -0,0 +1,4 @@
|
||||
__all__: list[str] = []
|
||||
|
||||
# Initialize all routes
|
||||
from .routes import *
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
|
||||
from app.api.auth.deps import BearerAuth
|
||||
|
||||
items_router = APIRouter(
|
||||
dependencies=[Depends(BearerAuth())],
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
from uuid import UUID
|
||||
|
||||
import fastapi
|
||||
import sqlmodel
|
||||
|
||||
import app.core.db
|
||||
from app.models.event import Event
|
||||
from app.models.item import Item
|
||||
from app.models.item import ItemRequest
|
||||
from app.models.item import OutputItem
|
||||
from app.models.transactions import Transaction
|
||||
|
||||
from ...models import BasicResponse
|
||||
from ..auth.deps import BearerAuth
|
||||
from .routers import items_router
|
||||
|
||||
|
||||
@items_router.get(
|
||||
'/{transaction_id}',
|
||||
description='Get items from transaction',
|
||||
responses={
|
||||
404: {
|
||||
'model': BasicResponse,
|
||||
'description': 'Transaction by this ID is not found',
|
||||
},
|
||||
403: {'model': BasicResponse, 'description': 'Unauthorized'},
|
||||
},
|
||||
)
|
||||
async def get_items(transaction_id: UUID) -> list[OutputItem]:
|
||||
with sqlmodel.Session(bind=app.core.db.engine) as session:
|
||||
transaction = session.get(Transaction, transaction_id)
|
||||
if not transaction:
|
||||
raise fastapi.HTTPException(
|
||||
detail='Transaction not found', status_code=404
|
||||
)
|
||||
|
||||
out = []
|
||||
for ti in transaction.items:
|
||||
out.append(OutputItem(
|
||||
title=ti.title,
|
||||
id=ti.id,
|
||||
price=ti.price,
|
||||
assigned_to=ti.assigned_to,
|
||||
transaction_id=ti.transaction_id
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
@items_router.post(
|
||||
'/{transaction_id}',
|
||||
description='Create item in transaction',
|
||||
responses={
|
||||
400: {
|
||||
'model': BasicResponse,
|
||||
'description': 'Invalid data format (ex. negative price)',
|
||||
},
|
||||
404: {
|
||||
'model': BasicResponse,
|
||||
'description': 'Transaction with this ID is not found',
|
||||
},
|
||||
403: {'model': BasicResponse, 'description': 'Unauthorized'},
|
||||
},
|
||||
)
|
||||
async def create_item(item: ItemRequest, transaction_id: UUID) -> OutputItem:
|
||||
if item.price <= 0:
|
||||
raise fastapi.HTTPException(
|
||||
detail='Price cannot be null or negative', status_code=400
|
||||
)
|
||||
|
||||
with sqlmodel.Session(bind=app.core.db.engine) as session:
|
||||
transaction = session.get(Transaction, transaction_id)
|
||||
if not transaction:
|
||||
raise fastapi.HTTPException(
|
||||
detail='Transaction not found', status_code=404
|
||||
)
|
||||
event = session.get(Event, transaction.event_id)
|
||||
|
||||
new_item = Item(
|
||||
title=item.title,
|
||||
price=item.price,
|
||||
assigned_to=[] if not item.all_users_selected else event.users,
|
||||
transaction_id=transaction.id,
|
||||
)
|
||||
session.add(new_item)
|
||||
session.commit()
|
||||
session.refresh(new_item)
|
||||
|
||||
return OutputItem(
|
||||
title=item.title,
|
||||
id=item.id,
|
||||
price=item.price,
|
||||
assigned_to=item.assigned_to,
|
||||
transaction_id=item.transaction_id
|
||||
)
|
||||
|
||||
|
||||
@items_router.get(
|
||||
path='/{transaction_id}/{item_id}',
|
||||
description='Get item by itself ID',
|
||||
dependencies=[fastapi.Depends(BearerAuth())],
|
||||
responses={
|
||||
404: {
|
||||
'model': BasicResponse,
|
||||
'description': 'Item with this ID is not found',
|
||||
},
|
||||
403: {'model': BasicResponse, 'description': 'Unauthorized'},
|
||||
},
|
||||
)
|
||||
async def get_item(transaction_id: UUID, item_id: UUID) -> OutputItem:
|
||||
with sqlmodel.Session(bind=app.core.db.engine) as session:
|
||||
item = session.get(Item, item_id)
|
||||
if not item:
|
||||
raise fastapi.HTTPException(
|
||||
detail='Item not found', status_code=404
|
||||
)
|
||||
|
||||
return OutputItem(
|
||||
id=item.id,
|
||||
title=item.title,
|
||||
price=item.price,
|
||||
assigned_to=item.assigned_to,
|
||||
transaction_id=transaction_id,
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
import fastapi
|
||||
|
||||
import app.api.auth.routers
|
||||
import app.api.calculate_debits.routers
|
||||
import app.api.events.routers
|
||||
import app.api.items.routers
|
||||
import app.api.transactions.routers
|
||||
import app.api.utils.routers
|
||||
|
||||
api_router = fastapi.APIRouter()
|
||||
|
||||
api_router.include_router(
|
||||
app.api.auth.routers.auth_router, prefix='/auth', tags=['auth']
|
||||
)
|
||||
api_router.include_router(
|
||||
app.api.utils.routers.utils_router, prefix='/utils', tags=['utils']
|
||||
)
|
||||
api_router.include_router(
|
||||
app.api.events.routers.events_router, prefix='/events', tags=['events']
|
||||
)
|
||||
api_router.include_router(
|
||||
app.api.transactions.routers.transactions_router,
|
||||
prefix='/transaction',
|
||||
tags=['transactions'],
|
||||
)
|
||||
api_router.include_router(
|
||||
app.api.calculate_debits.routers.calculate_debits_router,
|
||||
prefix='/calculate_debits',
|
||||
tags=['calculate_debits'],
|
||||
)
|
||||
api_router.include_router(
|
||||
app.api.items.routers.items_router, prefix='/items', tags=['items']
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
__all__: list[str] = []
|
||||
|
||||
# Initialize all routes
|
||||
from .routes import *
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
transactions_router = APIRouter()
|
||||
@@ -0,0 +1,208 @@
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
import fastapi
|
||||
import sqlmodel
|
||||
|
||||
import app.api.auth.deps
|
||||
from app.api.transactions.routers import transactions_router
|
||||
import app.core.db
|
||||
import app.models.item
|
||||
|
||||
|
||||
@transactions_router.get(
|
||||
'/{event_id}/{transaction_id}',
|
||||
response_model=app.models.transactions.OutputTransaction,
|
||||
description='Get a transaction by its event and transaction id',
|
||||
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
|
||||
responses={
|
||||
fastapi.status.HTTP_404_NOT_FOUND: {
|
||||
'model': app.models.base.BasicResponse,
|
||||
'detail': r'Event \ transaction not found',
|
||||
},
|
||||
},
|
||||
)
|
||||
async def get_transaction(
|
||||
event_id: uuid.UUID,
|
||||
transaction_id: uuid.UUID,
|
||||
):
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
event = session.get(app.models.event.Event, event_id)
|
||||
|
||||
if not event:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404, detail='Event not found'
|
||||
)
|
||||
|
||||
transaction = session.get(
|
||||
app.models.transactions.Transaction, transaction_id
|
||||
)
|
||||
output_transaction = app.models.transactions.OutputTransaction(
|
||||
title=transaction.title,
|
||||
payer=transaction.payer,
|
||||
event_id=transaction.event_id,
|
||||
closed=transaction.closed,
|
||||
items=transaction.items,
|
||||
id=transaction.id,
|
||||
)
|
||||
|
||||
return output_transaction
|
||||
|
||||
|
||||
@transactions_router.get(
|
||||
'/{event_id}',
|
||||
description='Get all transactions of an event',
|
||||
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
|
||||
responses={
|
||||
fastapi.status.HTTP_404_NOT_FOUND: {
|
||||
'model': app.models.base.BasicResponse,
|
||||
'detail': r'Event not found',
|
||||
},
|
||||
},
|
||||
)
|
||||
async def list_transactions(
|
||||
event_id: uuid.UUID,
|
||||
) -> list[app.models.transactions.OutputTransaction]:
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
event = session.get(app.models.event.Event, event_id)
|
||||
|
||||
if not event:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404, detail='Event not found'
|
||||
)
|
||||
|
||||
output = []
|
||||
|
||||
for transaction in event.transactions:
|
||||
output.append(
|
||||
app.models.transactions.OutputTransaction(
|
||||
title=transaction.title,
|
||||
payer=transaction.payer,
|
||||
event_id=transaction.event_id,
|
||||
closed=transaction.closed,
|
||||
items=transaction.items,
|
||||
id=transaction.id,
|
||||
)
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
@transactions_router.post(
|
||||
'/{event_id}',
|
||||
response_model=app.models.transactions.OutputTransaction,
|
||||
description='Create a transaction by a event id',
|
||||
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
|
||||
responses={
|
||||
fastapi.status.HTTP_404_NOT_FOUND: {
|
||||
'model': app.models.base.BasicResponse,
|
||||
'detail': 'Event not found',
|
||||
},
|
||||
},
|
||||
)
|
||||
async def create_transaction(
|
||||
title: typing.Annotated[
|
||||
str, fastapi.Body(description='Title of transaction')
|
||||
],
|
||||
event_id: uuid.UUID,
|
||||
user: typing.Annotated[
|
||||
app.models.user.User,
|
||||
fastapi.Depends(app.api.auth.deps.get_current_user),
|
||||
],
|
||||
):
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
event = session.get(app.models.event.Event, event_id)
|
||||
|
||||
if not event:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404, detail='Event not found'
|
||||
)
|
||||
|
||||
transaction = app.models.transactions.Transaction(
|
||||
title=title,
|
||||
payer_id=user.id,
|
||||
event_id=event.id,
|
||||
items=[],
|
||||
)
|
||||
|
||||
session.add(transaction)
|
||||
session.commit()
|
||||
session.refresh(transaction)
|
||||
|
||||
output_transaction = app.models.transactions.OutputTransaction(
|
||||
id=transaction.id,
|
||||
title=transaction.title,
|
||||
payer=transaction.payer,
|
||||
event_id=transaction.event_id,
|
||||
closed=transaction.closed,
|
||||
items=transaction.items,
|
||||
)
|
||||
|
||||
return output_transaction
|
||||
|
||||
|
||||
@transactions_router.post(
|
||||
'/{event_id}/{transaction_id}/items',
|
||||
response_model=app.models.item.Item,
|
||||
description='Create a transaction by a event id',
|
||||
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
|
||||
responses={
|
||||
fastapi.status.HTTP_404_NOT_FOUND: {
|
||||
'model': app.models.base.BasicResponse,
|
||||
'detail': r'Event \ transaction not found',
|
||||
},
|
||||
400: {
|
||||
'model': app.models.base.BasicResponse,
|
||||
'description': 'Invalid data format (ex. negative price)',
|
||||
},
|
||||
},
|
||||
)
|
||||
async def add_item_to_transaction(
|
||||
event_id: uuid.UUID,
|
||||
transaction_id: uuid.UUID,
|
||||
title: str = fastapi.Body(description='Title of item'),
|
||||
price: float = fastapi.Body(description='Price of item'),
|
||||
add_all_users: bool = fastapi.Body(
|
||||
description='Assign all users to this item', default=False
|
||||
),
|
||||
):
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
event = session.get(app.models.event.Event, event_id)
|
||||
|
||||
if not event:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404, detail='Event not found'
|
||||
)
|
||||
|
||||
transaction = session.get(
|
||||
app.models.transactions.Transaction, transaction_id
|
||||
)
|
||||
|
||||
if not transaction:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404, detail='Transaction not found'
|
||||
)
|
||||
|
||||
if transaction.event_id != event.id:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404,
|
||||
detail='This transaction is not a child of this event',
|
||||
)
|
||||
|
||||
item = app.models.item.Item(
|
||||
title=title,
|
||||
price=price,
|
||||
assigned_to=[] if not add_all_users else event.users,
|
||||
transaction_id=transaction_id,
|
||||
)
|
||||
|
||||
if item.price <= 0:
|
||||
raise fastapi.HTTPException(
|
||||
detail='Price cannot be null or negative', status_code=400
|
||||
)
|
||||
|
||||
session.add(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
|
||||
return item
|
||||
@@ -0,0 +1,4 @@
|
||||
__all__: list[str] = []
|
||||
|
||||
# Initialize all routes
|
||||
from .routes import *
|
||||
@@ -0,0 +1,3 @@
|
||||
import fastapi
|
||||
|
||||
utils_router = fastapi.APIRouter()
|
||||
@@ -0,0 +1,78 @@
|
||||
import fastapi
|
||||
from fastapi import HTTPException
|
||||
import sqlmodel
|
||||
|
||||
from app.api.auth.deps import BearerAuth
|
||||
from app.api.auth.deps import get_current_user
|
||||
from app.api.utils.routers import utils_router
|
||||
import app.core.db
|
||||
from app.models import BasicResponse
|
||||
from app.models import User
|
||||
from app.models.base import OfdRequest
|
||||
from app.models.event import Event
|
||||
from app.models.item import Item
|
||||
from app.models.transactions import Transaction
|
||||
from app.utils.nalog import get_nalog_data
|
||||
|
||||
|
||||
@utils_router.get('/health-check')
|
||||
def health_check() -> dict[str, str]:
|
||||
return {'msg': 'healthy'}
|
||||
|
||||
|
||||
@utils_router.post(
|
||||
'/ofd',
|
||||
description='Get items info from OFD bare string',
|
||||
dependencies=[fastapi.Depends(BearerAuth())],
|
||||
responses={
|
||||
400: {'model': BasicResponse, 'description': 'Bad OFD data'},
|
||||
403: {'model': BasicResponse, 'description': 'Unauthorized'},
|
||||
404: {'model': BasicResponse, 'description': 'Event not found'},
|
||||
500: {
|
||||
'model': BasicResponse,
|
||||
'description': 'Error while processing OFD data',
|
||||
},
|
||||
},
|
||||
)
|
||||
async def ofd(
|
||||
ofd: OfdRequest, user: User = fastapi.Depends(get_current_user)
|
||||
) -> Transaction:
|
||||
with sqlmodel.Session(bind=app.core.db.engine) as session:
|
||||
event = session.get(Event, ofd.event_id)
|
||||
if not event:
|
||||
raise HTTPException(status_code=404, detail='Event not found')
|
||||
|
||||
data = await get_nalog_data(ofd.ofd_string)
|
||||
data = data['data']['json']
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail='Bad OFD data')
|
||||
|
||||
transaction = Transaction(
|
||||
payer_id=user.id, event_id=event.id, title=data['retailPlace']
|
||||
)
|
||||
session.add(transaction)
|
||||
session.commit()
|
||||
items = data['items']
|
||||
|
||||
for item in items:
|
||||
new_item = Item(
|
||||
title=item['name'],
|
||||
price=item['sum'] / 100,
|
||||
transaction_id=transaction.id,
|
||||
)
|
||||
session.add(new_item)
|
||||
session.commit()
|
||||
|
||||
transaction.items.append(new_item)
|
||||
session.add(transaction)
|
||||
session.refresh(transaction)
|
||||
session.commit()
|
||||
|
||||
try:
|
||||
return transaction
|
||||
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
detail='Error while getting information about check',
|
||||
status_code=500,
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
import secrets
|
||||
|
||||
import pydantic
|
||||
import pydantic_settings
|
||||
|
||||
|
||||
class Config(pydantic_settings.BaseSettings):
|
||||
model_config = pydantic_settings.SettingsConfigDict(
|
||||
env_file='../.env',
|
||||
env_ignore_empty=True,
|
||||
extra='ignore',
|
||||
)
|
||||
|
||||
DATABASE_URL: str = pydantic.fields.Field(default=None)
|
||||
TOKEN_TELEGRAM_API: str = pydantic.fields.Field(default=None)
|
||||
|
||||
JWT_SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||
JWT_ALGORITHM: str = 'HS256'
|
||||
SAMPLE_PAYLOAD: dict[str, str] = pydantic.fields.Field(
|
||||
default={}, exclude=True
|
||||
)
|
||||
TOKEN_TELEGRAM_API: str = pydantic.fields.Field(default=None)
|
||||
DEBUG: bool = pydantic.fields.Field(default=False)
|
||||
|
||||
|
||||
config = Config()
|
||||
@@ -0,0 +1,20 @@
|
||||
import sqlmodel
|
||||
|
||||
from app.core.config import config
|
||||
from app.utils.factories import UserFactory
|
||||
|
||||
engine = sqlmodel.create_engine(config.DATABASE_URL)
|
||||
|
||||
|
||||
def fill_with_sample_data():
|
||||
users = [
|
||||
{'id': 0, 'username': 'petr'},
|
||||
{'id': 1, 'username': 'aleksandr'},
|
||||
{'id': 2, 'username': 'seva'},
|
||||
{'id': 3, 'username': 'maksim'},
|
||||
{'id': 4, 'username': 'timur'},
|
||||
{'id': 5, 'username': 'roman'},
|
||||
]
|
||||
UserFactory.bulk_new_users(users)
|
||||
|
||||
events = []
|
||||
@@ -0,0 +1,11 @@
|
||||
import passlib.context
|
||||
|
||||
pwd_context = passlib.context.CryptContext(schemes=['bcrypt'])
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
@@ -0,0 +1,51 @@
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import jwt
|
||||
import sqlmodel
|
||||
|
||||
from app.core.config import config
|
||||
import app.models
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def generate_token(
|
||||
payload: dict[typing.Any, typing.Any],
|
||||
expires_delta: datetime.timedelta | None = None,
|
||||
) -> str:
|
||||
to_encode = payload.copy()
|
||||
|
||||
if expires_delta:
|
||||
expire = datetime.datetime.now() + expires_delta
|
||||
|
||||
else:
|
||||
expire = datetime.datetime.now() + datetime.timedelta(minutes=15)
|
||||
|
||||
to_encode.update({'exp': expire})
|
||||
encoded_jwt = jwt.encode(
|
||||
to_encode, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM
|
||||
)
|
||||
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def user_by_token(token: str) -> User | None:
|
||||
"""Expects user_id to be in token payload"""
|
||||
|
||||
try:
|
||||
payload: dict = jwt.decode(
|
||||
token, config.JWT_SECRET_KEY, [config.JWT_ALGORITHM]
|
||||
)
|
||||
|
||||
except (jwt.InvalidTokenError, jwt.InvalidSignatureError):
|
||||
return None
|
||||
|
||||
user_id: str = payload.get('user_id')
|
||||
|
||||
if not user_id.isdigit():
|
||||
return None
|
||||
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
user = session.get(User, user_id)
|
||||
|
||||
return user
|
||||
@@ -0,0 +1,23 @@
|
||||
import fastapi
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
import app.api.routers
|
||||
|
||||
app_router = fastapi.FastAPI()
|
||||
app_router.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=['*'],
|
||||
allow_credentials=True,
|
||||
allow_methods=['*'],
|
||||
allow_headers=['*'],
|
||||
)
|
||||
|
||||
|
||||
app_router.include_router(
|
||||
app.api.routers.api_router,
|
||||
prefix='/api',
|
||||
)
|
||||
|
||||
|
||||
# async def lifespan(app: fastapi.FastAPI):
|
||||
# fill_with_sample_data()
|
||||
@@ -0,0 +1,13 @@
|
||||
__all__ = ['SQLModel']
|
||||
|
||||
# Initialize all models for SQLModel's __init_subclass__ to trigger
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from .base import *
|
||||
from .event import *
|
||||
from .item import *
|
||||
from .ofd import *
|
||||
from .telegram import *
|
||||
from .tokens import *
|
||||
from .transactions import *
|
||||
from .user import *
|
||||
@@ -0,0 +1,12 @@
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BasicResponse(BaseModel):
|
||||
detail: str
|
||||
|
||||
|
||||
class OfdRequest(BaseModel):
|
||||
ofd_string: str
|
||||
event_id: UUID
|
||||
@@ -0,0 +1,50 @@
|
||||
from uuid import UUID
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import BigInteger
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlmodel import Field
|
||||
from sqlmodel import Relationship
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.models.transactions import Transaction
|
||||
|
||||
from .links import EventUserLink
|
||||
from .user import User
|
||||
|
||||
|
||||
class BaseEvent(SQLModel):
|
||||
name: str = Field(nullable=False, default='Change name pls')
|
||||
|
||||
|
||||
class Event(BaseEvent, table=True):
|
||||
id: UUID = Field(primary_key=True, default_factory=uuid4)
|
||||
owner_id: int = Field(
|
||||
sa_column=Column(BigInteger(), ForeignKey('user.id'))
|
||||
)
|
||||
invite: str | None = Field(default_factory=uuid4)
|
||||
users: list['User'] = Relationship(
|
||||
back_populates='events', link_model=EventUserLink
|
||||
)
|
||||
transactions: list['Transaction'] = Relationship(
|
||||
back_populates='event', cascade_delete=True
|
||||
)
|
||||
|
||||
async def add_user(self, user: User):
|
||||
self.users.append(user)
|
||||
|
||||
async def add_transaction(self, transaction: Transaction):
|
||||
self.transactions.append(transaction)
|
||||
|
||||
|
||||
class OutputEvent(BaseEvent):
|
||||
id: UUID
|
||||
owner: int
|
||||
users: list[User]
|
||||
invite: str | None
|
||||
name: str
|
||||
|
||||
|
||||
class AddUserRequest(SQLModel):
|
||||
user_id: int
|
||||
@@ -0,0 +1,40 @@
|
||||
from uuid import UUID
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlmodel import Field
|
||||
from sqlmodel import Relationship
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.models.user import User
|
||||
|
||||
from .links import ItemUserLink
|
||||
|
||||
|
||||
class Item(SQLModel, table=True):
|
||||
id: UUID = Field(primary_key=True, default_factory=uuid4)
|
||||
title: str = Field(nullable=False)
|
||||
price: float = Field(nullable=False)
|
||||
|
||||
assigned_to: list['User'] = Relationship(
|
||||
back_populates='items', link_model=ItemUserLink
|
||||
)
|
||||
transaction_id: UUID = Field(foreign_key='transaction.id')
|
||||
transaction: 'Transaction' = Relationship(back_populates='items')
|
||||
|
||||
def assign_user(self, user: User):
|
||||
self.assigned_to.append(user)
|
||||
|
||||
|
||||
class OutputItem(SQLModel):
|
||||
id: UUID
|
||||
title: str
|
||||
price: float
|
||||
assigned_to: list['User']
|
||||
transaction_id: UUID
|
||||
|
||||
|
||||
class ItemRequest(SQLModel):
|
||||
title: str
|
||||
price: float
|
||||
|
||||
all_users_selected: bool = False
|
||||
@@ -0,0 +1,24 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import BigInteger
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import ForeignKey
|
||||
import sqlmodel
|
||||
|
||||
|
||||
class EventUserLink(sqlmodel.SQLModel, table=True):
|
||||
event_id: uuid.UUID | None = sqlmodel.Field(
|
||||
default=None, foreign_key='event.id', primary_key=True
|
||||
)
|
||||
user_id: int | None = sqlmodel.Field(
|
||||
sa_column=Column(BigInteger(), ForeignKey('user.id'), primary_key=True)
|
||||
)
|
||||
|
||||
|
||||
class ItemUserLink(sqlmodel.SQLModel, table=True):
|
||||
item_id: uuid.UUID | None = sqlmodel.Field(
|
||||
default=None, foreign_key='item.id', primary_key=True
|
||||
)
|
||||
user_id: int | None = sqlmodel.Field(
|
||||
sa_column=Column(BigInteger(), ForeignKey('user.id'), primary_key=True)
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OfdRequest(BaseModel):
|
||||
qr_data: str
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: int
|
||||
quantity: int
|
||||
sum: int
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
user: str
|
||||
retailPlaceAddress: str
|
||||
userInn: str
|
||||
ticketDate: str
|
||||
requestNumber: int
|
||||
shiftNumber: int
|
||||
operator: str
|
||||
operationType: int
|
||||
items: list[Item]
|
||||
nds18: int | None
|
||||
nds10: int | None
|
||||
nds0: int | None
|
||||
ndsNo: int | None
|
||||
totalSum: int
|
||||
cashTotalSum: int | None
|
||||
ecashTotalSum: int | None
|
||||
taxationType: int
|
||||
kktRegId: str
|
||||
kktNumber: str
|
||||
fiscalDriveNumber: str
|
||||
fiscalDocumentNumber: int
|
||||
fiscalSign: int
|
||||
|
||||
|
||||
class RequestManual(BaseModel):
|
||||
fn: str
|
||||
fd: str
|
||||
fp: str
|
||||
check_time: str
|
||||
type: int
|
||||
sum: float
|
||||
|
||||
|
||||
class Request(BaseModel):
|
||||
qrurl: str
|
||||
qrfile: str
|
||||
qrraw: str
|
||||
manual: RequestManual
|
||||
|
||||
|
||||
class OfdResponse(BaseModel):
|
||||
code: int
|
||||
first: int
|
||||
data: Data
|
||||
html: str
|
||||
request: Request
|
||||
@@ -0,0 +1,28 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TelegramWebUser(BaseModel):
|
||||
id: int
|
||||
first_name: str
|
||||
last_name: str | None = ''
|
||||
username: str | None = ''
|
||||
language_code: str | None = ''
|
||||
is_premium: bool = False
|
||||
added_to_attachment_menu: bool = False
|
||||
allows_write_to_pm: bool = False
|
||||
photo_url: str | None = ''
|
||||
|
||||
|
||||
class TelegramInputData(BaseModel):
|
||||
query_id: str | None = ''
|
||||
user: TelegramWebUser
|
||||
receiver: Any = ''
|
||||
chat: Any = ''
|
||||
chat_type: str | None = ''
|
||||
chat_instance: str | None = ''
|
||||
start_param: str | None = ''
|
||||
can_send_after: int | None = ''
|
||||
auth_date: int
|
||||
hash: str
|
||||
@@ -0,0 +1,6 @@
|
||||
import pydantic
|
||||
|
||||
|
||||
class Token(pydantic.BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
@@ -0,0 +1,42 @@
|
||||
from uuid import UUID
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import BigInteger
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlmodel import Field
|
||||
from sqlmodel import Relationship
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.models.item import Item
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class BaseTransaction(SQLModel):
|
||||
title: str = Field(nullable=False)
|
||||
|
||||
|
||||
class Transaction(BaseTransaction, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
payer_id: int = Field(
|
||||
sa_column=Column(BigInteger(), ForeignKey('user.id'))
|
||||
)
|
||||
payer: User = Relationship(back_populates='transactions')
|
||||
event_id: UUID = Field(foreign_key='event.id')
|
||||
event: 'Event' = Relationship(back_populates='transactions')
|
||||
closed: bool = Field(nullable=False, default=False)
|
||||
items: list['Item'] = Relationship(back_populates='transaction')
|
||||
|
||||
async def add_participant(self, participant: User):
|
||||
self.participants.append(participant)
|
||||
|
||||
async def add_item(self, item: Item):
|
||||
self.items.append(item)
|
||||
|
||||
|
||||
class OutputTransaction(BaseTransaction):
|
||||
id: UUID
|
||||
payer: User
|
||||
event_id: UUID
|
||||
closed: bool
|
||||
items: list['Item']
|
||||
@@ -0,0 +1,38 @@
|
||||
from sqlalchemy import BigInteger
|
||||
from sqlalchemy import Column
|
||||
from sqlmodel import create_engine
|
||||
from sqlmodel import Field
|
||||
from sqlmodel import Relationship
|
||||
from sqlmodel import Session
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.core.config import config
|
||||
|
||||
from .links import ItemUserLink
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
id: int = Field(sa_column=Column(BigInteger(), primary_key=True))
|
||||
username: str = Field(nullable=False)
|
||||
events: list['Event'] = Relationship(back_populates='users')
|
||||
items: list['Item'] = Relationship(
|
||||
back_populates='assigned_to', link_model=ItemUserLink
|
||||
)
|
||||
transactions: list['Transaction'] = Relationship(back_populates='payer')
|
||||
|
||||
async def get_or_create_user(self):
|
||||
engine = create_engine(url=config.DATABASE_URL)
|
||||
with Session(engine) as session:
|
||||
user = session.get(User, self.id)
|
||||
|
||||
if not user:
|
||||
user = User(
|
||||
id=self.id,
|
||||
username=self.username,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
session.refresh(user)
|
||||
|
||||
return user
|
||||
@@ -0,0 +1,6 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_token():
|
||||
pass
|
||||
@@ -0,0 +1,41 @@
|
||||
from fastapi.testclient import TestClient
|
||||
import sqlmodel
|
||||
|
||||
from app.api.events.routers import events_router
|
||||
import app.core.db
|
||||
from app.models.event import Event
|
||||
from app.tests.api.mockdata import auth_client
|
||||
from app.tests.api.mockdata import telegram_init_data
|
||||
|
||||
client = TestClient(events_router)
|
||||
|
||||
|
||||
def test_events() -> None:
|
||||
auth_response = auth_client.post('/token', json=telegram_init_data)
|
||||
assert auth_response.status_code == 200, 'Failed to authenticate'
|
||||
|
||||
token = auth_response.json().get('access_token')
|
||||
assert token is not None, 'Token not found in response'
|
||||
|
||||
headers = {'Authorization': f'Bearer {token}'}
|
||||
|
||||
event_data = {'name': 'Sample Event Name'}
|
||||
|
||||
response = client.post('/', headers=headers, json=event_data)
|
||||
response_event = response.json()
|
||||
|
||||
assert response.status_code == 200, 'Event creation failed'
|
||||
|
||||
with sqlmodel.Session(app.core.db.engine) as session:
|
||||
event = session.get(Event, response_event['id'])
|
||||
assert event.name == event_data['name'], 'Wrong name for event'
|
||||
|
||||
response = client.get(f'/{response_event["id"]}', headers=headers)
|
||||
|
||||
assert response.status_code == 200, 'Event info gathering failed'
|
||||
|
||||
response = client.get('/', headers=headers)
|
||||
response_events = response.json()
|
||||
|
||||
assert response.status_code == 200, 'Event info getting failed'
|
||||
assert response_events
|
||||
@@ -0,0 +1,25 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.api.items.routers import items_router
|
||||
|
||||
client = TestClient(items_router)
|
||||
|
||||
|
||||
def test_items() -> None:
|
||||
# create_transaction = auth_client.post('/token', json=telegram_init_data)
|
||||
# assert auth_response.status_code == 200, 'Failed to authenticate'
|
||||
#
|
||||
# token = auth_response.json().get('access_token')
|
||||
# assert token is not None, 'Token not found in response'
|
||||
|
||||
# headers = {'Authorization': f'Bearer {token}'}
|
||||
|
||||
event_data = [
|
||||
{
|
||||
'title': 'vodka',
|
||||
'price': 1000,
|
||||
'all_users_selected': False,
|
||||
},
|
||||
]
|
||||
|
||||
assert event_data
|
||||
@@ -0,0 +1,28 @@
|
||||
import fastapi.testclient
|
||||
|
||||
import app.api.auth.routers
|
||||
|
||||
auth_client = fastapi.testclient.TestClient(app.api.auth.routers.auth_router)
|
||||
|
||||
telegram_init_data = {
|
||||
'query_id': '1',
|
||||
'user': {
|
||||
'id': 0,
|
||||
'first_name': 'string',
|
||||
'last_name': '1',
|
||||
'username': '1',
|
||||
'language_code': '1',
|
||||
'is_premium': False,
|
||||
'added_to_attachment_menu': False,
|
||||
'allows_write_to_pm': False,
|
||||
'photo_url': '1',
|
||||
},
|
||||
'receiver': '1',
|
||||
'chat': '1',
|
||||
'chat_type': '1',
|
||||
'chat_instance': '1',
|
||||
'start_param': '1',
|
||||
'can_send_after': '1',
|
||||
'auth_date': 0,
|
||||
'hash': 'string',
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.api.transactions.routers import transactions_router
|
||||
|
||||
client = TestClient(transactions_router)
|
||||
|
||||
|
||||
def test_transactions() -> None:
|
||||
# create_transaction = auth_client.post('/token', json=telegram_init_data)
|
||||
# assert auth_response.status_code == 200, 'Failed to authenticate'
|
||||
#
|
||||
# token = auth_response.json().get('access_token')
|
||||
# assert token is not None, 'Token not found in response'
|
||||
|
||||
# headers = {'Authorization': f'Bearer {token}'}
|
||||
|
||||
event_data = [
|
||||
{
|
||||
'title': 'vodka',
|
||||
'price': 1000,
|
||||
'all_users_selected': False,
|
||||
},
|
||||
]
|
||||
|
||||
assert event_data
|
||||
@@ -0,0 +1,11 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.api.utils.routers import utils_router
|
||||
|
||||
client = TestClient(utils_router)
|
||||
|
||||
|
||||
def test_read_main():
|
||||
response = client.get('/health-check')
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {'msg': 'healthy'}
|
||||
@@ -0,0 +1,66 @@
|
||||
import uuid
|
||||
|
||||
import sqlmodel
|
||||
|
||||
from app.core.config import config
|
||||
from app.models.user import User
|
||||
|
||||
engine = sqlmodel.create_engine(config.DATABASE_URL)
|
||||
|
||||
|
||||
class UserFactory:
|
||||
@staticmethod
|
||||
def new_user(
|
||||
id: int,
|
||||
username: str,
|
||||
events: list['Event'] = None,
|
||||
items: list['Item'] = None,
|
||||
transactions: list['Transaction'] = None,
|
||||
):
|
||||
with sqlmodel.Session(engine) as session:
|
||||
user = User(
|
||||
id=id,
|
||||
username=username,
|
||||
events=events if events else [],
|
||||
items=items if items else [],
|
||||
transactions=transactions if transactions else [],
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def bulk_new_users(users: list[dict]):
|
||||
with sqlmodel.Session(engine) as session:
|
||||
print(session.exec(sqlmodel.select(User)).all())
|
||||
|
||||
for user in users:
|
||||
UserFactory.new_user(**user)
|
||||
|
||||
|
||||
class EventFactory:
|
||||
@staticmethod
|
||||
def new_event(
|
||||
id: int,
|
||||
owner_id: int,
|
||||
invite: uuid.UUID = None,
|
||||
users: list['User'] = None,
|
||||
transactions: list['Transaction'] = None,
|
||||
):
|
||||
with sqlmodel.Session(engine) as session:
|
||||
user = User(
|
||||
id=id,
|
||||
username=username,
|
||||
events=events,
|
||||
items=items,
|
||||
transactions=transactions,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def bulk_new_users(users: list[dict]):
|
||||
with sqlmodel.Session(engine) as session:
|
||||
print(session.exec(sqlmodel.select(User)).all())
|
||||
|
||||
for user in users:
|
||||
UserFactory.new_user(**user)
|
||||
@@ -0,0 +1,28 @@
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
|
||||
async def get_nalog_data(ofd_string: str) -> dict:
|
||||
async with ClientSession() as session:
|
||||
url = 'https://proverkacheka.com/api/v1/check/get'
|
||||
data = {'qrraw': ofd_string, 'token': '{{sensitive_data}}'}
|
||||
response = await session.post(
|
||||
url=url,
|
||||
data=data,
|
||||
headers={
|
||||
'Content-Type': (
|
||||
'application/x-www-form-urlencoded;' ' charset=UTF-8'
|
||||
)
|
||||
},
|
||||
)
|
||||
if response.status != 200:
|
||||
logging.error('Received non-200 status on ofd check')
|
||||
return
|
||||
data = await response.json()
|
||||
if data['code'] == 1:
|
||||
return data
|
||||
logging.error(
|
||||
f'Received non-success status on ofd request with data: {data}'
|
||||
)
|
||||
return
|
||||
@@ -0,0 +1,16 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from app.core.config import config
|
||||
|
||||
TELEGRAM_BOT_TOKEN = config.TOKEN_TELEGRAM_API
|
||||
|
||||
|
||||
def send_telegram_message(chat_id: int, text: str):
|
||||
url = f'https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage'
|
||||
payload = {'chat_id': chat_id, 'text': text}
|
||||
response = requests.post(url, json=payload)
|
||||
|
||||
if response.status_code != 200:
|
||||
logging.error(f'Failed to send message to {chat_id}: {response.text}')
|
||||
Reference in New Issue
Block a user