From 648448ebdc482e83efbebd72996dadd34c3e0eb7 Mon Sep 17 00:00:00 2001 From: LinMihigo Date: Sat, 23 Aug 2025 09:16:37 +0200 Subject: [PATCH] remodeling table models and migration to postgres --- .gitignore | 1 + .vscode/settings.json | 14 ++- README.md | 20 +--- SYSTEM-DESIGN.md | 2 + backend/BACKEND.md | 28 +++++ backend/README.md | 12 -- backend/alembic.ini | 111 +----------------- backend/app/alembic/env.py | 10 +- backend/app/alembic/script.py.mako | 2 +- ...ates.py => 0aa4734ce008_initial_tables.py} | 12 +- ..._date_modified_columns_add_default_none.py | 32 ----- ...uild.py => 4966e016dd7c_initial_tables.py} | 23 ++-- .../8aefa882e096_product_id_made_nullable.py | 38 ------ ...product_id_made_nullable_using_default_.py | 32 ----- ...500_fix_client_id_in_credit_type_to_int.py | 60 ---------- ...e_making_product_date_modified_default_.py | 32 ----- backend/app/core/auth.py | 0 backend/app/schemas/base.py | 26 ++++ backend/app/schemas/models.py | 102 +++++++++------- backend/requirements.txt | 10 ++ 20 files changed, 166 insertions(+), 401 deletions(-) create mode 100644 SYSTEM-DESIGN.md create mode 100644 backend/BACKEND.md delete mode 100644 backend/README.md rename backend/app/alembic/versions/{e8c4300db3cb_checking_for_updates.py => 0aa4734ce008_initial_tables.py} (74%) delete mode 100644 backend/app/alembic/versions/174e0494276d_date_modified_columns_add_default_none.py rename backend/app/alembic/versions/{5840d2b52dd8_rebuild.py => 4966e016dd7c_initial_tables.py} (87%) delete mode 100644 backend/app/alembic/versions/8aefa882e096_product_id_made_nullable.py delete mode 100644 backend/app/alembic/versions/b5ff3e70bd95_product_id_made_nullable_using_default_.py delete mode 100644 backend/app/alembic/versions/bfb086d8d500_fix_client_id_in_credit_type_to_int.py delete mode 100644 backend/app/alembic/versions/d89dba0432de_making_product_date_modified_default_.py create mode 100644 backend/app/core/auth.py create mode 100644 backend/app/schemas/base.py diff --git a/.gitignore b/.gitignore index c1cd679..53918c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/.pytest_cache **/venv **/.env +**/.github diff --git a/.vscode/settings.json b/.vscode/settings.json index 68d96d1..d96979f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,17 @@ "**/*.key", "**/*.pem" ] - } + }, + "sqltools.connections": [ + { + "previewLimit": 50, + "server": "localhost", + "port": 5432, + "askForPassword": true, + "driver": "PostgreSQL", + "database": "cmt_db", + "username": "admin", + "name": "CMT" + } + ] } diff --git a/README.md b/README.md index 6110257..4e2d4fb 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ Postgresql ```bash # Installation - Arch sudo pacman -Syu postgresql +sudo dnf install postgresql-server postgresql-contrib # fedora # Initialising db cluster -sudo -u postgres initdb -D /var/lib/postgres/data +sudo -u postgres initdb -D /var/lib/postgres/data # Arch +sudo postgresql-setup --initdb # fedora # enable + start service sudo systemctl enable --now postgresql @@ -28,20 +30,6 @@ sudo -u postgres createuser -P appuser # Creating db owned by this user sudo -u postgres createdb -O appuser db_name -# Test +# Test user + db creation psql "postgresql://appuser:secret@localhost:5432/appdb" ``` -### Testing -``` -cd backend -pytest app/test.py - -# Curl POST command -curl -X POST "http://localhost:8000/clients/" -H "Content-Type: application/json" -d '{"tin_number": 100752121, "names": "Pax au Telemanus", "phone_number": "0788475021"}' - -# Trying updating client details -curl -X PATCH "http://localhost:8000/clients/1" -H "Content-Type: application/json" -d '{"names": "John Wick"}' - -# Deletion -curl -X DELETE http://localhost:8000/clients/2 -``` diff --git a/SYSTEM-DESIGN.md b/SYSTEM-DESIGN.md new file mode 100644 index 0000000..670d564 --- /dev/null +++ b/SYSTEM-DESIGN.md @@ -0,0 +1,2 @@ +### LOGIC +- forms on the frontend diff --git a/backend/BACKEND.md b/backend/BACKEND.md new file mode 100644 index 0000000..b12d139 --- /dev/null +++ b/backend/BACKEND.md @@ -0,0 +1,28 @@ +# CMT Backend +## Usage +### API +### Alembic +```bash +# updating changes to table models to the db +cd backend +alembic revision --autogenerate -m "Header message" +alembic upgrade head + +# Forcing alembic DB is up-to-date without actually running the migration +alembic stamp head +``` + +### Testing +``` +cd backend +pytest app/test.py + +# Curl POST command +curl -X POST "http://localhost:8000/clients/" -H "Content-Type: application/json" -d '{"tin_number": 100752121, "names": "Pax au Telemanus", "phone_number": "0788475021"}' + +# Trying updating client details +curl -X PATCH "http://localhost:8000/clients/1" -H "Content-Type: application/json" -d '{"names": "John Wick"}' + +# Deletion +curl -X DELETE http://localhost:8000/clients/2 +``` diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index c7aed3d..0000000 --- a/backend/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# CMT Backend -## Usage -### API -### Alembic -```bash -cd backend -alembic revision --autogenerate -m "Header message" -alembic upgrade head - -# Making alembic DB is up-to-date without actually running the migration -alembic stamp head -``` diff --git a/backend/alembic.ini b/backend/alembic.ini index b576264..dab55c4 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -1,111 +1,7 @@ -# A generic, single database configuration. - [alembic] -# path to migration scripts. -# this is typically a path given in POSIX (e.g. forward slashes) -# format, relative to the token %(here)s which refers to the location of this -# ini file script_location = app/alembic +# The sqlalchemy.url is ignored; set in env.py via config.settings -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. for multiple paths, the path separator -# is defined by "path_separator" below. -prepend_sys_path = . - - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. -# Any required deps can installed by adding `alembic[tz]` to the pip requirements -# string value is passed to ZoneInfo() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to /versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "path_separator" -# below. -# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions - -# path_separator; This indicates what character is used to split lists of file -# paths, including version_locations and prepend_sys_path within configparser -# files such as alembic.ini. -# The default rendered in new alembic.ini files is "os", which uses os.pathsep -# to provide os-dependent path splitting. -# -# Note that in order to support legacy alembic.ini files, this default does NOT -# take place if path_separator is not present in alembic.ini. If this -# option is omitted entirely, fallback logic is as follows: -# -# 1. Parsing of the version_locations option falls back to using the legacy -# "version_path_separator" key, which if absent then falls back to the legacy -# behavior of splitting on spaces and/or commas. -# 2. Parsing of the prepend_sys_path option falls back to the legacy -# behavior of splitting on spaces, commas, or colons. -# -# Valid values for path_separator are: -# -# path_separator = : -# path_separator = ; -# path_separator = space -# path_separator = newline -# -# Use os.pathsep. Default configuration used for new projects. -path_separator = os - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -# database URL. This is consumed by the user-maintained env.py script only. -# other means of configuring database URLs may be customized within the env.py -# file. -#sqlalchemy.url = driver://user:pass@localhost/dbname - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# lint with attempts to fix using "ruff" - use the exec runner, execute a binary -# hooks = ruff -# ruff.type = exec -# ruff.executable = %(here)s/.venv/bin/ruff -# ruff.options = check --fix REVISION_SCRIPT_FILENAME - -# Logging configuration. This is also consumed by the user-maintained -# env.py script only. [loggers] keys = root,sqlalchemy,alembic @@ -116,12 +12,12 @@ keys = console keys = generic [logger_root] -level = WARNING +level = WARN handlers = console qualname = [logger_sqlalchemy] -level = WARNING +level = WARN handlers = qualname = sqlalchemy.engine @@ -138,4 +34,3 @@ formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/backend/app/alembic/env.py b/backend/app/alembic/env.py index 84e37e3..aa7c924 100644 --- a/backend/app/alembic/env.py +++ b/backend/app/alembic/env.py @@ -1,18 +1,16 @@ from logging.config import fileConfig +from app.schemas.models import SQLModel from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context -import os -from dotenv import load_dotenv -from sqlmodel import SQLModel -from app.schemas.models import Client, Supplier, Product, Payment, Credit +from app.core.config import settings -load_dotenv() # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config +config.set_main_option('sqlalchemy.url', str(settings.database_uri)) # type: ignore # Interpret the config file for Python logging. # This line sets up loggers basically. @@ -24,7 +22,6 @@ if config.config_file_name is not None: # from myapp import mymodel # target_metadata = mymodel.Base.metadata target_metadata = SQLModel.metadata -url = os.getenv("DATABASE_URI") # other values from the config, defined by the needs of env.py, # can be acquired: @@ -66,7 +63,6 @@ def run_migrations_online() -> None: connectable = engine_from_config( config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", - url=url, poolclass=pool.NullPool, ) diff --git a/backend/app/alembic/script.py.mako b/backend/app/alembic/script.py.mako index 480b130..1101630 100644 --- a/backend/app/alembic/script.py.mako +++ b/backend/app/alembic/script.py.mako @@ -13,7 +13,7 @@ ${imports if imports else ""} # revision identifiers, used by Alembic. revision: str = ${repr(up_revision)} -down_revision: Union[str, None] = ${repr(down_revision)} +down_revision: Union[str, Sequence[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)} diff --git a/backend/app/alembic/versions/e8c4300db3cb_checking_for_updates.py b/backend/app/alembic/versions/0aa4734ce008_initial_tables.py similarity index 74% rename from backend/app/alembic/versions/e8c4300db3cb_checking_for_updates.py rename to backend/app/alembic/versions/0aa4734ce008_initial_tables.py index f13c400..d774bf3 100644 --- a/backend/app/alembic/versions/e8c4300db3cb_checking_for_updates.py +++ b/backend/app/alembic/versions/0aa4734ce008_initial_tables.py @@ -1,8 +1,8 @@ -"""Checking for updates +"""Initial tables -Revision ID: e8c4300db3cb -Revises: bfb086d8d500 -Create Date: 2025-06-08 19:06:55.200977 +Revision ID: 0aa4734ce008 +Revises: +Create Date: 2025-08-17 16:44:05.785214 """ from typing import Sequence, Union @@ -12,8 +12,8 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = 'e8c4300db3cb' -down_revision: Union[str, None] = 'bfb086d8d500' +revision: str = '0aa4734ce008' +down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/backend/app/alembic/versions/174e0494276d_date_modified_columns_add_default_none.py b/backend/app/alembic/versions/174e0494276d_date_modified_columns_add_default_none.py deleted file mode 100644 index 04a46ed..0000000 --- a/backend/app/alembic/versions/174e0494276d_date_modified_columns_add_default_none.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Date_modified columns - add default=None - -Revision ID: 174e0494276d -Revises: d89dba0432de -Create Date: 2025-06-15 20:42:30.850962 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '174e0494276d' -down_revision: Union[str, None] = 'd89dba0432de' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/5840d2b52dd8_rebuild.py b/backend/app/alembic/versions/4966e016dd7c_initial_tables.py similarity index 87% rename from backend/app/alembic/versions/5840d2b52dd8_rebuild.py rename to backend/app/alembic/versions/4966e016dd7c_initial_tables.py index a9f7cc7..4f40a10 100644 --- a/backend/app/alembic/versions/5840d2b52dd8_rebuild.py +++ b/backend/app/alembic/versions/4966e016dd7c_initial_tables.py @@ -1,20 +1,19 @@ -"""Rebuild +"""Initial tables -Revision ID: 5840d2b52dd8 -Revises: -Create Date: 2025-06-01 14:27:25.657473 +Revision ID: 4966e016dd7c +Revises: 0aa4734ce008 +Create Date: 2025-08-17 16:50:53.587969 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa -import sqlmodel - +import sqlmodel.sql.sqltypes # revision identifiers, used by Alembic. -revision: str = '5840d2b52dd8' -down_revision: Union[str, None] = None +revision: str = '4966e016dd7c' +down_revision: Union[str, Sequence[str], None] = '0aa4734ce008' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -52,8 +51,8 @@ def upgrade() -> None: sa.Column('id', sa.Integer(), nullable=False), sa.Column('transcation_type', sa.Enum('BUY', 'SELL', name='tradetype'), nullable=False), sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('client_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('supplier_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('client_id', sa.Integer(), nullable=False), + sa.Column('supplier_id', sa.Integer(), nullable=False), sa.Column('qty', sa.Integer(), nullable=False), sa.Column('amount', sa.Integer(), nullable=False), sa.Column('date', sa.DateTime(), server_default=sa.text('now()'), nullable=True), @@ -66,8 +65,8 @@ def upgrade() -> None: sa.Column('id', sa.Integer(), nullable=False), sa.Column('payment_type', sa.Enum('BUY', 'SELL', name='tradetype'), nullable=False), sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('client_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('supplier_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('client_id', sa.Integer(), nullable=False), + sa.Column('supplier_id', sa.Integer(), nullable=False), sa.Column('amount', sa.Integer(), nullable=False), sa.Column('payment_method', sqlmodel.sql.sqltypes.AutoString(length=24), nullable=False), sa.Column('date', sa.DateTime(), server_default=sa.text('now()'), nullable=True), diff --git a/backend/app/alembic/versions/8aefa882e096_product_id_made_nullable.py b/backend/app/alembic/versions/8aefa882e096_product_id_made_nullable.py deleted file mode 100644 index 66ac8f5..0000000 --- a/backend/app/alembic/versions/8aefa882e096_product_id_made_nullable.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Product id made nullable - -Revision ID: 8aefa882e096 -Revises: e8c4300db3cb -Create Date: 2025-06-15 19:33:39.299803 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision: str = '8aefa882e096' -down_revision: Union[str, None] = 'e8c4300db3cb' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('product', 'id', - existing_type=mysql.INTEGER(), - nullable=True, - autoincrement=True) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('product', 'id', - existing_type=mysql.INTEGER(), - nullable=False, - autoincrement=True) - # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/b5ff3e70bd95_product_id_made_nullable_using_default_.py b/backend/app/alembic/versions/b5ff3e70bd95_product_id_made_nullable_using_default_.py deleted file mode 100644 index b794380..0000000 --- a/backend/app/alembic/versions/b5ff3e70bd95_product_id_made_nullable_using_default_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Product id made nullable using 'default=None' - -Revision ID: b5ff3e70bd95 -Revises: 8aefa882e096 -Create Date: 2025-06-15 19:38:20.874456 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'b5ff3e70bd95' -down_revision: Union[str, None] = '8aefa882e096' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/bfb086d8d500_fix_client_id_in_credit_type_to_int.py b/backend/app/alembic/versions/bfb086d8d500_fix_client_id_in_credit_type_to_int.py deleted file mode 100644 index b94ba87..0000000 --- a/backend/app/alembic/versions/bfb086d8d500_fix_client_id_in_credit_type_to_int.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Fix client_id in Credit type to int - -Revision ID: bfb086d8d500 -Revises: 5840d2b52dd8 -Create Date: 2025-06-01 14:53:57.095181 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import sqlmodel - -# revision identifiers, used by Alembic. -revision: str = 'bfb086d8d500' -down_revision: Union[str, None] = '5840d2b52dd8' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('credit', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('transcation_type', sa.Enum('BUY', 'SELL', name='tradetype'), nullable=False), - sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('client_id', sa.Integer(), nullable=False), - sa.Column('supplier_id', sa.Integer(), nullable=False), - sa.Column('qty', sa.Integer(), nullable=False), - sa.Column('amount', sa.Integer(), nullable=False), - sa.Column('date', sa.DateTime(), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['client.id'], ), - sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], ), - sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('payment', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('payment_type', sa.Enum('BUY', 'SELL', name='tradetype'), nullable=False), - sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('client_id', sa.Integer(), nullable=False), - sa.Column('supplier_id', sa.Integer(), nullable=False), - sa.Column('amount', sa.Integer(), nullable=False), - sa.Column('payment_method', sqlmodel.sql.sqltypes.AutoString(length=24), nullable=False), - sa.Column('date', sa.DateTime(), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['client.id'], ), - sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], ), - sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], ), - sa.PrimaryKeyConstraint('id') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('payment') - op.drop_table('credit') - # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/d89dba0432de_making_product_date_modified_default_.py b/backend/app/alembic/versions/d89dba0432de_making_product_date_modified_default_.py deleted file mode 100644 index cae31d9..0000000 --- a/backend/app/alembic/versions/d89dba0432de_making_product_date_modified_default_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Making Product.date_modified default=None - -Revision ID: d89dba0432de -Revises: b5ff3e70bd95 -Create Date: 2025-06-15 20:06:11.734486 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'd89dba0432de' -down_revision: Union[str, None] = 'b5ff3e70bd95' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/base.py b/backend/app/schemas/base.py new file mode 100644 index 0000000..6ae9c95 --- /dev/null +++ b/backend/app/schemas/base.py @@ -0,0 +1,26 @@ +from sqlmodel import SQLModel +from enum import Enum + + +class UserRole(str, Enum): + ADMIN = "admin" + WRITE = "write" + READ_ONLY = "read_only" + + +class TransactionType(str, Enum): + SALE = "sell" + PURCHASE = "buy" + CREDIT = "credit" + + +class TransactionStatus(str, Enum): + UNPAID = "unpaid" + PARTIALLY_PAID = "partially_paid" + PAID = "paid" + CANCELLED = 'cancelled' + + +class PartnerType(str, Enum): + CLIENT = "client" + SUPPLIER = "supplier" diff --git a/backend/app/schemas/models.py b/backend/app/schemas/models.py index d1f6c7a..729131c 100644 --- a/backend/app/schemas/models.py +++ b/backend/app/schemas/models.py @@ -18,86 +18,100 @@ from datetime import datetime from sqlalchemy import Column, DateTime, func, Enum as SQLEnum from enum import Enum from typing import Optional +from base import UserRole, PartnerType, TransactionType, TransactionStatus -class TradeType(str, Enum): - BUY = "Buy" - SELL = "Sell" +class User(SQLModel, table=True): + """ + User table mapping, api response validation and serialisation + """ + id: Optional[int] = Field(default=None, primary_key=True) + username: str = Field(nullable=False,unique=True, max_length=100) + role: UserRole = Field(nullable=True, default=PartnerType.CLIENT) + password_hash: str = Field(nullable=False) - -class Client(SQLModel, table=True): +class Partner(SQLModel, table=True): """Clients table mapping, api response validation and serialisation""" id: Optional[int] = Field(default=None, primary_key=True) tin_number: int = Field(nullable=False, unique=True) names: str = Field(max_length=100, nullable=False) - phone_number: str = Field(max_length=10, nullable=False) - - -class Supplier(SQLModel, table=True): - """Supplier table mapping, api response validation and serialisation""" - id: Optional[int] = Field(default=None, primary_key=True) - tin_number: int = Field(nullable=False, unique=True) - names: str = Field(max_length=100, nullable=False) - phone_number: str = Field(max_length=10, nullable=False) + type: PartnerType = Field(nullable=False, default=PartnerType.CLIENT) + phone_number: str = Field(max_length=10, nullable=True) class Product(SQLModel, table=True): """Products table mapping, api response validation and serialisation - NOTE: purchase price should update every time a supplier credits us goods - and price has changed + NOTE: Every time a product's purchase price changes, it should be updated + here as well """ - __table_args__ = (UniqueConstraint("product_code"),) + __table_args__ = (UniqueConstraint("product_code")) id: Optional[int] = Field(default=None, primary_key=True) - product_code: str = Field(max_length=10, nullable=False) + product_code: str = Field(max_length=10, unique=True, nullable=False) product_name: str = Field(max_length=20, nullable=False, unique=True) purchase_price: int = Field(nullable=False) date_modified: datetime = Field( default=None, - sa_column=Column(DateTime, + sa_column=Column(DateTime(timezone=True), server_default=func.now(), server_onupdate=func.now()) ) +class Transaction(SQLModel, table=True): + """ + Transaction table mapping, api response validation and serialisation + + Include both business events to/from suppliers and to/from clients + """ + __tablename__: str = "transactions" + id: Optional[int] = Field(default=None, primary_key=True) + partner_id: Optional[int] = Field(nullable=False, foreign_key="partner.id") + transcation_type: TransactionType = Field( + sa_column=Column(SQLEnum(TransactionType), nullable=False) + ) + transaction_status: TransactionStatus + created_on: datetime = Field( + default=None, + sa_column=Column(DateTime(timezone=True), server_default=func.now()) + ) + updated_on: datetime = Field( + default=None, + sa_column=Column( + DateTime(timezone=True), + onupdate=func.now(), + server_default=func.now() + ) + ) + + +class Transaction_items(SQLModel, table=True): + """ + Transaction table mapping, api response validation and serialisation + Includes transactions details from transactions + """ + + class Payment(SQLModel, table=True): """ - Payments table mapping, api response validation and serialisation - - Include both payments to suppliers and from clients + """ - id: Optional[int] = Field(default=None, primary_key=True) - payment_type: TradeType = Field( - sa_column=Column(SQLEnum(TradeType), nullable=False) - ) - product_code: str = Field(nullable=False, foreign_key="product.product_code") - client_id: Optional[int] = Field(nullable=False, foreign_key="client.id") - supplier_id: Optional[int] = Field(nullable=False, foreign_key="supplier.id") - amount: int = Field(nullable=False) - payment_method: str = Field(max_length=24, nullable=False) - date: datetime = Field( - default=None, - sa_column=Column(DateTime, server_default=func.now()) - ) - - -class Credit(SQLModel, table=True): +class Credit_accounts(SQLModel, table=True): """Credit table mapping, api response validation and serialisation Include both credit from suppliers and to clients """ + __tablename__: str = "credit_accounts" + id: Optional[int] = Field(default=None, primary_key=True) - transcation_type: TradeType = Field( - sa_column=Column(SQLEnum(TradeType), nullable=False) - ) product_code: str = Field(nullable=False, foreign_key="product.product_code") - client_id: Optional[int] = Field(nullable=False, foreign_key="client.id") - supplier_id: Optional[int] = Field(nullable=False, foreign_key="supplier.id") + client_id: Optional[int] = Field(nullable=True, foreign_key="client.id") + supplier_id: Optional[int] = Field(nullable=True, foreign_key="supplier.id") qty: int = Field(nullable=False) amount: int = Field(nullable=False) date: datetime = Field( default=None, - sa_column=Column(DateTime, server_default=func.now()) + sa_column=Column(DateTime(timezone=True), server_default=func.now()) ) diff --git a/backend/requirements.txt b/backend/requirements.txt index e7561cd..5c3cb19 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,15 +1,25 @@ alembic==1.16.4 annotated-types==0.7.0 anyio==4.10.0 +asyncpg==0.30.0 fastapi==0.116.1 greenlet==3.2.4 idna==3.10 +iniconfig==2.1.0 Mako==1.3.10 MarkupSafe==3.0.2 +packaging==25.0 +pluggy==1.6.0 +psycopg2-binary==2.9.10 pydantic==2.11.7 +pydantic-settings==2.10.1 pydantic_core==2.33.2 +Pygments==2.19.2 +pytest==8.4.1 +python-dotenv==1.1.1 sniffio==1.3.1 SQLAlchemy==2.0.43 +sqlmodel==0.0.24 starlette==0.47.2 typing-inspection==0.4.1 typing_extensions==4.14.1