progress in new business

This commit is contained in:
Hesabix 2025-09-20 01:17:27 +03:30
parent ad5cd35f8f
commit 46902be8af
13 changed files with 3429 additions and 93 deletions

325
hesabixAPI/API_README.md Normal file
View file

@ -0,0 +1,325 @@
# Hesabix API Documentation
## 🔗 دسترسی به مستندات
- **Swagger UI**: http://localhost:8000/docs
- **ReDoc**: http://localhost:8000/redoc
- **OpenAPI Schema**: http://localhost:8000/openapi.json
## 🚀 شروع سریع
### 1. نصب و راه‌اندازی
```bash
# کلون کردن پروژه
git clone https://github.com/hesabix/api.git
cd hesabix-api
# نصب dependencies
pip install -r requirements.txt
# راه‌اندازی دیتابیس
alembic upgrade head
# اجرای سرور
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
### 2. اولین درخواست
```bash
# بررسی وضعیت API
curl http://localhost:8000/api/v1/health
# دریافت کپچا
curl -X POST http://localhost:8000/api/v1/auth/captcha
# ثبت‌نام
curl -X POST http://localhost:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali" \
-d '{
"first_name": "احمد",
"last_name": "احمدی",
"email": "ahmad@example.com",
"password": "password123",
"captcha_id": "captcha_id_from_previous_step",
"captcha_code": "12345"
}'
```
## 🔐 احراز هویت
### کلیدهای API
تمام endpoint های محافظت شده نیاز به کلید API دارند:
```bash
Authorization: Bearer sk_your_api_key_here
```
### نحوه دریافت کلید
1. **ثبت‌نام**: `POST /api/v1/auth/register`
2. **ورود**: `POST /api/v1/auth/login`
3. **کلیدهای شخصی**: `POST /api/v1/auth/api-keys`
### مثال کامل
```bash
# 1. دریافت کپچا
curl -X POST http://localhost:8000/api/v1/auth/captcha
# 2. ورود
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali" \
-d '{
"identifier": "user@example.com",
"password": "password123",
"captcha_id": "captcha_id_from_step_1",
"captcha_code": "12345"
}'
# 3. استفاده از API
curl -X GET http://localhost:8000/api/v1/auth/me \
-H "Authorization: Bearer sk_1234567890abcdef" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali"
```
## 🌍 چندزبانه
### هدرهای زبان
```bash
Accept-Language: fa # فارسی (پیش‌فرض)
Accept-Language: en # انگلیسی
Accept-Language: fa-IR # فارسی ایران
Accept-Language: en-US # انگلیسی آمریکا
```
## 📅 تقویم
### هدرهای تقویم
```bash
X-Calendar-Type: jalali # تقویم شمسی (پیش‌فرض)
X-Calendar-Type: gregorian # تقویم میلادی
```
## 🛡️ مجوزهای دسترسی
### مجوزهای اپلیکیشن
- `user_management`: مدیریت کاربران
- `superadmin`: دسترسی کامل
- `business_management`: مدیریت کسب و کارها
- `system_settings`: تنظیمات سیستم
### endpoint های محافظت شده
- `/api/v1/users/*` → نیاز به `user_management`
- `/api/v1/auth/me` → نیاز به احراز هویت
- `/api/v1/auth/api-keys/*` → نیاز به احراز هویت
## 📊 فرمت پاسخ‌ها
### پاسخ موفق
```json
{
"success": true,
"message": "عملیات با موفقیت انجام شد",
"data": {
// داده‌های اصلی
}
}
```
### پاسخ خطا
```json
{
"success": false,
"message": "پیام خطا",
"error_code": "ERROR_CODE",
"details": {
// جزئیات خطا
}
}
```
### کدهای خطا
| کد | معنی |
|----|------|
| 200 | موفقیت |
| 400 | خطا در اعتبارسنجی |
| 401 | احراز هویت نشده |
| 403 | دسترسی غیرمجاز |
| 404 | منبع یافت نشد |
| 422 | خطا در اعتبارسنجی |
| 500 | خطای سرور |
## 🔒 امنیت
### کپچا
برای عملیات حساس:
```bash
# دریافت کپچا
curl -X POST http://localhost:8000/api/v1/auth/captcha
# استفاده در ثبت‌نام/ورود
{
"captcha_id": "captcha_id_from_previous_step",
"captcha_code": "12345"
}
```
### رمزگذاری
- رمزهای عبور: bcrypt
- کلیدهای API: SHA-256
## 📝 مثال‌های کاربردی
### مدیریت کاربران
```bash
# لیست کاربران (نیاز به مجوز usermanager)
curl -X GET http://localhost:8000/api/v1/users \
-H "Authorization: Bearer sk_your_api_key" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali"
# جستجوی پیشرفته کاربران
curl -X POST http://localhost:8000/api/v1/users/search \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali" \
-d '{
"take": 10,
"skip": 0,
"search": "احمد",
"search_fields": ["first_name", "last_name", "email"],
"filters": [
{
"property": "is_active",
"operator": "=",
"value": true
}
]
}'
# دریافت اطلاعات یک کاربر
curl -X GET http://localhost:8000/api/v1/users/1 \
-H "Authorization: Bearer sk_your_api_key" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali"
# آمار کاربران
curl -X GET http://localhost:8000/api/v1/users/stats/summary \
-H "Authorization: Bearer sk_your_api_key" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali"
```
### مدیریت معرفی‌ها
```bash
# آمار معرفی‌ها
curl -X GET "http://localhost:8000/api/v1/auth/referrals/stats?start=2024-01-01&end=2024-12-31" \
-H "Authorization: Bearer sk_your_api_key" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali"
# لیست معرفی‌ها
curl -X POST http://localhost:8000/api/v1/auth/referrals/list \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali" \
-d '{
"take": 10,
"skip": 0,
"sort_by": "created_at",
"sort_desc": true
}'
# خروجی PDF
curl -X POST http://localhost:8000/api/v1/auth/referrals/export/pdf \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali" \
-d '{
"take": 100,
"skip": 0
}' \
--output referrals.pdf
# خروجی Excel
curl -X POST http://localhost:8000/api/v1/auth/referrals/export/excel \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-H "Accept-Language: fa" \
-H "X-Calendar-Type: jalali" \
-d '{
"take": 100,
"skip": 0
}' \
--output referrals.xlsx
```
## 🛠️ توسعه
### ساختار پروژه
```
hesabixAPI/
├── app/
│ ├── core/ # تنظیمات اصلی
│ ├── services/ # سرویس‌ها
│ └── main.py # نقطه ورود
├── adapters/
│ ├── api/v1/ # API endpoints
│ └── db/ # دیتابیس
├── migrations/ # مهاجرت‌های دیتابیس
└── tests/ # تست‌ها
```
### اجرای تست‌ها
```bash
pytest tests/
```
### مهاجرت دیتابیس
```bash
# ایجاد migration جدید
alembic revision --autogenerate -m "description"
# اعمال migrations
alembic upgrade head
# بازگشت به migration قبلی
alembic downgrade -1
```
## 📞 پشتیبانی
- **ایمیل**: support@hesabix.com
- **مستندات**: http://localhost:8000/docs
- **ReDoc**: http://localhost:8000/redoc
- **GitHub**: https://github.com/hesabix/api
## 📄 مجوز
این پروژه تحت مجوز MIT منتشر شده است. برای جزئیات بیشتر به فایل [LICENSE](LICENSE) مراجعه کنید.

View file

@ -1,7 +1,7 @@
from __future__ import annotations # Removed __future__ annotations to fix OpenAPI schema generation
import datetime import datetime
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request, Query
from fastapi.responses import Response from fastapi.responses import Response
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -10,7 +10,12 @@ from app.core.responses import success_response, format_datetime_fields
from app.services.captcha_service import create_captcha from app.services.captcha_service import create_captcha
from app.services.auth_service import register_user, login_user, create_password_reset, reset_password, change_password, referral_stats from app.services.auth_service import register_user, login_user, create_password_reset, reset_password, change_password, referral_stats
from app.services.pdf import PDFService from app.services.pdf import PDFService
from .schemas import RegisterRequest, LoginRequest, ForgotPasswordRequest, ResetPasswordRequest, ChangePasswordRequest, CreateApiKeyRequest, QueryInfo, FilterItem from .schemas import (
RegisterRequest, LoginRequest, ForgotPasswordRequest, ResetPasswordRequest,
ChangePasswordRequest, CreateApiKeyRequest, QueryInfo, FilterItem,
SuccessResponse, CaptchaResponse, LoginResponse, ApiKeyResponse,
ReferralStatsResponse, UserResponse
)
from app.core.auth_dependency import get_current_user, AuthContext from app.core.auth_dependency import get_current_user, AuthContext
from app.services.api_key_service import list_personal_keys, create_personal_key, revoke_key from app.services.api_key_service import list_personal_keys, create_personal_key, revoke_key
@ -18,7 +23,29 @@ from app.services.api_key_service import list_personal_keys, create_personal_key
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/captcha", summary="Generate numeric captcha") @router.post("/captcha",
summary="تولید کپچای عددی",
description="تولید کپچای عددی برای تأیید هویت در عملیات حساس",
response_model=SuccessResponse,
responses={
200: {
"description": "کپچا با موفقیت تولید شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کپچا تولید شد",
"data": {
"captcha_id": "abc123def456",
"image_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
"ttl_seconds": 180
}
}
}
}
}
}
)
def generate_captcha(db: Session = Depends(get_db)) -> dict: def generate_captcha(db: Session = Depends(get_db)) -> dict:
captcha_id, image_base64, ttl = create_captcha(db) captcha_id, image_base64, ttl = create_captcha(db)
return success_response({ return success_response({
@ -28,7 +55,49 @@ def generate_captcha(db: Session = Depends(get_db)) -> dict:
}) })
@router.get("/me", summary="Get current user info") @router.get("/me",
summary="دریافت اطلاعات کاربر کنونی",
description="دریافت اطلاعات کامل کاربری که در حال حاضر وارد سیستم شده است",
response_model=SuccessResponse,
responses={
200: {
"description": "اطلاعات کاربر با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "اطلاعات کاربر دریافت شد",
"data": {
"id": 1,
"email": "user@example.com",
"mobile": "09123456789",
"first_name": "احمد",
"last_name": "احمدی",
"is_active": True,
"referral_code": "ABC123",
"referred_by_user_id": None,
"app_permissions": {"admin": True},
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "احراز هویت مورد نیاز است",
"error_code": "UNAUTHORIZED"
}
}
}
}
}
)
def get_current_user_info( def get_current_user_info(
request: Request, request: Request,
ctx: AuthContext = Depends(get_current_user) ctx: AuthContext = Depends(get_current_user)
@ -37,7 +106,61 @@ def get_current_user_info(
return success_response(ctx.to_dict(), request) return success_response(ctx.to_dict(), request)
@router.post("/register", summary="Register new user") @router.post("/register",
summary="ثبت‌نام کاربر جدید",
description="ثبت‌نام کاربر جدید در سیستم با تأیید کپچا",
response_model=SuccessResponse,
responses={
200: {
"description": "کاربر با موفقیت ثبت‌نام شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "ثبت‌نام با موفقیت انجام شد",
"data": {
"api_key": "sk_1234567890abcdef",
"expires_at": None,
"user": {
"id": 1,
"first_name": "احمد",
"last_name": "احمدی",
"email": "ahmad@example.com",
"mobile": "09123456789",
"referral_code": "ABC123",
"app_permissions": None
}
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کپچا نامعتبر است",
"error_code": "INVALID_CAPTCHA"
}
}
}
},
409: {
"description": "کاربر با این ایمیل یا موبایل قبلاً ثبت‌نام کرده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر با این ایمیل قبلاً ثبت‌نام کرده است",
"error_code": "USER_EXISTS"
}
}
}
}
}
)
def register(request: Request, payload: RegisterRequest, db: Session = Depends(get_db)) -> dict: def register(request: Request, payload: RegisterRequest, db: Session = Depends(get_db)) -> dict:
user_id = register_user( user_id = register_user(
db=db, db=db,
@ -66,7 +189,61 @@ def register(request: Request, payload: RegisterRequest, db: Session = Depends(g
return success_response(formatted_data, request) return success_response(formatted_data, request)
@router.post("/login", summary="Login with email or mobile") @router.post("/login",
summary="ورود با ایمیل یا موبایل",
description="ورود کاربر به سیستم با استفاده از ایمیل یا شماره موبایل و رمز عبور",
response_model=SuccessResponse,
responses={
200: {
"description": "ورود با موفقیت انجام شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "ورود با موفقیت انجام شد",
"data": {
"api_key": "sk_1234567890abcdef",
"expires_at": "2024-01-02T00:00:00Z",
"user": {
"id": 1,
"first_name": "احمد",
"last_name": "احمدی",
"email": "ahmad@example.com",
"mobile": "09123456789",
"referral_code": "ABC123",
"app_permissions": {"admin": True}
}
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کپچا نامعتبر است",
"error_code": "INVALID_CAPTCHA"
}
}
}
},
401: {
"description": "اطلاعات ورود نامعتبر است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "ایمیل یا رمز عبور اشتباه است",
"error_code": "INVALID_CREDENTIALS"
}
}
}
}
}
)
def login(request: Request, payload: LoginRequest, db: Session = Depends(get_db)) -> dict: def login(request: Request, payload: LoginRequest, db: Session = Depends(get_db)) -> dict:
user_agent = request.headers.get("User-Agent") user_agent = request.headers.get("User-Agent")
ip = request.client.host if request.client else None ip = request.client.host if request.client else None
@ -94,32 +271,213 @@ def login(request: Request, payload: LoginRequest, db: Session = Depends(get_db)
return success_response(formatted_data, request) return success_response(formatted_data, request)
@router.post("/forgot-password", summary="Create password reset token") @router.post("/forgot-password",
summary="ایجاد توکن بازنشانی رمز عبور",
description="ایجاد توکن برای بازنشانی رمز عبور کاربر",
response_model=SuccessResponse,
responses={
200: {
"description": "توکن بازنشانی با موفقیت ایجاد شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "توکن بازنشانی ارسال شد",
"data": {
"ok": True,
"token": "reset_token_1234567890abcdef"
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کپچا نامعتبر است",
"error_code": "INVALID_CAPTCHA"
}
}
}
},
404: {
"description": "کاربر یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر با این ایمیل یا موبایل یافت نشد",
"error_code": "USER_NOT_FOUND"
}
}
}
}
}
)
def forgot_password(payload: ForgotPasswordRequest, db: Session = Depends(get_db)) -> dict: def forgot_password(payload: ForgotPasswordRequest, db: Session = Depends(get_db)) -> dict:
# In production do not return token; send via email/SMS. Here we return for dev/testing. # In production do not return token; send via email/SMS. Here we return for dev/testing.
token = create_password_reset(db=db, identifier=payload.identifier, captcha_id=payload.captcha_id, captcha_code=payload.captcha_code) token = create_password_reset(db=db, identifier=payload.identifier, captcha_id=payload.captcha_id, captcha_code=payload.captcha_code)
return success_response({"ok": True, "token": token if token else None}) return success_response({"ok": True, "token": token if token else None})
@router.post("/reset-password", summary="Reset password with token") @router.post("/reset-password",
summary="بازنشانی رمز عبور با توکن",
description="بازنشانی رمز عبور کاربر با استفاده از توکن دریافتی",
response_model=SuccessResponse,
responses={
200: {
"description": "رمز عبور با موفقیت بازنشانی شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "رمز عبور با موفقیت تغییر کرد",
"data": {
"ok": True
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کپچا نامعتبر است",
"error_code": "INVALID_CAPTCHA"
}
}
}
},
404: {
"description": "توکن نامعتبر یا منقضی شده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "توکن نامعتبر یا منقضی شده است",
"error_code": "INVALID_TOKEN"
}
}
}
}
}
)
def reset_password_endpoint(payload: ResetPasswordRequest, db: Session = Depends(get_db)) -> dict: def reset_password_endpoint(payload: ResetPasswordRequest, db: Session = Depends(get_db)) -> dict:
reset_password(db=db, token=payload.token, new_password=payload.new_password, captcha_id=payload.captcha_id, captcha_code=payload.captcha_code) reset_password(db=db, token=payload.token, new_password=payload.new_password, captcha_id=payload.captcha_id, captcha_code=payload.captcha_code)
return success_response({"ok": True}) return success_response({"ok": True})
@router.get("/api-keys", summary="List personal API keys") @router.get("/api-keys",
summary="لیست کلیدهای API شخصی",
description="دریافت لیست کلیدهای API شخصی کاربر",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست کلیدهای API با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست کلیدهای API دریافت شد",
"data": [
{
"id": 1,
"name": "کلید اصلی",
"scopes": "read,write",
"device_id": "device123",
"user_agent": "Mozilla/5.0...",
"ip": "192.168.1.1",
"expires_at": None,
"last_used_at": "2024-01-01T12:00:00Z",
"created_at": "2024-01-01T00:00:00Z"
}
]
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def list_keys(request: Request, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict: def list_keys(request: Request, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
items = list_personal_keys(db, ctx.user.id) items = list_personal_keys(db, ctx.user.id)
return success_response(items) return success_response(items)
@router.post("/api-keys", summary="Create personal API key") @router.post("/api-keys",
summary="ایجاد کلید API شخصی",
description="ایجاد کلید API جدید برای کاربر",
response_model=SuccessResponse,
responses={
200: {
"description": "کلید API با موفقیت ایجاد شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کلید API ایجاد شد",
"data": {
"id": 1,
"api_key": "sk_1234567890abcdef"
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def create_key(request: Request, payload: CreateApiKeyRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict: def create_key(request: Request, payload: CreateApiKeyRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
id_, api_key = create_personal_key(db, ctx.user.id, payload.name, payload.scopes, None) id_, api_key = create_personal_key(db, ctx.user.id, payload.name, payload.scopes, None)
return success_response({"id": id_, "api_key": api_key}) return success_response({"id": id_, "api_key": api_key})
@router.post("/change-password", summary="Change user password") @router.post("/change-password",
summary="تغییر رمز عبور",
description="تغییر رمز عبور کاربر با تأیید رمز عبور فعلی",
response_model=SuccessResponse,
responses={
200: {
"description": "رمز عبور با موفقیت تغییر کرد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "رمز عبور با موفقیت تغییر کرد",
"data": {
"ok": True
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "رمز عبور فعلی اشتباه است",
"error_code": "INVALID_CURRENT_PASSWORD"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def change_password_endpoint(request: Request, payload: ChangePasswordRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict: def change_password_endpoint(request: Request, payload: ChangePasswordRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
# دریافت translator از request state # دریافت translator از request state
translator = getattr(request.state, "translator", None) translator = getattr(request.state, "translator", None)
@ -135,14 +493,75 @@ def change_password_endpoint(request: Request, payload: ChangePasswordRequest, c
return success_response({"ok": True}) return success_response({"ok": True})
@router.delete("/api-keys/{key_id}", summary="Revoke API key") @router.delete("/api-keys/{key_id}",
summary="حذف کلید API",
description="حذف کلید API مشخص شده",
response_model=SuccessResponse,
responses={
200: {
"description": "کلید API با موفقیت حذف شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کلید API حذف شد",
"data": {
"ok": True
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
404: {
"description": "کلید API یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کلید API یافت نشد",
"error_code": "API_KEY_NOT_FOUND"
}
}
}
}
}
)
def delete_key(request: Request, key_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict: def delete_key(request: Request, key_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
revoke_key(db, ctx.user.id, key_id) revoke_key(db, ctx.user.id, key_id)
return success_response({"ok": True}) return success_response({"ok": True})
@router.get("/referrals/stats", summary="Referral stats for current user") @router.get("/referrals/stats",
def get_referral_stats(request: Request, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), start: str | None = None, end: str | None = None) -> dict: summary="آمار معرفی‌ها",
description="دریافت آمار معرفی‌های کاربر فعلی",
response_model=SuccessResponse,
responses={
200: {
"description": "آمار معرفی‌ها با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "آمار معرفی‌ها دریافت شد",
"data": {
"total_referrals": 25,
"active_referrals": 20,
"recent_referrals": 5,
"referral_rate": 0.8
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def get_referral_stats(request: Request, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), start: str = Query(None, description="تاریخ شروع (ISO format)"), end: str = Query(None, description="تاریخ پایان (ISO format)")):
from datetime import datetime from datetime import datetime
start_dt = datetime.fromisoformat(start) if start else None start_dt = datetime.fromisoformat(start) if start else None
end_dt = datetime.fromisoformat(end) if end else None end_dt = datetime.fromisoformat(end) if end else None
@ -150,7 +569,45 @@ def get_referral_stats(request: Request, ctx: AuthContext = Depends(get_current_
return success_response(stats) return success_response(stats)
@router.post("/referrals/list", summary="Referral list with advanced filtering") @router.post("/referrals/list",
summary="لیست معرفی‌ها با فیلتر پیشرفته",
description="دریافت لیست معرفی‌ها با قابلیت فیلتر، جستجو، مرتب‌سازی و صفحه‌بندی",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست معرفی‌ها با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست معرفی‌ها دریافت شد",
"data": {
"items": [
{
"id": 1,
"first_name": "علی",
"last_name": "احمدی",
"email": "ali@example.com",
"mobile": "09123456789",
"created_at": "2024-01-01T00:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 10,
"total_pages": 1,
"has_next": False,
"has_prev": False
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def get_referral_list_advanced( def get_referral_list_advanced(
request: Request, request: Request,
query_info: QueryInfo, query_info: QueryInfo,
@ -229,7 +686,26 @@ def get_referral_list_advanced(
}, request) }, request)
@router.post("/referrals/export/pdf", summary="Export referrals to PDF") @router.post("/referrals/export/pdf",
summary="خروجی PDF لیست معرفی‌ها",
description="خروجی PDF لیست معرفی‌ها با قابلیت فیلتر و انتخاب سطرهای خاص",
responses={
200: {
"description": "فایل PDF با موفقیت تولید شد",
"content": {
"application/pdf": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def export_referrals_pdf( def export_referrals_pdf(
request: Request, request: Request,
query_info: QueryInfo, query_info: QueryInfo,
@ -320,7 +796,26 @@ def export_referrals_pdf(
) )
@router.post("/referrals/export/excel", summary="Export referrals to Excel") @router.post("/referrals/export/excel",
summary="خروجی Excel لیست معرفی‌ها",
description="خروجی Excel لیست معرفی‌ها با قابلیت فیلتر و انتخاب سطرهای خاص",
responses={
200: {
"description": "فایل Excel با موفقیت تولید شد",
"content": {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def export_referrals_excel( def export_referrals_excel(
request: Request, request: Request,
query_info: QueryInfo, query_info: QueryInfo,

View file

@ -0,0 +1,310 @@
# Removed __future__ annotations to fix OpenAPI schema generation
from fastapi import APIRouter, Depends, Request, Query, HTTPException
from sqlalchemy.orm import Session
from adapters.db.session import get_db
from adapters.api.v1.schemas import (
BusinessCreateRequest, BusinessUpdateRequest, BusinessResponse,
BusinessListResponse, BusinessSummaryResponse, SuccessResponse,
QueryInfo
)
from app.core.responses import success_response, format_datetime_fields
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_business_management
from app.services.business_service import (
create_business, get_business_by_id, get_businesses_by_owner,
update_business, delete_business, get_business_summary
)
router = APIRouter(prefix="/businesses", tags=["businesses"])
@router.post("",
summary="ایجاد کسب و کار جدید",
description="ایجاد کسب و کار جدید برای کاربر جاری",
response_model=SuccessResponse,
responses={
200: {
"description": "کسب و کار با موفقیت ایجاد شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کسب و کار با موفقیت ایجاد شد",
"data": {
"id": 1,
"name": "شرکت نمونه",
"business_type": "شرکت",
"business_field": "تولیدی",
"owner_id": 1,
"created_at": "2024-01-01T00:00:00Z"
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها"
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def create_new_business(
request: Request,
business_data: BusinessCreateRequest,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""ایجاد کسب و کار جدید"""
owner_id = ctx.get_user_id()
business = create_business(db, business_data, owner_id)
formatted_data = format_datetime_fields(business, request)
return success_response(formatted_data, request, "کسب و کار با موفقیت ایجاد شد")
@router.get("",
summary="لیست کسب و کارهای کاربر",
description="دریافت لیست کسب و کارهای کاربر جاری با قابلیت فیلتر و جستجو",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست کسب و کارها با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست کسب و کارها دریافت شد",
"data": {
"items": [
{
"id": 1,
"name": "شرکت نمونه",
"business_type": "شرکت",
"business_field": "تولیدی",
"owner_id": 1,
"created_at": "2024-01-01T00:00:00Z"
}
],
"pagination": {
"total": 1,
"page": 1,
"per_page": 10,
"total_pages": 1,
"has_next": False,
"has_prev": False
}
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def list_user_businesses(
request: Request,
query_info: QueryInfo,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""لیست کسب و کارهای کاربر"""
owner_id = ctx.get_user_id()
query_dict = query_info.dict()
businesses = get_businesses_by_owner(db, owner_id, query_dict)
formatted_data = format_datetime_fields(businesses, request)
return success_response(formatted_data, request, "لیست کسب و کارها دریافت شد")
@router.get("/{business_id}",
summary="جزئیات کسب و کار",
description="دریافت جزئیات یک کسب و کار خاص",
response_model=SuccessResponse,
responses={
200: {
"description": "جزئیات کسب و کار با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "جزئیات کسب و کار دریافت شد",
"data": {
"id": 1,
"name": "شرکت نمونه",
"business_type": "شرکت",
"business_field": "تولیدی",
"owner_id": 1,
"address": "تهران، خیابان ولیعصر",
"phone": "02112345678",
"created_at": "2024-01-01T00:00:00Z"
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
404: {
"description": "کسب و کار یافت نشد"
}
}
)
def get_business(
request: Request,
business_id: int,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""دریافت جزئیات کسب و کار"""
owner_id = ctx.get_user_id()
business = get_business_by_id(db, business_id, owner_id)
if not business:
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
formatted_data = format_datetime_fields(business, request)
return success_response(formatted_data, request, "جزئیات کسب و کار دریافت شد")
@router.put("/{business_id}",
summary="ویرایش کسب و کار",
description="ویرایش اطلاعات یک کسب و کار",
response_model=SuccessResponse,
responses={
200: {
"description": "کسب و کار با موفقیت ویرایش شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کسب و کار با موفقیت ویرایش شد",
"data": {
"id": 1,
"name": "شرکت نمونه ویرایش شده",
"business_type": "شرکت",
"business_field": "تولیدی",
"owner_id": 1,
"updated_at": "2024-01-01T12:00:00Z"
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها"
},
401: {
"description": "کاربر احراز هویت نشده است"
},
404: {
"description": "کسب و کار یافت نشد"
}
}
)
def update_business_info(
request: Request,
business_id: int,
business_data: BusinessUpdateRequest,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""ویرایش کسب و کار"""
owner_id = ctx.get_user_id()
business = update_business(db, business_id, business_data, owner_id)
if not business:
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
formatted_data = format_datetime_fields(business, request)
return success_response(formatted_data, request, "کسب و کار با موفقیت ویرایش شد")
@router.delete("/{business_id}",
summary="حذف کسب و کار",
description="حذف یک کسب و کار",
response_model=SuccessResponse,
responses={
200: {
"description": "کسب و کار با موفقیت حذف شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کسب و کار با موفقیت حذف شد",
"data": {"ok": True}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
404: {
"description": "کسب و کار یافت نشد"
}
}
)
def delete_business_info(
request: Request,
business_id: int,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""حذف کسب و کار"""
owner_id = ctx.get_user_id()
success = delete_business(db, business_id, owner_id)
if not success:
raise HTTPException(status_code=404, detail="کسب و کار یافت نشد")
return success_response({"ok": True}, request, "کسب و کار با موفقیت حذف شد")
@router.get("/summary/stats",
summary="آمار کسب و کارها",
description="دریافت آمار کلی کسب و کارهای کاربر",
response_model=SuccessResponse,
responses={
200: {
"description": "آمار کسب و کارها با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "آمار کسب و کارها دریافت شد",
"data": {
"total_businesses": 5,
"by_type": {
"شرکت": 2,
"مغازه": 1,
"فروشگاه": 2
},
"by_field": {
"تولیدی": 3,
"خدماتی": 2
}
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def get_business_stats(
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""آمار کسب و کارها"""
owner_id = ctx.get_user_id()
stats = get_business_summary(db, owner_id)
return success_response(stats, request, "آمار کسب و کارها دریافت شد")

View file

@ -1,8 +1,30 @@
from fastapi import APIRouter from fastapi import APIRouter
from .schemas import SuccessResponse
router = APIRouter(prefix="/health", tags=["health"]) router = APIRouter(prefix="/health", tags=["health"])
@router.get("", summary="Health check") @router.get("",
summary="بررسی وضعیت سرویس",
description="بررسی وضعیت کلی سرویس و در دسترس بودن آن",
response_model=SuccessResponse,
responses={
200: {
"description": "سرویس در دسترس است",
"content": {
"application/json": {
"example": {
"success": True,
"message": "سرویس در دسترس است",
"data": {
"status": "ok",
"timestamp": "2024-01-01T00:00:00Z"
}
}
}
}
}
}
)
def health() -> dict[str, str]: def health() -> dict[str, str]:
return {"status": "ok"} return {"status": "ok"}

View file

@ -1,7 +1,6 @@
from __future__ import annotations from typing import Any, List, Optional, Union
from typing import Any
from pydantic import BaseModel, EmailStr, Field from pydantic import BaseModel, EmailStr, Field
from enum import Enum
class FilterItem(BaseModel): class FilterItem(BaseModel):
@ -11,13 +10,13 @@ class FilterItem(BaseModel):
class QueryInfo(BaseModel): class QueryInfo(BaseModel):
sort_by: str | None = Field(default=None, description="نام فیلد مورد نظر برای مرتب سازی") sort_by: Optional[str] = Field(default=None, description="نام فیلد مورد نظر برای مرتب سازی")
sort_desc: bool = Field(default=False, description="false = مرتب سازی صعودی، true = مرتب سازی نزولی") sort_desc: bool = Field(default=False, description="false = مرتب سازی صعودی، true = مرتب سازی نزولی")
take: int = Field(default=10, ge=1, le=1000, description="حداکثر تعداد رکورد بازگشتی") take: int = Field(default=10, ge=1, le=1000, description="حداکثر تعداد رکورد بازگشتی")
skip: int = Field(default=0, ge=0, description="تعداد رکوردی که از ابتدای لیست صرف نظر می شود") skip: int = Field(default=0, ge=0, description="تعداد رکوردی که از ابتدای لیست صرف نظر می شود")
search: str | None = Field(default=None, description="عبارت جستجو") search: Optional[str] = Field(default=None, description="عبارت جستجو")
search_fields: list[str] | None = Field(default=None, description="آرایه ای از فیلدهایی که جستجو در آن انجام می گیرد") search_fields: Optional[List[str]] = Field(default=None, description="آرایه ای از فیلدهایی که جستجو در آن انجام می گیرد")
filters: list[FilterItem] | None = Field(default=None, description="آرایه ای از اشیا برای اعمال فیلتر بر روی لیست") filters: Optional[List[FilterItem]] = Field(default=None, description="آرایه ای از اشیا برای اعمال فیلتر بر روی لیست")
class CaptchaSolve(BaseModel): class CaptchaSolve(BaseModel):
@ -26,19 +25,19 @@ class CaptchaSolve(BaseModel):
class RegisterRequest(CaptchaSolve): class RegisterRequest(CaptchaSolve):
first_name: str | None = Field(default=None, max_length=100) first_name: Optional[str] = Field(default=None, max_length=100)
last_name: str | None = Field(default=None, max_length=100) last_name: Optional[str] = Field(default=None, max_length=100)
email: EmailStr | None = None email: Optional[EmailStr] = None
mobile: str | None = Field(default=None, max_length=32) mobile: Optional[str] = Field(default=None, max_length=32)
password: str = Field(..., min_length=8, max_length=128) password: str = Field(..., min_length=8, max_length=128)
device_id: str | None = Field(default=None, max_length=100) device_id: Optional[str] = Field(default=None, max_length=100)
referrer_code: str | None = Field(default=None, min_length=4, max_length=32) referrer_code: Optional[str] = Field(default=None, min_length=4, max_length=32)
class LoginRequest(CaptchaSolve): class LoginRequest(CaptchaSolve):
identifier: str = Field(..., min_length=3, max_length=255) identifier: str = Field(..., min_length=3, max_length=255)
password: str = Field(..., min_length=8, max_length=128) password: str = Field(..., min_length=8, max_length=128)
device_id: str | None = Field(default=None, max_length=100) device_id: Optional[str] = Field(default=None, max_length=100)
class ForgotPasswordRequest(CaptchaSolve): class ForgotPasswordRequest(CaptchaSolve):
@ -57,8 +56,171 @@ class ChangePasswordRequest(BaseModel):
class CreateApiKeyRequest(BaseModel): class CreateApiKeyRequest(BaseModel):
name: str | None = Field(default=None, max_length=100) name: Optional[str] = Field(default=None, max_length=100)
scopes: str | None = Field(default=None, max_length=500) scopes: Optional[str] = Field(default=None, max_length=500)
expires_at: str | None = None # ISO string; parse server-side if provided expires_at: Optional[str] = None # ISO string; parse server-side if provided
# Response Models
class SuccessResponse(BaseModel):
success: bool = Field(default=True, description="وضعیت موفقیت عملیات")
message: Optional[str] = Field(default=None, description="پیام توضیحی")
data: Optional[Union[dict, list]] = Field(default=None, description="داده‌های بازگشتی")
class ErrorResponse(BaseModel):
success: bool = Field(default=False, description="وضعیت موفقیت عملیات")
message: str = Field(..., description="پیام خطا")
error_code: Optional[str] = Field(default=None, description="کد خطا")
details: Optional[dict] = Field(default=None, description="جزئیات خطا")
class UserResponse(BaseModel):
id: int = Field(..., description="شناسه کاربر")
email: Optional[str] = Field(default=None, description="ایمیل کاربر")
mobile: Optional[str] = Field(default=None, description="شماره موبایل")
first_name: Optional[str] = Field(default=None, description="نام")
last_name: Optional[str] = Field(default=None, description="نام خانوادگی")
is_active: bool = Field(..., description="وضعیت فعال بودن")
referral_code: str = Field(..., description="کد معرفی")
referred_by_user_id: Optional[int] = Field(default=None, description="شناسه کاربر معرف")
app_permissions: Optional[dict] = Field(default=None, description="مجوزهای اپلیکیشن")
created_at: str = Field(..., description="تاریخ ایجاد")
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
class CaptchaResponse(BaseModel):
captcha_id: str = Field(..., description="شناسه کپچا")
image_base64: str = Field(..., description="تصویر کپچا به صورت base64")
ttl_seconds: int = Field(..., description="زمان انقضا به ثانیه")
class LoginResponse(BaseModel):
api_key: str = Field(..., description="کلید API")
expires_at: Optional[str] = Field(default=None, description="تاریخ انقضا")
user: UserResponse = Field(..., description="اطلاعات کاربر")
class ApiKeyResponse(BaseModel):
id: int = Field(..., description="شناسه کلید")
name: Optional[str] = Field(default=None, description="نام کلید")
scopes: Optional[str] = Field(default=None, description="محدوده دسترسی")
device_id: Optional[str] = Field(default=None, description="شناسه دستگاه")
user_agent: Optional[str] = Field(default=None, description="اطلاعات مرورگر")
ip: Optional[str] = Field(default=None, description="آدرس IP")
expires_at: Optional[str] = Field(default=None, description="تاریخ انقضا")
last_used_at: Optional[str] = Field(default=None, description="آخرین استفاده")
created_at: str = Field(..., description="تاریخ ایجاد")
class ReferralStatsResponse(BaseModel):
total_referrals: int = Field(..., description="تعداد کل معرفی‌ها")
active_referrals: int = Field(..., description="تعداد معرفی‌های فعال")
recent_referrals: int = Field(..., description="تعداد معرفی‌های اخیر")
referral_rate: float = Field(..., description="نرخ معرفی")
class PaginationInfo(BaseModel):
total: int = Field(..., description="تعداد کل رکوردها")
page: int = Field(..., description="شماره صفحه فعلی")
per_page: int = Field(..., description="تعداد رکورد در هر صفحه")
total_pages: int = Field(..., description="تعداد کل صفحات")
has_next: bool = Field(..., description="آیا صفحه بعدی وجود دارد")
has_prev: bool = Field(..., description="آیا صفحه قبلی وجود دارد")
class UsersListResponse(BaseModel):
items: List[UserResponse] = Field(..., description="لیست کاربران")
pagination: PaginationInfo = Field(..., description="اطلاعات صفحه‌بندی")
query_info: dict = Field(..., description="اطلاعات جستجو و فیلتر")
class UsersSummaryResponse(BaseModel):
total_users: int = Field(..., description="تعداد کل کاربران")
active_users: int = Field(..., description="تعداد کاربران فعال")
inactive_users: int = Field(..., description="تعداد کاربران غیرفعال")
active_percentage: float = Field(..., description="درصد کاربران فعال")
# Business Schemas
class BusinessType(str, Enum):
COMPANY = "شرکت"
SHOP = "مغازه"
STORE = "فروشگاه"
UNION = "اتحادیه"
CLUB = "باشگاه"
INSTITUTE = "موسسه"
INDIVIDUAL = "شخصی"
class BusinessField(str, Enum):
MANUFACTURING = "تولیدی"
COMMERCIAL = "بازرگانی"
SERVICE = "خدماتی"
OTHER = "سایر"
class BusinessCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=255, description="نام کسب و کار")
business_type: BusinessType = Field(..., description="نوع کسب و کار")
business_field: BusinessField = Field(..., description="زمینه فعالیت")
address: Optional[str] = Field(default=None, max_length=1000, description="آدرس")
phone: Optional[str] = Field(default=None, max_length=20, description="تلفن ثابت")
mobile: Optional[str] = Field(default=None, max_length=20, description="موبایل")
national_id: Optional[str] = Field(default=None, max_length=20, description="کد ملی")
registration_number: Optional[str] = Field(default=None, max_length=50, description="شماره ثبت")
economic_id: Optional[str] = Field(default=None, max_length=50, description="شناسه اقتصادی")
country: Optional[str] = Field(default=None, max_length=100, description="کشور")
province: Optional[str] = Field(default=None, max_length=100, description="استان")
city: Optional[str] = Field(default=None, max_length=100, description="شهر")
postal_code: Optional[str] = Field(default=None, max_length=20, description="کد پستی")
class BusinessUpdateRequest(BaseModel):
name: Optional[str] = Field(default=None, min_length=1, max_length=255, description="نام کسب و کار")
business_type: Optional[BusinessType] = Field(default=None, description="نوع کسب و کار")
business_field: Optional[BusinessField] = Field(default=None, description="زمینه فعالیت")
address: Optional[str] = Field(default=None, max_length=1000, description="آدرس")
phone: Optional[str] = Field(default=None, max_length=20, description="تلفن ثابت")
mobile: Optional[str] = Field(default=None, max_length=20, description="موبایل")
national_id: Optional[str] = Field(default=None, max_length=20, description="کد ملی")
registration_number: Optional[str] = Field(default=None, max_length=50, description="شماره ثبت")
economic_id: Optional[str] = Field(default=None, max_length=50, description="شناسه اقتصادی")
country: Optional[str] = Field(default=None, max_length=100, description="کشور")
province: Optional[str] = Field(default=None, max_length=100, description="استان")
city: Optional[str] = Field(default=None, max_length=100, description="شهر")
postal_code: Optional[str] = Field(default=None, max_length=20, description="کد پستی")
class BusinessResponse(BaseModel):
id: int = Field(..., description="شناسه کسب و کار")
name: str = Field(..., description="نام کسب و کار")
business_type: str = Field(..., description="نوع کسب و کار")
business_field: str = Field(..., description="زمینه فعالیت")
owner_id: int = Field(..., description="شناسه مالک")
address: Optional[str] = Field(default=None, description="آدرس")
phone: Optional[str] = Field(default=None, description="تلفن ثابت")
mobile: Optional[str] = Field(default=None, description="موبایل")
national_id: Optional[str] = Field(default=None, description="کد ملی")
registration_number: Optional[str] = Field(default=None, description="شماره ثبت")
economic_id: Optional[str] = Field(default=None, description="شناسه اقتصادی")
country: Optional[str] = Field(default=None, description="کشور")
province: Optional[str] = Field(default=None, description="استان")
city: Optional[str] = Field(default=None, description="شهر")
postal_code: Optional[str] = Field(default=None, description="کد پستی")
created_at: str = Field(..., description="تاریخ ایجاد")
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
class BusinessListResponse(BaseModel):
items: List[BusinessResponse] = Field(..., description="لیست کسب و کارها")
pagination: PaginationInfo = Field(..., description="اطلاعات صفحه‌بندی")
query_info: dict = Field(..., description="اطلاعات جستجو و فیلتر")
class BusinessSummaryResponse(BaseModel):
total_businesses: int = Field(..., description="تعداد کل کسب و کارها")
by_type: dict = Field(..., description="تعداد بر اساس نوع")
by_field: dict = Field(..., description="تعداد بر اساس زمینه فعالیت")

View file

@ -1,25 +1,81 @@
from __future__ import annotations # Removed __future__ annotations to fix OpenAPI schema generation
from fastapi import APIRouter, Depends, Request, Query from fastapi import APIRouter, Depends, Request, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from adapters.db.session import get_db from adapters.db.session import get_db
from adapters.db.repositories.user_repo import UserRepository from adapters.db.repositories.user_repo import UserRepository
from adapters.api.v1.schemas import QueryInfo from adapters.api.v1.schemas import QueryInfo, SuccessResponse, UsersListResponse, UsersSummaryResponse, UserResponse
from app.core.responses import success_response, format_datetime_fields from app.core.responses import success_response, format_datetime_fields
from app.core.auth_dependency import get_current_user, AuthContext from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_user_management
router = APIRouter(prefix="/users", tags=["users"]) router = APIRouter(prefix="/users", tags=["users"])
@router.get("", summary="لیست کاربران با فیلتر پیشرفته") @router.post("/search",
summary="لیست کاربران با فیلتر پیشرفته",
description="دریافت لیست کاربران با قابلیت فیلتر، جستجو، مرتب‌سازی و صفحه‌بندی. نیاز به مجوز usermanager در سطح اپلیکیشن دارد.",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست کاربران با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست کاربران دریافت شد",
"data": {
"items": [
{
"id": 1,
"email": "user@example.com",
"mobile": "09123456789",
"first_name": "احمد",
"last_name": "احمدی",
"is_active": True,
"referral_code": "ABC123",
"created_at": "2024-01-01T00:00:00Z"
}
],
"pagination": {
"total": 1,
"page": 1,
"per_page": 10,
"total_pages": 1,
"has_next": False,
"has_prev": False
}
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز usermanager",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Missing app permission: user_management",
"error_code": "FORBIDDEN"
}
}
}
}
}
)
@require_user_management()
def list_users( def list_users(
request: Request, request: Request,
query_info: QueryInfo = Depends(), query_info: QueryInfo,
ctx: AuthContext = Depends(get_current_user), ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
) -> dict: ):
""" """
دریافت لیست کاربران با قابلیت فیلتر، جستجو، مرتبسازی و صفحهبندی دریافت لیست کاربران با قابلیت فیلتر، جستجو، مرتبسازی و صفحهبندی
@ -91,13 +147,136 @@ def list_users(
return success_response(response_data, request) return success_response(response_data, request)
@router.get("/{user_id}", summary="دریافت اطلاعات یک کاربر") @router.get("",
summary="لیست ساده کاربران",
description="دریافت لیست ساده کاربران. نیاز به مجوز usermanager در سطح اپلیکیشن دارد.",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست کاربران با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست کاربران دریافت شد",
"data": [
{
"id": 1,
"email": "user@example.com",
"mobile": "09123456789",
"first_name": "احمد",
"last_name": "احمدی",
"is_active": True,
"referral_code": "ABC123",
"created_at": "2024-01-01T00:00:00Z"
}
]
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز usermanager",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Missing app permission: user_management",
"error_code": "FORBIDDEN"
}
}
}
}
}
)
@require_user_management()
def list_users_simple(
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
limit: int = Query(10, ge=1, le=100, description="تعداد رکورد در هر صفحه"),
offset: int = Query(0, ge=0, description="تعداد رکورد صرف‌نظر شده")
):
"""دریافت لیست ساده کاربران"""
repo = UserRepository(db)
# Create basic query info
query_info = QueryInfo(take=limit, skip=offset)
users, total = repo.query_with_filters(query_info)
# تبدیل User objects به dictionary
user_dicts = [repo.to_dict(user) for user in users]
# فرمت کردن تاریخ‌ها
formatted_users = [format_datetime_fields(user_dict, None) for user_dict in user_dicts]
return success_response(formatted_users, None)
@router.get("/{user_id}",
summary="دریافت اطلاعات یک کاربر",
description="دریافت اطلاعات کامل یک کاربر بر اساس شناسه. نیاز به مجوز usermanager در سطح اپلیکیشن دارد.",
response_model=SuccessResponse,
responses={
200: {
"description": "اطلاعات کاربر با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "اطلاعات کاربر دریافت شد",
"data": {
"id": 1,
"email": "user@example.com",
"mobile": "09123456789",
"first_name": "احمد",
"last_name": "احمدی",
"is_active": True,
"referral_code": "ABC123",
"created_at": "2024-01-01T00:00:00Z"
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز usermanager",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Missing app permission: user_management",
"error_code": "FORBIDDEN"
}
}
}
},
404: {
"description": "کاربر یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر یافت نشد",
"error_code": "USER_NOT_FOUND"
}
}
}
}
}
)
@require_user_management()
def get_user( def get_user(
user_id: int, user_id: int,
request: Request, request: Request,
ctx: AuthContext = Depends(get_current_user), ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
) -> dict: ):
"""دریافت اطلاعات یک کاربر بر اساس ID""" """دریافت اطلاعات یک کاربر بر اساس ID"""
repo = UserRepository(db) repo = UserRepository(db)
user = repo.get_by_id(user_id) user = repo.get_by_id(user_id)
@ -112,12 +291,51 @@ def get_user(
return success_response(formatted_user, request) return success_response(formatted_user, request)
@router.get("/stats/summary", summary="آمار کلی کاربران") @router.get("/stats/summary",
summary="آمار کلی کاربران",
description="دریافت آمار کلی کاربران شامل تعداد کل، فعال و غیرفعال. نیاز به مجوز usermanager در سطح اپلیکیشن دارد.",
response_model=SuccessResponse,
responses={
200: {
"description": "آمار کاربران با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "آمار کاربران دریافت شد",
"data": {
"total_users": 100,
"active_users": 85,
"inactive_users": 15,
"active_percentage": 85.0
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز usermanager",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Missing app permission: user_management",
"error_code": "FORBIDDEN"
}
}
}
}
}
)
@require_user_management()
def get_users_summary( def get_users_summary(
request: Request, request: Request,
ctx: AuthContext = Depends(get_current_user), ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
) -> dict: ):
"""دریافت آمار کلی کاربران""" """دریافت آمار کلی کاربران"""
repo = UserRepository(db) repo = UserRepository(db)

View file

@ -1,37 +0,0 @@
from .permissions import (
require_app_permission,
require_business_permission,
require_any_permission,
require_superadmin,
require_business_access,
require_sales_write,
require_sales_delete,
require_sales_approve,
require_purchases_write,
require_accounting_write,
require_inventory_write,
require_reports_export,
require_settings_manage_users,
require_user_management,
require_business_management,
require_system_settings,
)
__all__ = [
"require_app_permission",
"require_business_permission",
"require_any_permission",
"require_superadmin",
"require_business_access",
"require_sales_write",
"require_sales_delete",
"require_sales_approve",
"require_purchases_write",
"require_accounting_write",
"require_inventory_write",
"require_reports_export",
"require_settings_manage_users",
"require_user_management",
"require_business_management",
"require_system_settings",
]

View file

@ -6,6 +6,7 @@ from app.core.logging import configure_logging
from adapters.api.v1.health import router as health_router from adapters.api.v1.health import router as health_router
from adapters.api.v1.auth import router as auth_router from adapters.api.v1.auth import router as auth_router
from adapters.api.v1.users import router as users_router from adapters.api.v1.users import router as users_router
from adapters.api.v1.businesses import router as businesses_router
from app.core.i18n import negotiate_locale, Translator from app.core.i18n import negotiate_locale, Translator
from app.core.error_handlers import register_error_handlers from app.core.error_handlers import register_error_handlers
from app.core.smart_normalizer import smart_normalize_json, SmartNormalizerConfig from app.core.smart_normalizer import smart_normalize_json, SmartNormalizerConfig
@ -20,6 +21,204 @@ def create_app() -> FastAPI:
title=settings.app_name, title=settings.app_name,
version=settings.app_version, version=settings.app_version,
debug=settings.debug, debug=settings.debug,
description="""
# Hesabix API
API جامع برای مدیریت کاربران، احراز هویت و سیستم معرفی
## ویژگی‌های اصلی:
- **احراز هویت**: ثبتنام، ورود، فراموشی رمز عبور
- **مدیریت کاربران**: لیست، جستجو، فیلتر و آمار کاربران
- **سیستم معرفی**: آمار و مدیریت معرفیها
- **خروجی**: PDF و Excel برای گزارشها
- **امنیت**: کپچا، کلیدهای API، رمزگذاری
## 🔐 احراز هویت (Authentication)
### کلیدهای API
تمام endpoint های محافظت شده نیاز به کلید API دارند که در header `Authorization` ارسال میشود:
```
Authorization: Bearer sk_your_api_key_here
```
### نحوه دریافت کلید API:
1. **ثبتنام**: با ثبتنام، یک کلید session دریافت میکنید
2. **ورود**: با ورود موفق، کلید session دریافت میکنید
3. **کلیدهای شخصی**: از endpoint `/api/v1/auth/api-keys` میتوانید کلیدهای شخصی ایجاد کنید
### انواع کلیدهای API:
- **Session Keys**: کلیدهای موقت که با ورود ایجاد میشوند
- **Personal Keys**: کلیدهای دائمی که خودتان ایجاد میکنید
### مثال درخواست با احراز هویت:
```bash
curl -X GET "http://localhost:8000/api/v1/auth/me" \\
-H "Authorization: Bearer sk_1234567890abcdef" \\
-H "Accept: application/json"
```
## 🛡️ مجوزهای دسترسی (Permissions)
برخی endpoint ها نیاز به مجوزهای خاص دارند:
### مجوزهای اپلیکیشن (App-Level Permissions):
- `user_management`: دسترسی به مدیریت کاربران
- `superadmin`: دسترسی کامل به سیستم
- `business_management`: مدیریت کسب و کارها
- `system_settings`: دسترسی به تنظیمات سیستم
### مثال مجوزها در JSON:
```json
{
"user_management": true,
"superadmin": false,
"business_management": true,
"system_settings": false
}
```
### endpoint های محافظت شده:
- تمام endpoint های `/api/v1/users/*` نیاز به مجوز `user_management` دارند
- endpoint های `/api/v1/auth/me` و `/api/v1/auth/api-keys/*` نیاز به احراز هویت دارند
## 🌍 چندزبانه (Internationalization)
API از چندزبانه پشتیبانی میکند:
### هدر زبان:
```
Accept-Language: fa
Accept-Language: en
Accept-Language: fa-IR
Accept-Language: en-US
```
### زبان‌های پشتیبانی شده:
- **فارسی (fa)**: پیشفرض
- **انگلیسی (en)**
### مثال درخواست با زبان فارسی:
```bash
curl -X GET "http://localhost:8000/api/v1/auth/me" \\
-H "Authorization: Bearer sk_1234567890abcdef" \\
-H "Accept-Language: fa" \\
-H "Accept: application/json"
```
## 📅 تقویم (Calendar)
API از تقویم شمسی (جلالی) پشتیبانی میکند:
### هدر تقویم:
```
X-Calendar-Type: jalali
X-Calendar-Type: gregorian
```
### انواع تقویم:
- **جلالی (jalali)**: تقویم شمسی - پیشفرض
- **میلادی (gregorian)**: تقویم میلادی
### مثال درخواست با تقویم شمسی:
```bash
curl -X GET "http://localhost:8000/api/v1/users" \\
-H "Authorization: Bearer sk_1234567890abcdef" \\
-H "X-Calendar-Type: jalali" \\
-H "Accept: application/json"
```
## 📊 فرمت پاسخ‌ها (Response Format)
تمام پاسخها در فرمت زیر هستند:
```json
{
"success": true,
"message": "پیام توضیحی",
"data": {
// دادههای اصلی
}
}
```
### کدهای خطا:
- **200**: موفقیت
- **400**: خطا در اعتبارسنجی دادهها
- **401**: احراز هویت نشده
- **403**: دسترسی غیرمجاز
- **404**: منبع یافت نشد
- **422**: خطا در اعتبارسنجی
- **500**: خطای سرور
## 🔒 امنیت (Security)
### کپچا:
برای عملیات حساس از کپچا استفاده میشود:
- دریافت کپچا: `POST /api/v1/auth/captcha`
- استفاده در ثبتنام، ورود، فراموشی رمز عبور
### رمزگذاری:
- رمزهای عبور با bcrypt رمزگذاری میشوند
- کلیدهای API با SHA-256 هش میشوند
## 📝 مثال کامل درخواست:
```bash
# 1. دریافت کپچا
curl -X POST "http://localhost:8000/api/v1/auth/captcha"
# 2. ورود
curl -X POST "http://localhost:8000/api/v1/auth/login" \\
-H "Content-Type: application/json" \\
-H "Accept-Language: fa" \\
-H "X-Calendar-Type: jalali" \\
-d '{
"identifier": "user@example.com",
"password": "password123",
"captcha_id": "captcha_id_from_step_1",
"captcha_code": "12345"
}'
# 3. استفاده از API با کلید دریافتی
curl -X GET "http://localhost:8000/api/v1/users" \\
-H "Authorization: Bearer sk_1234567890abcdef" \\
-H "Accept-Language: fa" \\
-H "X-Calendar-Type: jalali" \\
-H "Accept: application/json"
```
## 🚀 شروع سریع:
1. **ثبتنام**: `POST /api/v1/auth/register`
2. **ورود**: `POST /api/v1/auth/login`
3. **دریافت اطلاعات کاربر**: `GET /api/v1/auth/me`
4. **مدیریت کاربران**: `GET /api/v1/users` (نیاز به مجوز usermanager)
## 📞 پشتیبانی:
- **ایمیل**: support@hesabix.ir
- **مستندات**: `/docs` (Swagger UI)
- **ReDoc**: `/redoc`
""",
contact={
"name": "Hesabix Team",
"email": "support@hesabix.ir",
"url": "https://hesabix.ir",
},
license_info={
"name": "GNU GPLv3 License",
"url": "https://opensource.org/licenses/GPL-3.0",
},
servers=[
{
"url": "http://localhost:8000",
"description": "Development server"
},
{
"url": "https://agent.hesabix.ir",
"description": "Production server"
}
],
) )
application.add_middleware( application.add_middleware(
@ -62,13 +261,55 @@ def create_app() -> FastAPI:
application.include_router(health_router, prefix=settings.api_v1_prefix) application.include_router(health_router, prefix=settings.api_v1_prefix)
application.include_router(auth_router, prefix=settings.api_v1_prefix) application.include_router(auth_router, prefix=settings.api_v1_prefix)
application.include_router(users_router, prefix=settings.api_v1_prefix) application.include_router(users_router, prefix=settings.api_v1_prefix)
application.include_router(businesses_router, prefix=settings.api_v1_prefix)
register_error_handlers(application) register_error_handlers(application)
@application.get("/") @application.get("/",
summary="اطلاعات سرویس",
description="دریافت اطلاعات کلی سرویس و نسخه",
tags=["general"]
)
def read_root() -> dict[str, str]: def read_root() -> dict[str, str]:
return {"service": settings.app_name, "version": settings.app_version} return {"service": settings.app_name, "version": settings.app_version}
# اضافه کردن security schemes
from fastapi.openapi.utils import get_openapi
def custom_openapi():
if application.openapi_schema:
return application.openapi_schema
openapi_schema = get_openapi(
title=application.title,
version=application.version,
description=application.description,
routes=application.routes,
)
# اضافه کردن security schemes
openapi_schema["components"]["securitySchemes"] = {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "API Key",
"description": "کلید API برای احراز هویت. فرمت: Bearer sk_your_api_key_here"
}
}
# اضافه کردن security به endpoint های محافظت شده
for path, methods in openapi_schema["paths"].items():
for method, details in methods.items():
if method in ["get", "post", "put", "delete", "patch"]:
# تمام endpoint های auth و users نیاز به احراز هویت دارند
if "/auth/" in path or "/users" in path:
details["security"] = [{"BearerAuth": []}]
application.openapi_schema = openapi_schema
return application.openapi_schema
application.openapi = custom_openapi
return application return application

View file

@ -0,0 +1,186 @@
from __future__ import annotations
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select, and_, func
from adapters.db.repositories.business_repo import BusinessRepository
from adapters.db.models.business import Business, BusinessType, BusinessField
from adapters.api.v1.schemas import (
BusinessCreateRequest, BusinessUpdateRequest, BusinessResponse,
BusinessListResponse, BusinessSummaryResponse, PaginationInfo
)
from app.core.responses import format_datetime_fields
def create_business(db: Session, business_data: BusinessCreateRequest, owner_id: int) -> Dict[str, Any]:
"""ایجاد کسب و کار جدید"""
business_repo = BusinessRepository(db)
# تبدیل enum values به مقادیر فارسی
# business_data.business_type و business_data.business_field قبلاً مقادیر فارسی هستند
business_type_enum = business_data.business_type
business_field_enum = business_data.business_field
# ذخیره در دیتابیس
created_business = business_repo.create_business(
name=business_data.name,
business_type=business_type_enum,
business_field=business_field_enum,
owner_id=owner_id,
address=business_data.address,
phone=business_data.phone,
mobile=business_data.mobile,
national_id=business_data.national_id,
registration_number=business_data.registration_number,
economic_id=business_data.economic_id,
country=business_data.country,
province=business_data.province,
city=business_data.city,
postal_code=business_data.postal_code
)
# تبدیل به response format
return _business_to_dict(created_business)
def get_business_by_id(db: Session, business_id: int, owner_id: int) -> Optional[Dict[str, Any]]:
"""دریافت کسب و کار بر اساس شناسه"""
business_repo = BusinessRepository(db)
business = business_repo.get_by_id(business_id)
if not business or business.owner_id != owner_id:
return None
return _business_to_dict(business)
def get_businesses_by_owner(db: Session, owner_id: int, query_info: Dict[str, Any]) -> Dict[str, Any]:
"""دریافت لیست کسب و کارهای یک مالک"""
business_repo = BusinessRepository(db)
# دریافت کسب و کارها
businesses = business_repo.get_by_owner_id(owner_id)
# اعمال فیلترها
if query_info.get('search'):
search_term = query_info['search']
businesses = [b for b in businesses if search_term.lower() in b.name.lower()]
# اعمال مرتب‌سازی
sort_by = query_info.get('sort_by', 'created_at')
sort_desc = query_info.get('sort_desc', True)
if sort_by == 'name':
businesses.sort(key=lambda x: x.name, reverse=sort_desc)
elif sort_by == 'business_type':
businesses.sort(key=lambda x: x.business_type.value, reverse=sort_desc)
elif sort_by == 'created_at':
businesses.sort(key=lambda x: x.created_at, reverse=sort_desc)
# صفحه‌بندی
total = len(businesses)
skip = query_info.get('skip', 0)
take = query_info.get('take', 10)
start_idx = skip
end_idx = skip + take
paginated_businesses = businesses[start_idx:end_idx]
# محاسبه اطلاعات صفحه‌بندی
total_pages = (total + take - 1) // take
current_page = (skip // take) + 1
pagination = PaginationInfo(
total=total,
page=current_page,
per_page=take,
total_pages=total_pages,
has_next=current_page < total_pages,
has_prev=current_page > 1
)
# تبدیل به response format
items = [_business_to_dict(business) for business in paginated_businesses]
return {
"items": items,
"pagination": pagination.dict(),
"query_info": query_info
}
def update_business(db: Session, business_id: int, business_data: BusinessUpdateRequest, owner_id: int) -> Optional[Dict[str, Any]]:
"""ویرایش کسب و کار"""
business_repo = BusinessRepository(db)
business = business_repo.get_by_id(business_id)
if not business or business.owner_id != owner_id:
return None
# به‌روزرسانی فیلدها
update_data = business_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(business, field, value)
# ذخیره تغییرات
updated_business = business_repo.update(business)
return _business_to_dict(updated_business)
def delete_business(db: Session, business_id: int, owner_id: int) -> bool:
"""حذف کسب و کار"""
business_repo = BusinessRepository(db)
business = business_repo.get_by_id(business_id)
if not business or business.owner_id != owner_id:
return False
business_repo.delete(business_id)
return True
def get_business_summary(db: Session, owner_id: int) -> Dict[str, Any]:
"""دریافت خلاصه آمار کسب و کارها"""
business_repo = BusinessRepository(db)
businesses = business_repo.get_by_owner_id(owner_id)
# شمارش بر اساس نوع
by_type = {}
for business_type in BusinessType:
by_type[business_type.value] = len([b for b in businesses if b.business_type == business_type])
# شمارش بر اساس زمینه فعالیت
by_field = {}
for business_field in BusinessField:
by_field[business_field.value] = len([b for b in businesses if b.business_field == business_field])
return {
"total_businesses": len(businesses),
"by_type": by_type,
"by_field": by_field
}
def _business_to_dict(business: Business) -> Dict[str, Any]:
"""تبدیل مدل کسب و کار به dictionary"""
return {
"id": business.id,
"name": business.name,
"business_type": business.business_type.value,
"business_field": business.business_field.value,
"owner_id": business.owner_id,
"address": business.address,
"phone": business.phone,
"mobile": business.mobile,
"national_id": business.national_id,
"registration_number": business.registration_number,
"economic_id": business.economic_id,
"country": business.country,
"province": business.province,
"city": business.city,
"postal_code": business.postal_code,
"created_at": business.created_at.isoformat(),
"updated_at": business.updated_at.isoformat()
}

View file

@ -4,6 +4,7 @@ adapters/__init__.py
adapters/api/__init__.py adapters/api/__init__.py
adapters/api/v1/__init__.py adapters/api/v1/__init__.py
adapters/api/v1/auth.py adapters/api/v1/auth.py
adapters/api/v1/businesses.py
adapters/api/v1/health.py adapters/api/v1/health.py
adapters/api/v1/schemas.py adapters/api/v1/schemas.py
adapters/api/v1/users.py adapters/api/v1/users.py
@ -37,9 +38,9 @@ app/core/responses.py
app/core/security.py app/core/security.py
app/core/settings.py app/core/settings.py
app/core/smart_normalizer.py app/core/smart_normalizer.py
app/core/permissions/__init__.py
app/services/api_key_service.py app/services/api_key_service.py
app/services/auth_service.py app/services/auth_service.py
app/services/business_service.py
app/services/captcha_service.py app/services/captcha_service.py
app/services/query_service.py app/services/query_service.py
app/services/pdf/__init__.py app/services/pdf/__init__.py

View file

@ -0,0 +1,314 @@
enum BusinessType {
company('شرکت'),
shop('مغازه'),
store('فروشگاه'),
union('اتحادیه'),
club('باشگاه'),
institute('موسسه'),
individual('شخصی');
const BusinessType(this.displayName);
final String displayName;
}
enum BusinessField {
manufacturing('تولیدی'),
commercial('بازرگانی'),
service('خدماتی'),
other('سایر');
const BusinessField(this.displayName);
final String displayName;
}
class BusinessData {
// مرحله 1: اطلاعات پایه
String name;
BusinessType? businessType;
BusinessField? businessField;
// مرحله 2: اطلاعات تماس
String? address;
String? phone;
String? mobile;
String? postalCode;
// مرحله 3: اطلاعات قانونی
String? nationalId;
String? registrationNumber;
String? economicId;
// مرحله 4: اطلاعات جغرافیایی
String? country;
String? province;
String? city;
BusinessData({
this.name = '',
this.businessType,
this.businessField,
this.address,
this.phone,
this.mobile,
this.postalCode,
this.nationalId,
this.registrationNumber,
this.economicId,
this.country,
this.province,
this.city,
});
// تبدیل به Map برای ارسال به API
Map<String, dynamic> toJson() {
return {
'name': name,
'business_type': businessType?.name,
'business_field': businessField?.name,
'address': address,
'phone': phone,
'mobile': mobile,
'postal_code': postalCode,
'national_id': nationalId,
'registration_number': registrationNumber,
'economic_id': economicId,
'country': country,
'province': province,
'city': city,
};
}
// کپی کردن با تغییرات
BusinessData copyWith({
String? name,
BusinessType? businessType,
BusinessField? businessField,
String? address,
String? phone,
String? mobile,
String? postalCode,
String? nationalId,
String? registrationNumber,
String? economicId,
String? country,
String? province,
String? city,
}) {
return BusinessData(
name: name ?? this.name,
businessType: businessType ?? this.businessType,
businessField: businessField ?? this.businessField,
address: address ?? this.address,
phone: phone ?? this.phone,
mobile: mobile ?? this.mobile,
postalCode: postalCode ?? this.postalCode,
nationalId: nationalId ?? this.nationalId,
registrationNumber: registrationNumber ?? this.registrationNumber,
economicId: economicId ?? this.economicId,
country: country ?? this.country,
province: province ?? this.province,
city: city ?? this.city,
);
}
// بررسی اعتبار مرحله 1
bool isStep1Valid() {
return name.isNotEmpty && businessType != null && businessField != null;
}
// بررسی اعتبار مرحله 2 (اختیاری)
bool isStep2Valid() {
// اعتبارسنجی موبایل اگر وارد شده باشد
if (mobile != null && mobile!.isNotEmpty) {
if (!_isValidMobile(mobile!)) {
return false;
}
}
// اعتبارسنجی تلفن ثابت اگر وارد شده باشد
if (phone != null && phone!.isNotEmpty) {
if (!_isValidPhone(phone!)) {
return false;
}
}
return true;
}
// بررسی اعتبار مرحله 3 (اختیاری)
bool isStep3Valid() {
// اعتبارسنجی کد ملی اگر وارد شده باشد
if (nationalId != null && nationalId!.isNotEmpty) {
if (!_isValidNationalId(nationalId!)) {
return false;
}
}
return true;
}
// بررسی اعتبار مرحله 4 (اختیاری)
bool isStep4Valid() {
return true; // همه فیلدها اختیاری هستند
}
// بررسی اعتبار کل فرم
bool isFormValid() {
return isStep1Valid() && isStep2Valid() && isStep3Valid() && isStep4Valid();
}
// اعتبارسنجی شماره موبایل ایرانی
bool _isValidMobile(String mobile) {
// حذف فاصلهها و کاراکترهای اضافی
String cleanMobile = mobile.replaceAll(RegExp(r'[\s\-\(\)]'), '');
// بررسی فرمتهای مختلف موبایل ایرانی
RegExp mobileRegex = RegExp(r'^(\+98|0)?9\d{9}$');
if (!mobileRegex.hasMatch(cleanMobile)) {
return false;
}
// بررسی طول نهایی (باید 11 رقم باشد)
String finalMobile = cleanMobile.startsWith('+98')
? cleanMobile.substring(3)
: cleanMobile.startsWith('0')
? cleanMobile
: '0$cleanMobile';
return finalMobile.length == 11 && finalMobile.startsWith('09');
}
// اعتبارسنجی شماره تلفن ثابت ایرانی
bool _isValidPhone(String phone) {
// حذف فاصلهها و کاراکترهای اضافی
String cleanPhone = phone.replaceAll(RegExp(r'[\s\-\(\)]'), '');
// بررسی فرمتهای مختلف تلفن ثابت ایرانی
RegExp phoneRegex = RegExp(r'^(\+98|0)?[1-9]\d{7,8}$');
if (!phoneRegex.hasMatch(cleanPhone)) {
return false;
}
// بررسی طول نهایی (باید 8-11 رقم باشد)
String finalPhone = cleanPhone.startsWith('+98')
? cleanPhone.substring(3)
: cleanPhone.startsWith('0')
? cleanPhone
: '0$cleanPhone';
return finalPhone.length >= 8 && finalPhone.length <= 11;
}
// اعتبارسنجی کد ملی ایرانی
bool _isValidNationalId(String nationalId) {
// حذف فاصلهها و کاراکترهای اضافی
String cleanId = nationalId.replaceAll(RegExp(r'[\s\-]'), '');
// بررسی طول (باید 10 رقم باشد)
if (cleanId.length != 10) {
return false;
}
// بررسی اینکه همه کاراکترها عدد باشند
if (!RegExp(r'^\d{10}$').hasMatch(cleanId)) {
return false;
}
// بررسی الگوریتم کد ملی
int sum = 0;
for (int i = 0; i < 9; i++) {
sum += int.parse(cleanId[i]) * (10 - i);
}
int remainder = sum % 11;
int checkDigit = remainder < 2 ? remainder : 11 - remainder;
return checkDigit == int.parse(cleanId[9]);
}
// دریافت پیام خطای اعتبارسنجی
String? getValidationError(String field) {
switch (field) {
case 'mobile':
if (mobile != null && mobile!.isNotEmpty && !_isValidMobile(mobile!)) {
return 'شماره موبایل نامعتبر است. مثال: 09123456789';
}
break;
case 'phone':
if (phone != null && phone!.isNotEmpty && !_isValidPhone(phone!)) {
return 'شماره تلفن ثابت نامعتبر است. مثال: 02112345678';
}
break;
case 'nationalId':
if (nationalId != null && nationalId!.isNotEmpty && !_isValidNationalId(nationalId!)) {
return 'کد ملی نامعتبر است. مثال: 1234567890';
}
break;
}
return null;
}
}
class BusinessResponse {
final int id;
final String name;
final String businessType;
final String businessField;
final int ownerId;
final String? address;
final String? phone;
final String? mobile;
final String? nationalId;
final String? registrationNumber;
final String? economicId;
final String? country;
final String? province;
final String? city;
final String? postalCode;
final DateTime createdAt;
final DateTime updatedAt;
BusinessResponse({
required this.id,
required this.name,
required this.businessType,
required this.businessField,
required this.ownerId,
this.address,
this.phone,
this.mobile,
this.nationalId,
this.registrationNumber,
this.economicId,
this.country,
this.province,
this.city,
this.postalCode,
required this.createdAt,
required this.updatedAt,
});
factory BusinessResponse.fromJson(Map<String, dynamic> json) {
return BusinessResponse(
id: json['id'],
name: json['name'],
businessType: json['business_type'],
businessField: json['business_field'],
ownerId: json['owner_id'],
address: json['address'],
phone: json['phone'],
mobile: json['mobile'],
nationalId: json['national_id'],
registrationNumber: json['registration_number'],
economicId: json['economic_id'],
country: json['country'],
province: json['province'],
city: json['city'],
postalCode: json['postal_code'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,104 @@
import '../core/api_client.dart';
import '../models/business_models.dart';
class BusinessApiService {
static const String _basePath = '/api/v1/businesses';
static final ApiClient _apiClient = ApiClient();
// ایجاد کسب و کار جدید
static Future<BusinessResponse> createBusiness(BusinessData businessData) async {
final response = await _apiClient.post(
_basePath,
data: businessData.toJson(),
);
if (response.data['success'] == true) {
return BusinessResponse.fromJson(response.data['data']);
} else {
throw Exception(response.data['message'] ?? 'خطا در ایجاد کسب و کار');
}
}
// دریافت لیست کسب و کارها
static Future<List<BusinessResponse>> getBusinesses({
int page = 1,
int perPage = 10,
String? search,
String? sortBy,
bool sortDesc = true,
}) async {
final queryParams = <String, dynamic>{
'take': perPage,
'skip': (page - 1) * perPage,
};
if (search != null && search.isNotEmpty) {
queryParams['search'] = search;
}
if (sortBy != null) {
queryParams['sort_by'] = sortBy;
queryParams['sort_desc'] = sortDesc;
}
final response = await _apiClient.post(
'$_basePath/search',
data: queryParams,
);
if (response.data['success'] == true) {
final List<dynamic> items = response.data['data']['items'];
return items.map((item) => BusinessResponse.fromJson(item)).toList();
} else {
throw Exception(response.data['message'] ?? 'خطا در دریافت لیست کسب و کارها');
}
}
// دریافت جزئیات یک کسب و کار
static Future<BusinessResponse> getBusiness(int businessId) async {
final response = await _apiClient.get('$_basePath/$businessId');
if (response.data['success'] == true) {
return BusinessResponse.fromJson(response.data['data']);
} else {
throw Exception(response.data['message'] ?? 'خطا در دریافت جزئیات کسب و کار');
}
}
// ویرایش کسب و کار
static Future<BusinessResponse> updateBusiness(
int businessId,
BusinessData businessData,
) async {
final response = await _apiClient.put(
'$_basePath/$businessId',
data: businessData.toJson(),
);
if (response.data['success'] == true) {
return BusinessResponse.fromJson(response.data['data']);
} else {
throw Exception(response.data['message'] ?? 'خطا در ویرایش کسب و کار');
}
}
// حذف کسب و کار
static Future<void> deleteBusiness(int businessId) async {
final response = await _apiClient.delete('$_basePath/$businessId');
if (response.data['success'] != true) {
throw Exception(response.data['message'] ?? 'خطا در حذف کسب و کار');
}
}
// دریافت آمار کسب و کارها
static Future<Map<String, dynamic>> getBusinessStats() async {
final response = await _apiClient.get('$_basePath/summary/stats');
if (response.data['success'] == true) {
return response.data['data'];
} else {
throw Exception(response.data['message'] ?? 'خطا در دریافت آمار کسب و کارها');
}
}
}