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
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, Request, Query
from fastapi.responses import Response
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.auth_service import register_user, login_user, create_password_reset, reset_password, change_password, referral_stats
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.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.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:
captcha_id, image_base64, ttl = create_captcha(db)
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(
request: Request,
ctx: AuthContext = Depends(get_current_user)
@ -37,7 +106,61 @@ def get_current_user_info(
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:
user_id = register_user(
db=db,
@ -66,7 +189,61 @@ def register(request: Request, payload: RegisterRequest, db: Session = Depends(g
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:
user_agent = request.headers.get("User-Agent")
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)
@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:
# 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)
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:
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})
@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:
items = list_personal_keys(db, ctx.user.id)
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:
id_, api_key = create_personal_key(db, ctx.user.id, payload.name, payload.scopes, None)
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:
# دریافت translator از request state
translator = getattr(request.state, "translator", None)
@ -135,14 +493,75 @@ def change_password_endpoint(request: Request, payload: ChangePasswordRequest, c
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:
revoke_key(db, ctx.user.id, key_id)
return success_response({"ok": True})
@router.get("/referrals/stats", summary="Referral stats for current user")
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:
@router.get("/referrals/stats",
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
start_dt = datetime.fromisoformat(start) if start 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)
@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(
request: Request,
query_info: QueryInfo,
@ -229,7 +686,26 @@ def get_referral_list_advanced(
}, 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(
request: Request,
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(
request: Request,
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 .schemas import SuccessResponse
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]:
return {"status": "ok"}

View file

@ -1,7 +1,6 @@
from __future__ import annotations
from typing import Any
from typing import Any, List, Optional, Union
from pydantic import BaseModel, EmailStr, Field
from enum import Enum
class FilterItem(BaseModel):
@ -11,13 +10,13 @@ class FilterItem(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 = مرتب سازی نزولی")
take: int = Field(default=10, ge=1, le=1000, description="حداکثر تعداد رکورد بازگشتی")
skip: int = Field(default=0, ge=0, description="تعداد رکوردی که از ابتدای لیست صرف نظر می شود")
search: str | None = Field(default=None, description="عبارت جستجو")
search_fields: list[str] | None = Field(default=None, description="آرایه ای از فیلدهایی که جستجو در آن انجام می گیرد")
filters: list[FilterItem] | None = Field(default=None, description="آرایه ای از اشیا برای اعمال فیلتر بر روی لیست")
search: Optional[str] = Field(default=None, description="عبارت جستجو")
search_fields: Optional[List[str]] = Field(default=None, description="آرایه ای از فیلدهایی که جستجو در آن انجام می گیرد")
filters: Optional[List[FilterItem]] = Field(default=None, description="آرایه ای از اشیا برای اعمال فیلتر بر روی لیست")
class CaptchaSolve(BaseModel):
@ -26,19 +25,19 @@ class CaptchaSolve(BaseModel):
class RegisterRequest(CaptchaSolve):
first_name: str | None = Field(default=None, max_length=100)
last_name: str | None = Field(default=None, max_length=100)
email: EmailStr | None = None
mobile: str | None = Field(default=None, max_length=32)
first_name: Optional[str] = Field(default=None, max_length=100)
last_name: Optional[str] = Field(default=None, max_length=100)
email: Optional[EmailStr] = None
mobile: Optional[str] = Field(default=None, max_length=32)
password: str = Field(..., min_length=8, max_length=128)
device_id: str | None = Field(default=None, max_length=100)
referrer_code: str | None = Field(default=None, min_length=4, max_length=32)
device_id: Optional[str] = Field(default=None, max_length=100)
referrer_code: Optional[str] = Field(default=None, min_length=4, max_length=32)
class LoginRequest(CaptchaSolve):
identifier: str = Field(..., min_length=3, max_length=255)
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):
@ -57,8 +56,171 @@ class ChangePasswordRequest(BaseModel):
class CreateApiKeyRequest(BaseModel):
name: str | None = Field(default=None, max_length=100)
scopes: str | None = Field(default=None, max_length=500)
expires_at: str | None = None # ISO string; parse server-side if provided
name: Optional[str] = Field(default=None, max_length=100)
scopes: Optional[str] = Field(default=None, max_length=500)
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 sqlalchemy.orm import Session
from adapters.db.session import get_db
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.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_user_management
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(
request: Request,
query_info: QueryInfo = Depends(),
query_info: QueryInfo,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
):
"""
دریافت لیست کاربران با قابلیت فیلتر، جستجو، مرتبسازی و صفحهبندی
@ -91,13 +147,136 @@ def list_users(
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(
user_id: int,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
):
"""دریافت اطلاعات یک کاربر بر اساس ID"""
repo = UserRepository(db)
user = repo.get_by_id(user_id)
@ -112,12 +291,51 @@ def get_user(
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(
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
):
"""دریافت آمار کلی کاربران"""
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.auth import router as auth_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.error_handlers import register_error_handlers
from app.core.smart_normalizer import smart_normalize_json, SmartNormalizerConfig
@ -20,6 +21,204 @@ def create_app() -> FastAPI:
title=settings.app_name,
version=settings.app_version,
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(
@ -62,13 +261,55 @@ def create_app() -> FastAPI:
application.include_router(health_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(businesses_router, prefix=settings.api_v1_prefix)
register_error_handlers(application)
@application.get("/")
@application.get("/",
summary="اطلاعات سرویس",
description="دریافت اطلاعات کلی سرویس و نسخه",
tags=["general"]
)
def read_root() -> dict[str, str]:
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

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/v1/__init__.py
adapters/api/v1/auth.py
adapters/api/v1/businesses.py
adapters/api/v1/health.py
adapters/api/v1/schemas.py
adapters/api/v1/users.py
@ -37,9 +38,9 @@ app/core/responses.py
app/core/security.py
app/core/settings.py
app/core/smart_normalizer.py
app/core/permissions/__init__.py
app/services/api_key_service.py
app/services/auth_service.py
app/services/business_service.py
app/services/captcha_service.py
app/services/query_service.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'] ?? 'خطا در دریافت آمار کسب و کارها');
}
}
}