feat: implement complete CMT backend with API endpoints and test suite

- Add 7 core API endpoints: users, transactions, partners, products, inventory, payments, credit
- Implement role-based authentication (admin/write/read-only access)
- Add comprehensive database models with proper relationships
- Include full test coverage for all endpoints and business logic
- Set up Alembic migrations and Docker configuration
- Configure FastAPI app with CORS and database integration
This commit is contained in:
2025-09-14 21:04:07 +02:00
parent 49c813778b
commit c086f64363
48 changed files with 6992 additions and 126 deletions
@@ -0,0 +1,206 @@
"""Remove payment_method ENUM - Complexity not worth it when CheckConstraint can serve
Revision ID: 997376dc1774
Revises: e777b1b307b5
Create Date: 2025-08-25 23:18:53.106182
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '997376dc1774'
down_revision: Union[str, Sequence[str], None] = 'e777b1b307b5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
import sqlmodel.sql.sqltypes
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('partner',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tin_number', sa.Integer(), nullable=False),
sa.Column('names', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
sa.Column('type', sa.Enum('CLIENT', 'SUPPLIER', name='partnertype'), nullable=False),
sa.Column('phone_number', sqlmodel.sql.sqltypes.AutoString(length=10), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('tin_number')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
sa.Column('role', sa.Enum('ADMIN', 'WRITE', 'READ_ONLY', name='userrole'), nullable=False),
sa.Column('password_hash', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_table('inventory',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('total_qty', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('product_id')
)
op.create_table('transaction_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('qty', sa.Integer(), nullable=False),
sa.Column('selling_price', sa.Integer(), nullable=False),
sa.Column('total_value', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transactions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('transcation_type', sa.Enum('SALE', 'PURCHASE', 'CREDIT', name='transactiontype'), nullable=False),
sa.Column('transaction_status', sa.Enum('UNPAID', 'PARTIALLY_PAID', 'PAID', 'CANCELLED', name='transactionstatus'), nullable=False),
sa.Column('total_amount', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_on', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_on', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('credit_accounts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.Integer(), nullable=False),
sa.Column('credit_amount', sa.Integer(), nullable=False),
sa.Column('credit_limit', sa.Integer(), nullable=False),
sa.Column('balance', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['transaction_id'], ['transactions.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('partner_id')
)
op.add_column('payment', sa.Column('transaction_id', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('paid_amount', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('payment_date', sa.Date(), nullable=False))
op.add_column('payment', sa.Column('created_by', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('updated_by', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True))
op.add_column('payment', sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True))
op.alter_column('payment', 'payment_method',
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=10),
existing_nullable=False)
op.drop_constraint(op.f('payment_client_id_fkey'), 'payment', type_='foreignkey')
op.drop_constraint(op.f('payment_supplier_id_fkey'), 'payment', type_='foreignkey')
op.drop_constraint(op.f('payment_product_code_fkey'), 'payment', type_='foreignkey')
op.drop_table('credit')
op.drop_table('supplier')
op.drop_table('client')
op.create_foreign_key(None, 'payment', 'transactions', ['transaction_id'], ['id'])
op.create_foreign_key(None, 'payment', 'user', ['created_by'], ['id'])
op.create_foreign_key(None, 'payment', 'user', ['updated_by'], ['id'])
op.drop_column('payment', 'product_code')
op.drop_column('payment', 'payment_type')
op.drop_column('payment', 'date')
op.drop_column('payment', 'amount')
op.drop_column('payment', 'client_id')
op.drop_column('payment', 'supplier_id')
op.add_column('product', sa.Column('selling_price', sa.Integer(), nullable=False))
op.alter_column('product', 'date_modified',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'date_modified',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
op.drop_column('product', 'selling_price')
op.add_column('payment', sa.Column('supplier_id', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('client_id', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('amount', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True))
op.add_column('payment', sa.Column('payment_type', postgresql.ENUM('BUY', 'SELL', name='tradetype'), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('product_code', sa.VARCHAR(), autoincrement=False, nullable=False))
op.drop_constraint(None, 'payment', type_='foreignkey')
op.drop_constraint(None, 'payment', type_='foreignkey')
op.drop_constraint(None, 'payment', type_='foreignkey')
op.create_foreign_key(op.f('payment_product_code_fkey'), 'payment', 'product', ['product_code'], ['product_code'])
op.create_foreign_key(op.f('payment_supplier_id_fkey'), 'payment', 'supplier', ['supplier_id'], ['id'])
op.create_foreign_key(op.f('payment_client_id_fkey'), 'payment', 'client', ['client_id'], ['id'])
op.alter_column('payment', 'payment_method',
existing_type=sa.String(length=10),
type_=sa.VARCHAR(length=24),
existing_nullable=False)
op.drop_column('payment', 'updated_at')
op.drop_column('payment', 'created_at')
op.drop_column('payment', 'updated_by')
op.drop_column('payment', 'created_by')
op.drop_column('payment', 'payment_date')
op.drop_column('payment', 'paid_amount')
op.drop_column('payment', 'transaction_id')
op.create_table('client',
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('client_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('tin_number', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('names', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
sa.Column('phone_number', sa.VARCHAR(length=10), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name='client_pkey'),
sa.UniqueConstraint('tin_number', name='client_tin_number_key', postgresql_include=[], postgresql_nulls_not_distinct=False),
postgresql_ignore_search_path=False
)
op.create_table('credit',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('transcation_type', postgresql.ENUM('BUY', 'SELL', name='tradetype'), autoincrement=False, nullable=False),
sa.Column('product_code', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('client_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('supplier_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('qty', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('amount', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['client.id'], name=op.f('credit_client_id_fkey')),
sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], name=op.f('credit_product_code_fkey')),
sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], name=op.f('credit_supplier_id_fkey')),
sa.PrimaryKeyConstraint('id', name=op.f('credit_pkey'))
)
op.create_table('supplier',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('tin_number', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('names', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
sa.Column('phone_number', sa.VARCHAR(length=10), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('supplier_pkey')),
sa.UniqueConstraint('tin_number', name=op.f('supplier_tin_number_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
)
op.drop_table('credit_accounts')
op.drop_table('transactions')
op.drop_table('transaction_details')
op.drop_table('inventory')
op.drop_table('user')
op.drop_table('partner')
# ### end Alembic commands ###
@@ -0,0 +1,203 @@
"""Update table models to better logic & table relationship (Transaction + payment feed into credit accounts etc...)
Revision ID: a4126dbcfd9e
Revises: 4966e016dd7c
Create Date: 2025-08-25 22:25:57.071318
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import sqlmodel.sql.sqltypes
# revision identifiers, used by Alembic.
revision: str = 'a4126dbcfd9e'
down_revision: Union[str, Sequence[str], None] = '4966e016dd7c'
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('partner',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tin_number', sa.Integer(), nullable=False),
sa.Column('names', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
sa.Column('type', sa.Enum('CLIENT', 'SUPPLIER', name='partnertype'), nullable=False),
sa.Column('phone_number', sqlmodel.sql.sqltypes.AutoString(length=10), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('tin_number')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
sa.Column('role', sa.Enum('ADMIN', 'WRITE', 'READ_ONLY', name='userrole'), nullable=False),
sa.Column('password_hash', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_table('inventory',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('total_qty', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('product_id')
)
op.create_table('transaction_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('product_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('qty', sa.Integer(), nullable=False),
sa.Column('selling_price', sa.Integer(), nullable=False),
sa.Column('total_value', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transactions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('transcation_type', sa.Enum('SALE', 'PURCHASE', 'CREDIT', name='transactiontype'), nullable=False),
sa.Column('transaction_status', sa.Enum('UNPAID', 'PARTIALLY_PAID', 'PAID', 'CANCELLED', name='transactionstatus'), nullable=False),
sa.Column('total_amount', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_on', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_on', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('credit_accounts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.Integer(), nullable=False),
sa.Column('credit_amount', sa.Integer(), nullable=False),
sa.Column('credit_limit', sa.Integer(), nullable=False),
sa.Column('balance', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['transaction_id'], ['transactions.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('partner_id')
)
op.drop_table('client')
op.drop_table('supplier')
op.drop_table('credit')
op.add_column('payment', sa.Column('transaction_id', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('paid_amount', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('payment_date', sa.Date(), nullable=False))
op.add_column('payment', sa.Column('created_by', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('updated_by', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True))
op.add_column('payment', sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True))
op.alter_column('payment', 'payment_method',
existing_type=sa.VARCHAR(length=24),
type_=sa.Enum('MOMO', 'BANK', 'CASH', name='paymentmethod'),
existing_nullable=False)
op.drop_constraint(op.f('payment_supplier_id_fkey'), 'payment', type_='foreignkey')
op.drop_constraint(op.f('payment_client_id_fkey'), 'payment', type_='foreignkey')
op.drop_constraint(op.f('payment_product_code_fkey'), 'payment', type_='foreignkey')
op.create_foreign_key(None, 'payment', 'transactions', ['transaction_id'], ['id'])
op.create_foreign_key(None, 'payment', 'user', ['updated_by'], ['id'])
op.create_foreign_key(None, 'payment', 'user', ['created_by'], ['id'])
op.drop_column('payment', 'product_code')
op.drop_column('payment', 'supplier_id')
op.drop_column('payment', 'client_id')
op.drop_column('payment', 'amount')
op.drop_column('payment', 'date')
op.drop_column('payment', 'payment_type')
op.add_column('product', sa.Column('selling_price', sa.Integer(), nullable=False))
op.alter_column('product', 'date_modified',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'date_modified',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
op.drop_column('product', 'selling_price')
op.add_column('payment', sa.Column('payment_type', postgresql.ENUM('BUY', 'SELL', name='tradetype'), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True))
op.add_column('payment', sa.Column('amount', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('client_id', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('supplier_id', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('product_code', sa.VARCHAR(), autoincrement=False, nullable=False))
op.drop_constraint(None, 'payment', type_='foreignkey')
op.drop_constraint(None, 'payment', type_='foreignkey')
op.drop_constraint(None, 'payment', type_='foreignkey')
op.create_foreign_key(op.f('payment_product_code_fkey'), 'payment', 'product', ['product_code'], ['product_code'])
op.create_foreign_key(op.f('payment_client_id_fkey'), 'payment', 'client', ['client_id'], ['id'])
op.create_foreign_key(op.f('payment_supplier_id_fkey'), 'payment', 'supplier', ['supplier_id'], ['id'])
op.alter_column('payment', 'payment_method',
existing_type=sa.Enum('MOMO', 'BANK', 'CASH', name='paymentmethod'),
type_=sa.VARCHAR(length=24),
existing_nullable=False)
op.drop_column('payment', 'updated_at')
op.drop_column('payment', 'created_at')
op.drop_column('payment', 'updated_by')
op.drop_column('payment', 'created_by')
op.drop_column('payment', 'payment_date')
op.drop_column('payment', 'paid_amount')
op.drop_column('payment', 'transaction_id')
op.create_table('credit',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('transcation_type', postgresql.ENUM('BUY', 'SELL', name='tradetype'), autoincrement=False, nullable=False),
sa.Column('product_code', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('client_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('supplier_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('qty', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('amount', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['client.id'], name=op.f('credit_client_id_fkey')),
sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], name=op.f('credit_product_code_fkey')),
sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], name=op.f('credit_supplier_id_fkey')),
sa.PrimaryKeyConstraint('id', name=op.f('credit_pkey'))
)
op.create_table('supplier',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('tin_number', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('names', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
sa.Column('phone_number', sa.VARCHAR(length=10), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('supplier_pkey')),
sa.UniqueConstraint('tin_number', name=op.f('supplier_tin_number_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
)
op.create_table('client',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('tin_number', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('names', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
sa.Column('phone_number', sa.VARCHAR(length=10), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('client_pkey')),
sa.UniqueConstraint('tin_number', name=op.f('client_tin_number_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
)
op.drop_table('credit_accounts')
op.drop_table('transactions')
op.drop_table('transaction_details')
op.drop_table('inventory')
op.drop_table('user')
op.drop_table('partner')
# ### end Alembic commands ###
@@ -0,0 +1,219 @@
"""product.id to match types with product_id in transaction_detail
Revision ID: e777b1b307b5
Revises: a4126dbcfd9e
Create Date: 2025-08-25 22:34:19.869427
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import sqlmodel.sql.sqltypes
# revision identifiers, used by Alembic.
revision: str = 'e777b1b307b5'
down_revision: Union[str, Sequence[str], None] = 'a4126dbcfd9e'
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! ###
# FIRST: Remove foreign key constraints and columns from payment table
op.drop_constraint('payment_supplier_id_fkey', 'payment', type_='foreignkey')
op.drop_constraint('payment_client_id_fkey', 'payment', type_='foreignkey')
op.drop_constraint('payment_product_code_fkey', 'payment', type_='foreignkey')
op.drop_column('payment', 'product_code')
op.drop_column('payment', 'date')
op.drop_column('payment', 'amount')
op.drop_column('payment', 'payment_type')
op.drop_column('payment', 'client_id')
op.drop_column('payment', 'supplier_id')
# THEN: Drop the referenced tables (now safe)
op.drop_table('credit')
op.drop_table('supplier')
op.drop_table('client')
# Create new tables
op.create_table('partner',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tin_number', sa.Integer(), nullable=False),
sa.Column('names', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
sa.Column('type', sa.Enum('CLIENT', 'SUPPLIER', name='partnertype'), nullable=False),
sa.Column('phone_number', sqlmodel.sql.sqltypes.AutoString(length=10), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('tin_number')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
sa.Column('role', sa.Enum('ADMIN', 'WRITE', 'READ_ONLY', name='userrole'), nullable=False),
sa.Column('password_hash', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_table('inventory',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('total_qty', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('product_id')
)
op.create_table('transaction_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('qty', sa.Integer(), nullable=False),
sa.Column('selling_price', sa.Integer(), nullable=False),
sa.Column('total_value', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('transactions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('transcation_type', sa.Enum('SALE', 'PURCHASE', 'CREDIT', name='transactiontype'), nullable=False),
sa.Column('transaction_status', sa.Enum('UNPAID', 'PARTIALLY_PAID', 'PAID', 'CANCELLED', name='transactionstatus'), nullable=False),
sa.Column('total_amount', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_on', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_on', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('credit_accounts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.Integer(), nullable=False),
sa.Column('credit_amount', sa.Integer(), nullable=False),
sa.Column('credit_limit', sa.Integer(), nullable=False),
sa.Column('balance', sa.Integer(), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
sa.ForeignKeyConstraint(['partner_id'], ['partner.id'], ),
sa.ForeignKeyConstraint(['transaction_id'], ['transactions.id'], ),
sa.ForeignKeyConstraint(['updated_by'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('partner_id')
)
# FINALLY: Add new columns and constraints to payment table
op.add_column('payment', sa.Column('transaction_id', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('paid_amount', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('payment_date', sa.Date(), nullable=False))
op.add_column('payment', sa.Column('created_by', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('updated_by', sa.Integer(), nullable=False))
op.add_column('payment', sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True))
op.add_column('payment', sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True))
op.execute("CREATE TYPE paymentmethod AS ENUM ('momo', 'bank', 'cash')")
op.alter_column('payment', 'payment_method',
existing_type=sa.VARCHAR(length=24),
type_=sa.Enum('momo', 'bank', 'cash', name='paymentmethod'),
existing_nullable=False,
postgresql_using='payment_method::paymentmethod')
op.create_foreign_key(None, 'payment', 'transactions', ['transaction_id'], ['id'])
op.create_foreign_key(None, 'payment', 'user', ['created_by'], ['id'])
op.create_foreign_key(None, 'payment', 'user', ['updated_by'], ['id'])
op.add_column('product', sa.Column('selling_price', sa.Integer(), nullable=False))
op.alter_column('product', 'date_modified',
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=True,
existing_server_default=sa.text('now()'))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'date_modified',
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
existing_server_default=sa.text('now()'))
op.drop_column('product', 'selling_price')
op.add_column('payment', sa.Column('supplier_id', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('client_id', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('payment_type', postgresql.ENUM('BUY', 'SELL', name='tradetype'), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('amount', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('payment', sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True))
op.add_column('payment', sa.Column('product_code', sa.VARCHAR(), autoincrement=False, nullable=False))
op.drop_constraint(None, 'payment', type_='foreignkey')
op.drop_constraint(None, 'payment', type_='foreignkey')
op.drop_constraint(None, 'payment', type_='foreignkey')
op.create_foreign_key(op.f('payment_product_code_fkey'), 'payment', 'product', ['product_code'], ['product_code'])
op.create_foreign_key(op.f('payment_client_id_fkey'), 'payment', 'client', ['client_id'], ['id'])
op.create_foreign_key(op.f('payment_supplier_id_fkey'), 'payment', 'supplier', ['supplier_id'], ['id'])
op.alter_column('payment', 'payment_method',
existing_type=sa.Enum('MOMO', 'BANK', 'CASH', name='paymentmethod'),
type_=sa.VARCHAR(length=24),
existing_nullable=False)
op.drop_column('payment', 'updated_at')
op.drop_column('payment', 'created_at')
op.drop_column('payment', 'updated_by')
op.drop_column('payment', 'created_by')
op.drop_column('payment', 'payment_date')
op.drop_column('payment', 'paid_amount')
op.drop_column('payment', 'transaction_id')
op.create_table('client',
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('client_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('tin_number', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('names', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
sa.Column('phone_number', sa.VARCHAR(length=10), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name='client_pkey'),
sa.UniqueConstraint('tin_number', name='client_tin_number_key', postgresql_include=[], postgresql_nulls_not_distinct=False),
postgresql_ignore_search_path=False
)
op.create_table('supplier',
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('supplier_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('tin_number', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('names', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
sa.Column('phone_number', sa.VARCHAR(length=10), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name='supplier_pkey'),
sa.UniqueConstraint('tin_number', name='supplier_tin_number_key', postgresql_include=[], postgresql_nulls_not_distinct=False),
postgresql_ignore_search_path=False
)
op.create_table('credit',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('transcation_type', postgresql.ENUM('BUY', 'SELL', name='tradetype'), autoincrement=False, nullable=False),
sa.Column('product_code', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('client_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('supplier_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('qty', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('amount', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['client.id'], name=op.f('credit_client_id_fkey')),
sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], name=op.f('credit_product_code_fkey')),
sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], name=op.f('credit_supplier_id_fkey')),
sa.PrimaryKeyConstraint('id', name=op.f('credit_pkey'))
)
op.drop_table('credit_accounts')
op.drop_table('transactions')
op.drop_table('transaction_details')
op.drop_table('inventory')
op.drop_table('user')
op.drop_table('partner')
# ### end Alembic commands ###