Skip to content
Closed
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
1 change: 0 additions & 1 deletion .copier/.copier-answers.yml.jinja

This file was deleted.

26 changes: 0 additions & 26 deletions .copier/update_dotenv.py

This file was deleted.

1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
24 changes: 24 additions & 0 deletions TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Backend task for implementation

Create a backend endpoints which implements following functionality:

- Introduce a new entity Wallet and Transaction.
- Wallet should have fields: id, user_id (foreign key to User), balance (float), currency (string).
- Available currencies: USD, EUR, RUB.
- Transaction should have fields: id, wallet_id (foreign key to Wallet), amount (float), type (enum: 'credit', 'debit'), timestamp (datetime), currency (string).
- Implement endpoint to create a wallet for a user.
- Implement endpoint to get wallet details including current balance.
- Implement endpoint to create a transaction (credit or debit) for a wallet.

# Rules for wallet

- A user can have three wallets.
- Wallet balance should start at 0.0.
- Arithmetic operations on balance should be precise up to two decimal places.

# Rules for transaction

- For 'credit' transactions, the amount should be added to the wallet balance.
- For 'debit' transactions, the amount should be subtracted from the wallet balance.
- Ensure that the wallet balance cannot go negative. If a debit transaction would cause the balance to go negative, the transaction should be rejected with an appropriate error message.
- Transaction between wallets with different currencies must be converted using a fixed exchange rate (you can hardcode some exchange rates for simplicity) and fees should be applied.
Empty file added backend/app/alembic/__init__.py
Empty file.
41 changes: 19 additions & 22 deletions backend/app/alembic/env.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,39 +1,31 @@
import os
"""Alembic configuration for database migrations."""

from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config, pool
from sqlmodel import SQLModel

from app.core.config import settings

# 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.
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# target_metadata = None

from app.models import SQLModel # noqa
from app.core.config import settings # noqa
if config.config_file_name:
fileConfig(config.config_file_name)

target_metadata = SQLModel.metadata

# 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 get_url():
def get_url() -> str:
"""Get database URL from settings."""
return str(settings.SQLALCHEMY_DATABASE_URI)


def run_migrations_offline():
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.

This configures the context with just a URL
Expand All @@ -47,21 +39,24 @@ def run_migrations_offline():
"""
url = get_url()
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True
url=url,
target_metadata=target_metadata,
literal_binds=True,
compare_type=True,
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online():
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.

"""
configuration = config.get_section(config.config_ini_section)
configuration = config.get_section(config.config_ini_section) or {}
configuration["sqlalchemy.url"] = get_url()
connectable = engine_from_config(
configuration,
Expand All @@ -71,7 +66,9 @@ def run_migrations_online():

with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata, compare_type=True
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)

with context.begin_transaction():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
"""Add cascade delete relationships
"""Add cascade delete relationships.

Revision ID: 1a31ce608336
Revises: d98dd8ec85a3
Create Date: 2024-07-31 22:24:34.447891

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = '1a31ce608336'
down_revision = 'd98dd8ec85a3'
branch_labels = None
depends_on = None
revision = "1a31ce608336"
down_revision = "d98dd8ec85a3"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade():
def upgrade() -> None:
"""Upgrade database schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('item', 'owner_id',
existing_type=sa.UUID(),
nullable=False)
op.drop_constraint('item_owner_id_fkey', 'item', type_='foreignkey')
op.create_foreign_key(None, 'item', 'user', ['owner_id'], ['id'], ondelete='CASCADE')
op.alter_column(
"item",
"owner_id",
existing_type=sa.UUID(),
nullable=False,
)
op.drop_constraint("item_owner_id_fkey", "item", type_="foreignkey")
op.create_foreign_key(
None,
"item",
"user",
["owner_id"],
["id"],
ondelete="CASCADE",
)
# ### end Alembic commands ###


def downgrade():
def downgrade() -> None:
"""Downgrade database schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'item', type_='foreignkey')
op.create_foreign_key('item_owner_id_fkey', 'item', 'user', ['owner_id'], ['id'])
op.alter_column('item', 'owner_id',
existing_type=sa.UUID(),
nullable=True)
op.drop_constraint("item_owner_id_fkey", "item", type_="foreignkey")
op.create_foreign_key(
"item_owner_id_fkey",
"item",
"user",
["owner_id"],
["id"],
)
op.alter_column("item", "owner_id", existing_type=sa.UUID(), nullable=True)
# ### end Alembic commands ###
112 changes: 71 additions & 41 deletions backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,69 +1,99 @@
"""Add max length for string(varchar) fields in User and Items models
"""Add max length for string(varchar) fields in User and Items models.

Revision ID: 9c0a54914c78
Revises: e2412789c190
Create Date: 2024-06-17 14:42:44.639457

"""
from alembic import op

import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op

# Constants
STRING_FIELD_LENGTH = 255
USER_TABLE = "user"
ITEM_TABLE = "item"

# revision identifiers, used by Alembic.
revision = '9c0a54914c78'
down_revision = 'e2412789c190'
branch_labels = None
depends_on = None
revision = "9c0a54914c78"
down_revision = "e2412789c190"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade():
def upgrade() -> None:
"""Upgrade database schema."""
# Adjust the length of the email field in the User table
op.alter_column('user', 'email',
existing_type=sa.String(),
type_=sa.String(length=255),
existing_nullable=False)
op.alter_column(
USER_TABLE,
"email",
existing_type=sa.String(),
type_=sa.String(length=STRING_FIELD_LENGTH),
existing_nullable=False,
)

# Adjust the length of the full_name field in the User table
op.alter_column('user', 'full_name',
existing_type=sa.String(),
type_=sa.String(length=255),
existing_nullable=True)
op.alter_column(
USER_TABLE,
"full_name",
existing_type=sa.String(),
type_=sa.String(length=STRING_FIELD_LENGTH),
existing_nullable=True,
)

# Adjust the length of the title field in the Item table
op.alter_column('item', 'title',
existing_type=sa.String(),
type_=sa.String(length=255),
existing_nullable=False)
op.alter_column(
ITEM_TABLE,
"title",
existing_type=sa.String(),
type_=sa.String(length=STRING_FIELD_LENGTH),
existing_nullable=False,
)

# Adjust the length of the description field in the Item table
op.alter_column('item', 'description',
existing_type=sa.String(),
type_=sa.String(length=255),
existing_nullable=True)
op.alter_column(
ITEM_TABLE,
"description",
existing_type=sa.String(),
type_=sa.String(length=STRING_FIELD_LENGTH),
existing_nullable=True,
)


def downgrade():
def downgrade() -> None:
"""Downgrade database schema."""
# Revert the length of the email field in the User table
op.alter_column('user', 'email',
existing_type=sa.String(length=255),
type_=sa.String(),
existing_nullable=False)
op.alter_column(
USER_TABLE,
"email",
existing_type=sa.String(length=STRING_FIELD_LENGTH),
type_=sa.String(),
existing_nullable=False,
)

# Revert the length of the full_name field in the User table
op.alter_column('user', 'full_name',
existing_type=sa.String(length=255),
type_=sa.String(),
existing_nullable=True)
op.alter_column(
USER_TABLE,
"full_name",
existing_type=sa.String(length=STRING_FIELD_LENGTH),
type_=sa.String(),
existing_nullable=True,
)

# Revert the length of the title field in the Item table
op.alter_column('item', 'title',
existing_type=sa.String(length=255),
type_=sa.String(),
existing_nullable=False)
op.alter_column(
ITEM_TABLE,
"title",
existing_type=sa.String(length=STRING_FIELD_LENGTH),
type_=sa.String(),
existing_nullable=False,
)

# Revert the length of the description field in the Item table
op.alter_column('item', 'description',
existing_type=sa.String(length=255),
type_=sa.String(),
existing_nullable=True)
op.alter_column(
ITEM_TABLE,
"description",
existing_type=sa.String(length=STRING_FIELD_LENGTH),
type_=sa.String(),
existing_nullable=True,
)
Empty file.
Loading
Loading