From 9e07470511682cd331bcf912616672263f64d082 Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Fri, 19 Sep 2025 10:09:33 +0330 Subject: [PATCH] start working on business --- hesabixAPI/adapters/db/models/__init__.py | 1 + hesabixAPI/adapters/db/models/business.py | 55 +++++++ .../adapters/db/repositories/business_repo.py | 143 ++++++++++++++++++ hesabixAPI/hesabix_api.egg-info/PKG-INFO | 1 + hesabixAPI/hesabix_api.egg-info/SOURCES.txt | 7 +- hesabixAPI/hesabix_api.egg-info/requires.txt | 1 + .../20250117_000003_add_business_table.py | 40 +++++ ...0117_000004_add_business_contact_fields.py | 40 +++++ ...7_000005_add_business_geographic_fields.py | 26 ++++ 9 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 hesabixAPI/adapters/db/models/business.py create mode 100644 hesabixAPI/adapters/db/repositories/business_repo.py create mode 100644 hesabixAPI/migrations/versions/20250117_000003_add_business_table.py create mode 100644 hesabixAPI/migrations/versions/20250117_000004_add_business_contact_fields.py create mode 100644 hesabixAPI/migrations/versions/20250117_000005_add_business_geographic_fields.py diff --git a/hesabixAPI/adapters/db/models/__init__.py b/hesabixAPI/adapters/db/models/__init__.py index af4b100..ad0fe75 100644 --- a/hesabixAPI/adapters/db/models/__init__.py +++ b/hesabixAPI/adapters/db/models/__init__.py @@ -5,5 +5,6 @@ from .user import User # noqa: F401 from .api_key import ApiKey # noqa: F401 from .captcha import Captcha # noqa: F401 from .password_reset import PasswordReset # noqa: F401 +from .business import Business # noqa: F401 diff --git a/hesabixAPI/adapters/db/models/business.py b/hesabixAPI/adapters/db/models/business.py new file mode 100644 index 0000000..aeee6cd --- /dev/null +++ b/hesabixAPI/adapters/db/models/business.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from datetime import datetime +from enum import Enum + +from sqlalchemy import String, DateTime, Integer, ForeignKey, Enum as SQLEnum, Text +from sqlalchemy.orm import Mapped, mapped_column + +from adapters.db.session import Base + + +class BusinessType(str, Enum): + """نوع کسب و کار""" + COMPANY = "شرکت" # شرکت + SHOP = "مغازه" # مغازه + STORE = "فروشگاه" # فروشگاه + UNION = "اتحادیه" # اتحادیه + CLUB = "باشگاه" # باشگاه + INSTITUTE = "موسسه" # موسسه + INDIVIDUAL = "شخصی" # شخصی + + +class BusinessField(str, Enum): + """زمینه فعالیت کسب و کار""" + MANUFACTURING = "تولیدی" # تولیدی + TRADING = "بازرگانی" # بازرگانی + SERVICE = "خدماتی" # خدماتی + OTHER = "سایر" # سایر + + +class Business(Base): + __tablename__ = "businesses" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + business_type: Mapped[BusinessType] = mapped_column(SQLEnum(BusinessType), nullable=False) + business_field: Mapped[BusinessField] = mapped_column(SQLEnum(BusinessField), nullable=False) + owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + + # فیلدهای جدید + address: Mapped[str | None] = mapped_column(Text, nullable=True) + phone: Mapped[str | None] = mapped_column(String(20), nullable=True) + mobile: Mapped[str | None] = mapped_column(String(20), nullable=True) + national_id: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True) + registration_number: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True) + economic_id: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True) + + # فیلدهای جغرافیایی + country: Mapped[str | None] = mapped_column(String(100), nullable=True) + province: Mapped[str | None] = mapped_column(String(100), nullable=True) + city: Mapped[str | None] = mapped_column(String(100), nullable=True) + postal_code: Mapped[str | None] = mapped_column(String(20), nullable=True) + + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) diff --git a/hesabixAPI/adapters/db/repositories/business_repo.py b/hesabixAPI/adapters/db/repositories/business_repo.py new file mode 100644 index 0000000..53c9ed8 --- /dev/null +++ b/hesabixAPI/adapters/db/repositories/business_repo.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from typing import List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import select, and_ + +from .base_repo import BaseRepository +from ..models.business import Business, BusinessType, BusinessField + + +class BusinessRepository(BaseRepository[Business]): + """Repository برای مدیریت کسب و کارها""" + + def __init__(self, db: Session) -> None: + super().__init__(db, Business) + + def get_by_owner_id(self, owner_id: int) -> List[Business]: + """دریافت تمام کسب و کارهای یک مالک""" + stmt = select(Business).where(Business.owner_id == owner_id) + return list(self.db.execute(stmt).scalars().all()) + + def get_by_business_type(self, business_type: BusinessType) -> List[Business]: + """دریافت کسب و کارها بر اساس نوع""" + stmt = select(Business).where(Business.business_type == business_type) + return list(self.db.execute(stmt).scalars().all()) + + def get_by_business_field(self, business_field: BusinessField) -> List[Business]: + """دریافت کسب و کارها بر اساس زمینه فعالیت""" + stmt = select(Business).where(Business.business_field == business_field) + return list(self.db.execute(stmt).scalars().all()) + + def get_by_owner_and_type(self, owner_id: int, business_type: BusinessType) -> List[Business]: + """دریافت کسب و کارهای یک مالک بر اساس نوع""" + stmt = select(Business).where( + and_( + Business.owner_id == owner_id, + Business.business_type == business_type + ) + ) + return list(self.db.execute(stmt).scalars().all()) + + def search_by_name(self, name: str) -> List[Business]: + """جستجوی کسب و کارها بر اساس نام (case-insensitive)""" + stmt = select(Business).where(Business.name.ilike(f"%{name}%")) + return list(self.db.execute(stmt).scalars().all()) + + def create_business( + self, + name: str, + business_type: BusinessType, + business_field: BusinessField, + owner_id: int, + address: str | None = None, + phone: str | None = None, + mobile: str | None = None, + national_id: str | None = None, + registration_number: str | None = None, + economic_id: str | None = None, + country: str | None = None, + province: str | None = None, + city: str | None = None, + postal_code: str | None = None + ) -> Business: + """ایجاد کسب و کار جدید""" + business = Business( + name=name, + business_type=business_type, + business_field=business_field, + owner_id=owner_id, + address=address, + phone=phone, + mobile=mobile, + national_id=national_id, + registration_number=registration_number, + economic_id=economic_id, + country=country, + province=province, + city=city, + postal_code=postal_code + ) + self.db.add(business) + self.db.commit() + self.db.refresh(business) + return business + + def get_by_national_id(self, national_id: str) -> Business | None: + """دریافت کسب و کار بر اساس شناسه ملی""" + stmt = select(Business).where(Business.national_id == national_id) + return self.db.execute(stmt).scalars().first() + + def get_by_registration_number(self, registration_number: str) -> Business | None: + """دریافت کسب و کار بر اساس شماره ثبت""" + stmt = select(Business).where(Business.registration_number == registration_number) + return self.db.execute(stmt).scalars().first() + + def get_by_economic_id(self, economic_id: str) -> Business | None: + """دریافت کسب و کار بر اساس شناسه اقتصادی""" + stmt = select(Business).where(Business.economic_id == economic_id) + return self.db.execute(stmt).scalars().first() + + def search_by_phone(self, phone: str) -> List[Business]: + """جستجوی کسب و کارها بر اساس شماره تلفن""" + stmt = select(Business).where( + (Business.phone == phone) | (Business.mobile == phone) + ) + return list(self.db.execute(stmt).scalars().all()) + + def get_by_country(self, country: str) -> List[Business]: + """دریافت کسب و کارها بر اساس کشور""" + stmt = select(Business).where(Business.country == country) + return list(self.db.execute(stmt).scalars().all()) + + def get_by_province(self, province: str) -> List[Business]: + """دریافت کسب و کارها بر اساس استان""" + stmt = select(Business).where(Business.province == province) + return list(self.db.execute(stmt).scalars().all()) + + def get_by_city(self, city: str) -> List[Business]: + """دریافت کسب و کارها بر اساس شهرستان""" + stmt = select(Business).where(Business.city == city) + return list(self.db.execute(stmt).scalars().all()) + + def get_by_postal_code(self, postal_code: str) -> List[Business]: + """دریافت کسب و کارها بر اساس کد پستی""" + stmt = select(Business).where(Business.postal_code == postal_code) + return list(self.db.execute(stmt).scalars().all()) + + def get_by_location(self, country: str | None = None, province: str | None = None, city: str | None = None) -> List[Business]: + """دریافت کسب و کارها بر اساس موقعیت جغرافیایی""" + stmt = select(Business) + conditions = [] + + if country: + conditions.append(Business.country == country) + if province: + conditions.append(Business.province == province) + if city: + conditions.append(Business.city == city) + + if conditions: + stmt = stmt.where(and_(*conditions)) + + return list(self.db.execute(stmt).scalars().all()) diff --git a/hesabixAPI/hesabix_api.egg-info/PKG-INFO b/hesabixAPI/hesabix_api.egg-info/PKG-INFO index d302d60..a9ccb77 100644 --- a/hesabixAPI/hesabix_api.egg-info/PKG-INFO +++ b/hesabixAPI/hesabix_api.egg-info/PKG-INFO @@ -21,6 +21,7 @@ Requires-Dist: Babel>=2.15.0 Requires-Dist: jdatetime>=4.1.0 Requires-Dist: weasyprint>=62.3 Requires-Dist: jinja2>=3.1.0 +Requires-Dist: openpyxl>=3.1.0 Provides-Extra: dev Requires-Dist: pytest>=8.2.0; extra == "dev" Requires-Dist: httpx>=0.27.0; extra == "dev" diff --git a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt index 9ea634e..01f391a 100644 --- a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt +++ b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt @@ -35,9 +35,12 @@ app/core/smart_normalizer.py app/services/api_key_service.py app/services/auth_service.py app/services/captcha_service.py -app/services/pdf_service.py app/services/query_service.py -app/services/user_context_service.py +app/services/pdf/__init__.py +app/services/pdf/base_pdf_service.py +app/services/pdf/modules/__init__.py +app/services/pdf/modules/marketing/__init__.py +app/services/pdf/modules/marketing/marketing_module.py hesabix_api.egg-info/PKG-INFO hesabix_api.egg-info/SOURCES.txt hesabix_api.egg-info/dependency_links.txt diff --git a/hesabixAPI/hesabix_api.egg-info/requires.txt b/hesabixAPI/hesabix_api.egg-info/requires.txt index 7c7baca..af04350 100644 --- a/hesabixAPI/hesabix_api.egg-info/requires.txt +++ b/hesabixAPI/hesabix_api.egg-info/requires.txt @@ -14,6 +14,7 @@ Babel>=2.15.0 jdatetime>=4.1.0 weasyprint>=62.3 jinja2>=3.1.0 +openpyxl>=3.1.0 [dev] pytest>=8.2.0 diff --git a/hesabixAPI/migrations/versions/20250117_000003_add_business_table.py b/hesabixAPI/migrations/versions/20250117_000003_add_business_table.py new file mode 100644 index 0000000..0531336 --- /dev/null +++ b/hesabixAPI/migrations/versions/20250117_000003_add_business_table.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "20250117_000003" +down_revision = "20250916_000002" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Create businesses table + op.create_table( + 'businesses', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('business_type', mysql.ENUM('شرکت', 'مغازه', 'فروشگاه', 'اتحادیه', 'باشگاه', 'موسسه', 'شخصی', name='businesstype'), nullable=False), + sa.Column('business_field', mysql.ENUM('تولیدی', 'بازرگانی', 'خدماتی', 'سایر', name='businessfield'), nullable=False), + sa.Column('owner_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexes + op.create_index('ix_businesses_name', 'businesses', ['name']) + op.create_index('ix_businesses_owner_id', 'businesses', ['owner_id']) + + +def downgrade() -> None: + # Drop indexes + op.drop_index('ix_businesses_owner_id', table_name='businesses') + op.drop_index('ix_businesses_name', table_name='businesses') + + # Drop table + op.drop_table('businesses') diff --git a/hesabixAPI/migrations/versions/20250117_000004_add_business_contact_fields.py b/hesabixAPI/migrations/versions/20250117_000004_add_business_contact_fields.py new file mode 100644 index 0000000..89862be --- /dev/null +++ b/hesabixAPI/migrations/versions/20250117_000004_add_business_contact_fields.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "20250117_000004" +down_revision = "20250117_000003" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add new contact and identification fields to businesses table + op.add_column('businesses', sa.Column('address', sa.Text(), nullable=True)) + op.add_column('businesses', sa.Column('phone', sa.String(length=20), nullable=True)) + op.add_column('businesses', sa.Column('mobile', sa.String(length=20), nullable=True)) + op.add_column('businesses', sa.Column('national_id', sa.String(length=20), nullable=True)) + op.add_column('businesses', sa.Column('registration_number', sa.String(length=50), nullable=True)) + op.add_column('businesses', sa.Column('economic_id', sa.String(length=50), nullable=True)) + + # Create indexes for the new fields + op.create_index('ix_businesses_national_id', 'businesses', ['national_id']) + op.create_index('ix_businesses_registration_number', 'businesses', ['registration_number']) + op.create_index('ix_businesses_economic_id', 'businesses', ['economic_id']) + + +def downgrade() -> None: + # Drop indexes + op.drop_index('ix_businesses_economic_id', table_name='businesses') + op.drop_index('ix_businesses_registration_number', table_name='businesses') + op.drop_index('ix_businesses_national_id', table_name='businesses') + + # Drop columns + op.drop_column('businesses', 'economic_id') + op.drop_column('businesses', 'registration_number') + op.drop_column('businesses', 'national_id') + op.drop_column('businesses', 'mobile') + op.drop_column('businesses', 'phone') + op.drop_column('businesses', 'address') diff --git a/hesabixAPI/migrations/versions/20250117_000005_add_business_geographic_fields.py b/hesabixAPI/migrations/versions/20250117_000005_add_business_geographic_fields.py new file mode 100644 index 0000000..d9bdafc --- /dev/null +++ b/hesabixAPI/migrations/versions/20250117_000005_add_business_geographic_fields.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "20250117_000005" +down_revision = "20250117_000004" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add geographic fields to businesses table + op.add_column('businesses', sa.Column('country', sa.String(length=100), nullable=True)) + op.add_column('businesses', sa.Column('province', sa.String(length=100), nullable=True)) + op.add_column('businesses', sa.Column('city', sa.String(length=100), nullable=True)) + op.add_column('businesses', sa.Column('postal_code', sa.String(length=20), nullable=True)) + + +def downgrade() -> None: + # Drop geographic columns + op.drop_column('businesses', 'postal_code') + op.drop_column('businesses', 'city') + op.drop_column('businesses', 'province') + op.drop_column('businesses', 'country')