This commit is contained in:
javad committed 2026-09-10 11:34:19 +03:30
commit fa9d154db9
260 files changed
+17643

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
# استقرار Production روی Coolify
این پروژه با سه سرویس اجرا می‌شود:
- `nginx`: فقط سرویس‌دهنده فایل‌های `/static/` روی پورت 80
- `web`: اجرای Django با Gunicorn روی پورت داخلی 8000
- `notification-worker`: اجرای دائمی `python manage.py process_notifications` برای SMS و Bale
PostgreSQL جزو این Compose نیست و از دیتابیس مدیریت‌شده موجود استفاده می‌شود. Supervisor لازم نیست؛ هر process یک container مستقل دارد و `restart: unless-stopped` بازیابی آن را انجام می‌دهد.
## 1. Volumeهای Coolify
پیش از اولین deploy دو Docker volume با همین نام‌ها ایجاد کنید:
```text
customer_ticket_static
customer_ticket_media
```
در صورت انتخاب نام دیگر، متغیرهای `STATIC_VOLUME_NAME` و `MEDIA_VOLUME_NAME` را مطابق آن تنظیم کنید. media حاوی آپلودهای ماندگار است. static در هر startup دوباره با `collectstatic` تکمیل می‌شود اما برای اشتراک بین web و Nginx volume لازم دارد.
## 2. دامنه‌ها و Traefik خود Coolify
دامنه اصلی HTTPS را در Coolify مستقیماً به سرویس `web` و پورت `8000` متصل کنید. Traefik خود Coolify TLS، reverse proxy و هدر `X-Forwarded-Proto` را مدیریت می‌کند؛ Nginx جلوی Django قرار نمی‌گیرد.
برای همان دامنه، routeهای `/static/`، `/media/branding/` و `/protected-media/` را به سرویس `nginx` و پورت `80` بدهید و مطمئن شوید routeهای path-based نسبت به route عمومی web اولویت بالاتری دارند. برای `notification-worker` هیچ domain یا host port عمومی نسازید. اگر نسخه Coolify شما تعریف چند route روی یک دامنه را از UI پشتیبانی نمی‌کند، routing rule متناظر Traefik را برای این سه path به nginx اضافه کنید؛ معماری containerها همچنان بدون proxy_pass بین Nginx و Django است.
فایل‌های آپلودی تیکت عمومی نیستند: Django ابتدا مجوز را بررسی می‌کند و سپس کاربر را با یک URL امضاشده پنج‌دقیقه‌ای به `/protected-media/` روی Nginx می‌فرستد. branding عمومی زیر `/media/branding/` مستقیماً از Nginx سرو می‌شود.
## 3. متغیرهای ضروری
مقادیر زیر را در Environment Variables برنامه Coolify بسازید:
```dotenv
DJANGO_SECRET_KEY=<random-secret-at-least-50-characters>
DJANGO_ALLOWED_HOSTS=support.example.com
CSRF_TRUSTED_ORIGINS=https://support.example.com
PUBLIC_BASE_URL=https://support.example.com
WEBOTP_HOST=support.example.com
TIME_ZONE=Asia/Tehran
DATABASE_URL=postgresql://postgres:<URL_ENCODED_PASSWORD>@<INTERNAL_HOST>:5432/customerTicket
DB_CONN_MAX_AGE=60
DB_CONNECT_TIMEOUT=10
DB_SSLMODE=prefer
DATABASE_WAIT_TIMEOUT=60
STATIC_VOLUME_NAME=customer_ticket_static
MEDIA_VOLUME_NAME=customer_ticket_media
MEDIA_SIGNING_SECRET=<separate-random-secret-at-least-32-characters>
```
نام دیتابیس انتهای URL باید نام واقعی دیتابیس باشد. اگر دیتابیس ساخته‌شده `customerTicket` است، URL باید به `/customerTicket` ختم شود؛ URLی که به `/postgres` ختم می‌شود به دیتابیس پیش‌فرض `postgres` متصل خواهد شد. اگر password شامل `@`، `:`، `/`، `#` یا `%` است آن را URL-encode کنید.
می‌توانید به‌جای `DATABASE_URL` آن را خالی بگذارید و `POSTGRES_DB`، `POSTGRES_USER`، `POSTGRES_PASSWORD`، `POSTGRES_HOST` و `POSTGRES_PORT` را جداگانه تعریف کنید. یکی از این دو روش را انتخاب کنید.
## 4. تنظیم SMS
فقط provider مورد استفاده را انتخاب و secretهای همان provider را پر کنید.
### 0098SMS
```dotenv
SMS_PROVIDER_CLASS=apps.integrations.sms.sms0098.ZeroZeroNinetyEightSMSProvider
SMS0098_USERNAME=<username>
SMS0098_PASSWORD=<password>
SMS0098_FROM=<sender-number>
SMS0098_API_URL=http://www.0098sms.com/sendsmslink.aspx
```
### Behin SMS
```dotenv
SMS_PROVIDER_CLASS=apps.integrations.sms.behin.BehinSMSProvider
BEHIN_SMS_USERNAME=<username>
BEHIN_SMS_PASSWORD=<password>
BEHIN_SMS_SPECIAL_NUMBER=<sender-number>
BEHIN_SMS_WSDL_URL=http://panel.behinsms.com/SMSWS/SOAP.asmx?wsdl
```
### HTTP JSON provider
```dotenv
SMS_PROVIDER_CLASS=apps.integrations.sms.http.HTTPSMSProvider
SMS_API_URL=https://provider.example/api/messages
SMS_API_TOKEN=<token>
SMS_SENDER=<sender>
```
## 5. تنظیم Bale و worker
```dotenv
BALE_API_BASE=https://tapi.bale.ai
BALE_BOT_TOKEN=<bot-token>
BALE_GROUP_CHAT_ID=<group-chat-id>
INTEGRATION_HTTP_TIMEOUT_SECONDS=10
MAX_NOTIFICATION_ATTEMPTS=5
```
در production نبودن `BALE_BOT_TOKEN` خطای قابل‌مشاهده ایجاد می‌کند و job را بی‌صدا SENT علامت نمی‌زند. worker هر پنج ثانیه outbox دیتابیس را بررسی می‌کند، با قفل `skip_locked` از پردازش هم‌زمان جلوگیری می‌کند و خطاها را با backoff تا سقف `MAX_NOTIFICATION_ATTEMPTS` دوباره امتحان می‌کند.
سیاست تأخیر پیامک پاسخ کارشناس و اعلان پاسخ مشتری در Bale از مدل `NotificationPolicy` در پنل مدیریت دیتابیس کنترل می‌شود؛ متغیر قدیمی `STAFF_REPLY_SMS_THROTTLE_SECONDS` مرجع اصلی این رفتار نیست.
## 6. تنظیمات امنیتی
```dotenv
SECURE_SSL_REDIRECT=true
SECURE_HSTS_SECONDS=31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS=true
SECURE_HSTS_PRELOAD=false
```
`SECURE_HSTS_PRELOAD` را فقط وقتی true کنید که تمام subdomainها همیشه HTTPS هستند. `DJANGO_ALLOWED_HOSTS` فقط hostname و `CSRF_TRUSTED_ORIGINS` origin کامل همراه `https://` است. برای چند مقدار از کاما استفاده کنید.
## 7. ترتیب اولین deploy
1. volumeها و database خالی را ایجاد کنید.
2. Environment Variables را در Coolify وارد کنید.
3. Compose را deploy، دامنه اصلی را به `web:8000` و pathهای static/media گفته‌شده را به `nginx:80` متصل کنید.
4. log سرویس `web` را برای موفقیت migrate، bootstrap، collectstatic و Gunicorn بررسی کنید.
5. log سرویس `notification-worker` را بررسی کنید؛ container باید Running بماند.
6. یک‌بار superuser بسازید:
```bash
docker compose exec web python manage.py createsuperuser
```
7. در پنل مدیریت، SMS templateها و `NotificationPolicy`ها را بررسی کنید و با گیرنده آزمایشی مجاز یک SMS و یک پیام Bale واقعی بفرستید.
## 8. کنترل سلامت و عیب‌یابی
- `GET /healthz/` هم web server و هم اتصال دیتابیس را بررسی می‌کند.
- `docker compose ps` باید `web` و `nginx` را healthy و worker را Running نشان دهد.
- `docker compose logs --tail=200 web notification-worker nginx` خطاهای startup و provider را نمایش می‌دهد.
- وضعیت outbox را می‌توان در Django admin یا shell از مدل `NotificationJob` بررسی کرد؛ FAILED و `last_error` دلیل خطا را نگه می‌دارند.
- برای پردازش کنترل‌شده یک job مشخص: `python manage.py process_notifications --once --job-id <ID>`.
ارسال اعلان ماهیت at-least-once دارد: اگر provider پیام را بپذیرد ولی process پیش از ثبت نتیجه در PostgreSQL قطع شود، retry ممکن است پیام تکراری بسازد. `dedupe_key` جلوی ساخت job تکراری در برنامه را می‌گیرد، اما تضمین exactly-once به پشتیبانی idempotency از طرف provider نیاز دارد.
## 9. Mirrorهای ایرانی
Dockerfile به‌طور پیش‌فرض PyPI لیارا را از آدرس `https://package-mirror.liara.ir/repository/pypi/simple` و mirror Debian لیارا را استفاده می‌کند. اگر mirror Debian در محیط شما متفاوت است، build argument با نام `DEBIAN_MIRROR_HOST` را تغییر دهید. برای image مربوط به Nginx نیز می‌توانید `NGINX_IMAGE` را به مسیر registry mirror مورد اعتماد خود تغییر دهید.
+41
View File
@@ -0,0 +1,41 @@
# Organizational ticketing rollout
The organizational layer is additive: legacy tickets with no unit keep their existing
visibility rules, while every ticket assigned to a unit is isolated by unit membership
and database-managed Django permissions.
## Admin setup order
1. Create organizational units under **Staff → Organizational units**.
2. Select the Django permissions inherited by every member of each unit.
3. Assign staff through unit memberships and choose one primary unit.
4. Classify external contacts as customer, supplier, carrier, or other.
5. Create unit-owned contact groups and assign each contact to any number of groups.
6. Create optional unit-specific SMS templates. A template with no unit is the global
fallback. Supported variables include `recipient_name`, `unit_name`, `company_name`,
`magic_link`, `ticket_number`, and `subject`.
7. Grant `tickets.dispatch_group_tickets` to units allowed to create bulk dispatches.
Grant `accounts.view_all_magic_links` only to senior auditors who may inspect every
unit; other Admin users see only links from their active unit memberships.
Useful unit permissions include `tickets.view_ticket`, `tickets.claim_ticket`,
`tickets.transfer_ticket`, `tickets.share_ticket`, `tickets.change_ticket`,
`tickets.change_ticket_priority`, `tickets.delete_ticket`, and
`messaging.add_message`.
## Security invariants
- Each group recipient gets a separate `Ticket`, notification job, and magic link.
- External users can access only tickets whose `customer_id` is their own user ID.
- Unit filtering happens in both list querysets and object-level policy checks.
- Every valid magic-link opening appends an immutable access-log row and atomically
increments its view counter.
- Raw magic-link tokens are never stored; only SHA-256 digests are persisted.
- Group dispatch runs inside a database transaction, preventing partially-created
batches when validation or ticket creation fails.
Apply schema changes with:
```powershell
.\.venv\Scripts\python.exe manage.py migrate
```
@@ -0,0 +1,35 @@
# ممیزی حلقه عملیاتی سامانه تیکتینگ سازمانی
این سند وضعیت پیاده‌سازی را در برابر نیازمندی شرکت لوله گستر گلپایگان ثبت می‌کند. معیار «تکمیل» فقط وجود مدل یا صفحه نیست؛ مسیر باید از ایجاد داده تا مجوز، ارسال، مشاهده، پاسخ، ممیزی و گزارش‌گیری قابل آزمون باشد.
| حوزه | مسیر عملیاتی | وضعیت فعلی | شاهد اجرایی |
|---|---|---|---|
| RBAC واحدی | عضویت کاربر → permission واحد/عضو → queryset و object policy | تکمیل | `OrganizationalUnit`, `UnitMembership`, `TicketPolicy` و تست ایزوله‌سازی واحد |
| ایجاد تیکت سازمانی | انتخاب واحد مجاز → مخاطب/نوع/گروه → تیکت واحدی → SMS | تکمیل | فرم staff اکنون permission پویا و مالکیت واحد را اعمال می‌کند |
| مخاطبان چندگروهی | مخاطب → چند `ContactGroup` متعلق به واحدهای مجاز | تکمیل | رابطه Many-to-Many و فرم محدودشده به واحدهای قابل مدیریت |
| ارسال گروهی ایزوله | گروه → یک تیکت خصوصی برای هر مخاطب → یک job و Magic Link مستقل | تکمیل | `dispatch_group_tickets` و تست جداسازی گیرندگان |
| اسناد گروهی | یک یا چند سند → ذخیره مستقل داخل پیام اولیه هر تیکت | تکمیل | `files` در فرم dispatch و تست مسیر ذخیره مجزا |
| پاسخ خارجی | Magic Link بدون حساب → فقط همان تیکت → متن/فایل → اعلان Bale | تکمیل | session grant محدود به یک ticket و تست پاسخ ناشناس |
| ممیزی Magic Link | سازنده/واحد/گیرنده → هر بازدید با زمان/IP/UA → شمارنده | تکمیل | `TicketMagicLink`, `MagicLinkAccessLog` و تست بازدید |
| عملیات امنیتی لینک | فیلتر وضعیت/واحد/بازه → ابطال فوری با permission واحدی | تکمیل | staff audit، pagination و endpoint ابطال POST |
| گزارش ارسال گروهی | وضعیت gateway → بازشدن لینک → پاسخ مخاطب → مقایسه | تکمیل در سطح قابل اثبات | وضعیت پایدار dispatch، پاسخ آخر و شمارنده بازدید |
| بازیابی ارسال خطادار | FAILED → اقدام مجاز واحد → PENDING → worker retry | تکمیل | endpoint تلاش مجدد و تست عدم دسترسی واحد دیگر |
| الگوهای SMS | الگوی واحدی + fallback عمومی + متغیرهای کنترل‌شده | تکمیل | `SMSTemplate`, validation و فرم واحدی |
| فیلتر پیشرفته | واحدها، نوع ارسال، وضعیت، مخاطب، گروه‌ها، تاریخ و tag | تکمیل | `filter_staff_tickets` و UI فیلتر ترکیبی |
| پردازش غیرهمزمان | outbox قفل‌شونده → retry/backoff → SMS/Bale | تکمیل | `process_notifications`, `skip_locked` و سرویس worker در Compose |
| رهگیری provider | نتیجه gateway → شناسه مرجع ذخیره‌شده | تکمیل | `provider_message_id` در `NotificationJob` |
| Delivery Receipt مخابراتی | callback اپراتور → وضعیت تحویل گوشی | نیازمند قرارداد اپراتور | API فعلی 0098SMS فقط پذیرش درخواست را برمی‌گرداند؛ `SENT` به معنی پذیرش gateway است، نه تحویل قطعی |
## مرزهای امنیتی
- مخاطب بیرونی با Magic Link فقط به یک تیکت دسترسی می‌گیرد و به فهرست تیکت‌ها یا تیکت دیگر دسترسی ندارد.
- یادداشت داخلی و پیوست آن برای مشتری و session جادویی پنهان است.
- queryهای staff ابتدا با واحدهای قابل مشاهده محدود می‌شوند و سپس فیلترهای کاربر اعمال می‌شوند.
- ابطال لینک و retry ارسال فقط با permission پویا در همان واحد انجام می‌شود.
- شناسه provider ذخیره می‌شود، اما متن پیام، credential درگاه و token خام Magic Link در گزارش‌ها ذخیره یا نمایش داده نمی‌شود.
## الزام استقرار
- `web` و `notification-worker` باید همزمان و تحت supervisor یا Docker Compose با restart policy اجرا شوند.
- production باید از `config.settings.prod`، HTTPS، secret قوی و `PUBLIC_BASE_URL` عمومی HTTPS استفاده کند.
- برای ادعای «تحویل به گوشی»، endpoint و امضای webhook اپراتور منتخب باید مشخص شود؛ تا آن زمان UI باید بین پذیرش gateway و بازشدن لینک تفاوت بگذارد.
+41
View File
@@ -0,0 +1,41 @@
# QA permission and representative matrix
This matrix is the maintained regression contract for role, unit, permission, scope,
and primary workflows. Representatives are deterministic records created by
`python manage.py bootstrap`; tests do not depend on mutable production data.
| Unit | Role | Representative | Scope | Expected access |
| --- | --- | --- | --- | --- |
| فروش | کارشناس فروش | کیمیا غفاری (`kimiya_ghaffari`) | `SELF` | Public unassigned sales tickets, tickets created by or assigned to self, reply in that scope |
| فروش | سرپرست فروش | مریم فتوت (`maryam_fotovat`) | `UNIT` | All sales-unit tickets and normal sales workflows |
| فروش | مدیر فروش | مرتضی سخائی فرد (`morteza_sokhaeifard`) | `UNIT` + manager role permissions | All sales-unit tickets and manager actions |
| بارگیری انبار | کارشناس بارگیری | خانم سبزعلی (`ms_sabzali`) | `UNIT` | Warehouse-loading-loading tickets only; create bulk/inquiry and view own operations |
| مالی / حسابداری | کارشناس مالی | خانم فرشته یاوری (`fereshteh_yavari`) | `UNIT` | Finance tickets, categories, and contact groups only |
| مدیریت ارشد | مدیرعامل | مصطفی سخائی فرد (`mostafa_sokhaeifard`) | Broad/global explicit permissions | All tickets, all bulk dispatches, all inquiries |
## Permission coverage contract
| Capability | Limited scope | Unit scope | Broad scope |
| --- | --- | --- | --- |
| Ticket view/reply | Public unassigned + created/assigned self | All permitted tickets in own unit | `view_all_tickets` |
| Staff ticket creation | Primary unit only | Primary unit only | Explicit `add_ticket`; unit/category validation still applies |
| Category / expert / queue | Selected/primary unit only | Selected/primary unit only | Still validated against selected unit |
| Contact group | Primary unit only | Primary unit only | Explicit permission plus unit validation |
| Bulk ticket | `create_bulk_dispatch`, own records | Same within permitted unit | `view_all_bulk_dispatches` |
| Inquiry | `create_inquiry`, own records | Same within permitted unit | `view_all_inquiries` |
The public “general queue” is a routing state, not a separate database model: it is
stored as the selected unit with `selected_expert = NULL`.
## Permanent commands
```powershell
# Backend/API/integration regression
.\.venv\Scripts\python.exe -m pytest tests -m "not e2e" -q
# Local real-browser regression (managed Chromium + isolated live_server)
.\.venv\Scripts\python.exe -m pytest tests\e2e -m "e2e and not external_e2e" -q
# Read-only deployed-environment smoke (requires environment variables in README)
.\.venv\Scripts\python.exe -m pytest tests\e2e\test_external_environment_smoke.py -m external_e2e -q
```
+26
View File
@@ -0,0 +1,26 @@
# Central color theme
The application uses one cohesive blue/navy theme. All colors are centralized in `static/src/tailwind.css`; templates do not define brand hex colors.
## Palette and roles
- `--color-ink: #08376b` — navigation, headings and structural contrast
- `--color-sea: #0693e3` — primary actions, links and active states
- `--color-sea-dark: #1977ce` — depth, secondary blue and hover states
- `--color-coral: #fa9f00` — selective attention, highlights and warnings
- `--color-cream: #eeeeee` — page background
- `--color-soft: #eaeaea` — secondary surfaces and conversation canvas
- `--color-paper: #ffffff` — elevated cards
- `--color-mint`, `--color-sun` — supporting light-blue and warm tints
Conversation surfaces and shadows are also variables in the same `:root` block.
## Changing the theme later
Edit only the `@theme` and `:root` token values near the top of `static/src/tailwind.css`, then run:
```powershell
npm run css:build
```
Component rules consume semantic names such as `ink`, `sea`, `coral`, and `paper`, so no template rewrite is required.