progress in new business
This commit is contained in:
parent
ad5cd35f8f
commit
46902be8af
325
hesabixAPI/API_README.md
Normal file
325
hesabixAPI/API_README.md
Normal 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) مراجعه کنید.
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
310
hesabixAPI/adapters/api/v1/businesses.py
Normal file
310
hesabixAPI/adapters/api/v1/businesses.py
Normal 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, "آمار کسب و کارها دریافت شد")
|
||||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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="تعداد بر اساس زمینه فعالیت")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
186
hesabixAPI/app/services/business_service.py
Normal file
186
hesabixAPI/app/services/business_service.py
Normal 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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
314
hesabixUI/hesabix_ui/lib/models/business_models.dart
Normal file
314
hesabixUI/hesabix_ui/lib/models/business_models.dart
Normal 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
104
hesabixUI/hesabix_ui/lib/services/business_api_service.dart
Normal file
104
hesabixUI/hesabix_ui/lib/services/business_api_service.dart
Normal 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'] ?? 'خطا در دریافت آمار کسب و کارها');
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue