From fa9d154db916ffcdd524073da7153881ca5edf47 Mon Sep 17 00:00:00 2001 From: javad Date: Thu, 10 Sep 2026 11:34:19 +0330 Subject: [PATCH] first --- .dockerignore | 26 + .env.example | 46 + .env.local.example | 12 + .gitignore | 70 + AGENTS.md | 42 + Dockerfile | 46 + PROJECT_TREE.txt | 738 +++ README.md | 45 + ...ture Specification — سیستم تیکتینگ فروش.md | 4029 +++++++++++++++++ apps/__init__.py | 0 apps/accounts/__init__.py | 0 apps/accounts/admin.py | 72 + apps/accounts/apps.py | 4 + apps/accounts/forms.py | 35 + apps/accounts/magic_access.py | 32 + apps/accounts/migrations/0001_initial.py | 78 + apps/accounts/migrations/0002_initial.py | 22 + ..._alter_ticketmagiclink_options_and_more.py | 38 + ...4_ticketmagiclink_created_unit_and_more.py | 45 + ...linkaccesslog_magic_opened_idx_and_more.py | 27 + apps/accounts/migrations/__init__.py | 0 apps/accounts/models.py | 157 + apps/accounts/urls.py | 4 + apps/accounts/views.py | 131 + apps/attachments/__init__.py | 0 apps/attachments/admin.py | 3 + apps/attachments/apps.py | 2 + apps/attachments/delivery.py | 21 + apps/attachments/migrations/0001_initial.py | 49 + apps/attachments/migrations/0002_initial.py | 29 + .../migrations/0003_alter_attachment_file.py | 19 + apps/attachments/migrations/__init__.py | 0 apps/attachments/models.py | 14 + apps/attachments/services.py | 32 + apps/attachments/urls.py | 4 + apps/attachments/views.py | 30 + apps/core/__init__.py | 0 apps/core/admin.py | 37 + apps/core/apps.py | 4 + apps/core/context_processors.py | 68 + apps/core/iran_locations.py | 36 + apps/core/management/__init__.py | 0 apps/core/management/commands/__init__.py | 0 apps/core/management/commands/bootstrap.py | 147 + .../commands/normalize_persian_data.py | 30 + apps/core/middleware.py | 13 + apps/core/migrations/0001_initial.py | 47 + .../0002_seed_branding_and_page_titles.py | 40 + ...branding_developer_credit_html_and_more.py | 53 + ...ding_staff_sidebar_credit_html_and_more.py | 28 + ...0005_sitebranding_company_logo_and_more.py | 93 + apps/core/migrations/__init__.py | 0 apps/core/models.py | 127 + apps/core/persian_text.py | 66 + apps/core/templatetags/__init__.py | 1 + apps/core/templatetags/jalali.py | 53 + apps/customers/__init__.py | 0 apps/customers/admin.py | 17 + apps/customers/apps.py | 2 + apps/customers/migrations/0001_initial.py | 32 + ...erprofile_city_customerprofile_province.py | 9 + ...tomerprofile_organization_name_and_more.py | 41 + ...up_unit_customerprofile_groups_and_more.py | 29 + apps/customers/migrations/__init__.py | 0 apps/customers/models.py | 54 + apps/integrations/__init__.py | 0 apps/integrations/bale/__init__.py | 0 apps/integrations/bale/providers.py | 30 + apps/integrations/sms/__init__.py | 0 apps/integrations/sms/base.py | 18 + apps/integrations/sms/behin.py | 69 + apps/integrations/sms/console.py | 10 + apps/integrations/sms/factory.py | 10 + apps/integrations/sms/http.py | 13 + apps/integrations/sms/providers.py | 7 + apps/integrations/sms/sms0098.py | 36 + apps/messaging/__init__.py | 0 apps/messaging/admin.py | 3 + apps/messaging/apps.py | 2 + apps/messaging/migrations/0001_initial.py | 53 + apps/messaging/migrations/0002_initial.py | 48 + .../migrations/0003_messagereadreceipt.py | 29 + apps/messaging/migrations/__init__.py | 0 apps/messaging/models.py | 31 + apps/messaging/services.py | 21 + apps/messaging/urls.py | 5 + apps/messaging/views.py | 28 + apps/notifications/__init__.py | 0 apps/notifications/admin.py | 13 + apps/notifications/apps.py | 2 + apps/notifications/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/process_notifications.py | 164 + apps/notifications/migrations/0001_initial.py | 37 + .../migrations/0002_smstemplate.py | 40 + ...tificationpolicy_notificationjob_status.py | 31 + .../0004_customer_reply_bale_policy.py | 27 + ...t_alter_smstemplate_event_type_and_more.py | 33 + .../0006_alter_smstemplate_allowed_tags.py | 29 + ..._notificationjob_notif_due_idx_and_more.py | 24 + ...008_notificationjob_provider_message_id.py | 18 + .../0009_ensure_global_sms_templates.py | 20 + apps/notifications/migrations/__init__.py | 0 apps/notifications/models.py | 50 + apps/notifications/services.py | 53 + apps/staff/__init__.py | 0 apps/staff/admin.py | 51 + apps/staff/apps.py | 2 + apps/staff/forms.py | 31 + apps/staff/migrations/0001_initial.py | 37 + ...st_name_staffprofile_job_title_and_more.py | 28 + .../migrations/0003_staffprofile_role.py | 18 + ...unit_staffprofile_primary_unit_and_more.py | 61 + ...nizationalunit_accepts_external_tickets.py | 18 + ...ter_organizationalunit_options_and_more.py | 27 + ...rganizationalunit_show_in_customer_form.py | 18 + ...8_unitmembership_ticket_scope_and_roles.py | 36 + .../0009_alter_staffprofile_sort_order.py | 18 + apps/staff/migrations/__init__.py | 0 apps/staff/models.py | 155 + apps/tickets/__init__.py | 0 apps/tickets/admin.py | 39 + apps/tickets/apps.py | 2 + apps/tickets/category_catalog.py | 41 + apps/tickets/customer_urls.py | 4 + apps/tickets/forms.py | 246 + apps/tickets/migrations/0001_initial.py | 130 + .../0002_alter_ticketreadstate_ticket.py | 6 + .../0003_ticket_city_ticket_province.py | 9 + ...ssigned_unit_ticket_created_by_and_more.py | 102 + .../0005_bootstrap_legacy_staff_unit.py | 53 + .../0006_assign_legacy_contacts_to_sales.py | 21 + ...ketcategory_options_ticketcategory_unit.py | 24 + .../0008_assign_categories_to_default_unit.py | 14 + ...ed_customer_facing_units_and_categories.py | 37 + ...0_alter_ticketcategory_options_and_more.py | 28 + ...11_ticketcategory_show_in_customer_form.py | 18 + ...atch_operation_permissions_and_seed_org.py | 124 + .../0013_merge_legacy_accounting_unit.py | 75 + .../0014_merge_legacy_warehouse_units.py | 76 + .../0015_seed_customer_category_catalog.py | 69 + apps/tickets/migrations/__init__.py | 0 apps/tickets/models.py | 140 + apps/tickets/policies.py | 163 + apps/tickets/selectors.py | 70 + apps/tickets/services.py | 207 + apps/tickets/staff_urls.py | 4 + apps/tickets/views.py | 766 ++++ config/__init__.py | 0 config/asgi.py | 5 + config/env.py | 43 + config/settings/__init__.py | 0 config/settings/base.py | 125 + config/settings/dev.py | 9 + config/settings/prod.py | 24 + config/urls.py | 64 + config/wsgi.py | 5 + docker-compose.yml | 105 + docker/entrypoint.sh | 49 + docker/nginx.conf | 43 + docs/DEPLOYMENT_COOLIFY.md | 142 + docs/ORGANIZATIONAL_TICKETING.md | 41 + docs/ORGANIZATIONAL_TICKETING_GAP_ANALYSIS.md | 35 + docs/QA_PERMISSION_MATRIX.md | 41 + docs/THEMING.md | 26 + manage.py | 8 + package-lock.json | 1131 +++++ package.json | 26 + project_tree.py | 263 ++ pytest.ini | 7 + requirements-dev.txt | 4 + requirements.txt | 10 + static/css/app.css | 1 + static/css/dispatches.css | 2 + static/css/enhancements.css | 11 + static/css/operations.css | 45 + static/css/tailwind.css | 2 + static/css/ticket-composer-v2.css | 7 + static/css/ticket-composer.css | 1 + static/data/iran-locations.LICENSE.md | 9 + static/data/iran-locations.json | 1 + .../fonts/vazirmatn-arabic-wght-normal.woff2 | Bin 0 -> 46308 bytes .../fonts/vazirmatn-latin-wght-normal.woff2 | Bin 0 -> 34524 bytes static/images/company-logo.png | Bin 0 -> 11649 bytes static/images/favicon.ico | Bin 0 -> 1150 bytes static/images/pwa-icon.png | Bin 0 -> 1516423 bytes static/js/attachment-uploader.js | 18 + static/js/chat-coach.js | 42 + static/js/conversation.js | 132 + static/js/image-viewer.js | 1 + static/js/location-picker.js | 38 + static/js/pwa.js | 15 + static/js/staff-customer-picker.js | 21 + static/js/ticket-intent-search.js | 20 + static/js/ticket-unit-options.js | 62 + static/js/ui.js | 8 + static/js/voice-recorder.js | 12 + static/js/webotp.js | 16 + static/manifest.webmanifest | 1 + static/src/tailwind.css | 134 + static/vendor/alpine-3.14.8.min.js | 5 + static/vendor/compressor.min.js | 10 + static/vendor/htmx-2.0.3.min.js | 1 + templates/accounts/customer_login.html | 10 + .../accounts/customer_password_login.html | 10 + templates/accounts/otp.html | 11 + templates/accounts/profile.html | 20 + templates/accounts/staff_login.html | 10 + templates/base.html | 70 + templates/includes/auth_brand.html | 4 + templates/includes/auth_footer.html | 1 + templates/messaging/revisions.html | 12 + templates/pwa/service-worker.js | 6 + templates/tickets/_image_viewer.html | 5 + templates/tickets/_messages.html | 22 + templates/tickets/_older_messages.html | 2 + .../tickets/_staff_customer_results.html | 5 + templates/tickets/_staff_ticket_results.html | 18 + templates/tickets/customer_detail.html | 28 + templates/tickets/customer_list.html | 28 + templates/tickets/customer_new.html | 31 + templates/tickets/staff_contacts.html | 7 + templates/tickets/staff_create.html | 24 + templates/tickets/staff_customers.html | 9 + templates/tickets/staff_detail.html | 37 + templates/tickets/staff_dispatch_create.html | 16 + templates/tickets/staff_dispatch_detail.html | 24 + templates/tickets/staff_entity_form.html | 12 + templates/tickets/staff_groups.html | 6 + templates/tickets/staff_list.html | 55 + templates/tickets/staff_magic_audit.html | 6 + templates/tickets/staff_sms_templates.html | 7 + tests/e2e/README.md | 111 + tests/e2e/config.py | 36 + tests/e2e/conftest.py | 77 + tests/e2e/pages/__init__.py | 1 + tests/e2e/pages/auth.py | 20 + tests/e2e/pages/base.py | 13 + tests/e2e/pages/customer_new_ticket.py | 31 + tests/e2e/pages/tickets.py | 120 + tests/e2e/test_chat_coach.py | 78 + .../e2e/test_customer_unit_categories_e2e.py | 73 + tests/e2e/test_external_environment_smoke.py | 51 + .../e2e/test_organization_permissions_e2e.py | 245 + tests/e2e/test_public_ticket_workflows_e2e.py | 290 ++ tests/e2e/test_pwa_versioning.py | 42 + tests/e2e/test_staff_operations_ui.py | 88 + tests/js/chat-coach.test.mjs | 67 + tests/test_branding_operations.py | 149 + tests/test_customer_category_catalog.py | 78 + tests/test_customer_unit_categories.py | 162 + tests/test_deployment.py | 52 + tests/test_domain.py | 392 ++ tests/test_env.py | 36 + tests/test_jalali_templates.py | 16 + tests/test_organizational_ticketing.py | 232 + tests/test_permission_architecture.py | 233 + tests/test_persian_text.py | 43 + tests/test_role_workflow_regression.py | 194 + tests/test_views.py | 545 +++ 260 files changed, 17643 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .env.local.example create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Dockerfile create mode 100644 PROJECT_TREE.txt create mode 100644 README.md create mode 100644 Software Requirements & Architecture Specification — سیستم تیکتینگ فروش.md create mode 100644 apps/__init__.py create mode 100644 apps/accounts/__init__.py create mode 100644 apps/accounts/admin.py create mode 100644 apps/accounts/apps.py create mode 100644 apps/accounts/forms.py create mode 100644 apps/accounts/magic_access.py create mode 100644 apps/accounts/migrations/0001_initial.py create mode 100644 apps/accounts/migrations/0002_initial.py create mode 100644 apps/accounts/migrations/0003_magiclinkaccesslog_alter_ticketmagiclink_options_and_more.py create mode 100644 apps/accounts/migrations/0004_ticketmagiclink_created_unit_and_more.py create mode 100644 apps/accounts/migrations/0005_magiclinkaccesslog_magic_opened_idx_and_more.py create mode 100644 apps/accounts/migrations/__init__.py create mode 100644 apps/accounts/models.py create mode 100644 apps/accounts/urls.py create mode 100644 apps/accounts/views.py create mode 100644 apps/attachments/__init__.py create mode 100644 apps/attachments/admin.py create mode 100644 apps/attachments/apps.py create mode 100644 apps/attachments/delivery.py create mode 100644 apps/attachments/migrations/0001_initial.py create mode 100644 apps/attachments/migrations/0002_initial.py create mode 100644 apps/attachments/migrations/0003_alter_attachment_file.py create mode 100644 apps/attachments/migrations/__init__.py create mode 100644 apps/attachments/models.py create mode 100644 apps/attachments/services.py create mode 100644 apps/attachments/urls.py create mode 100644 apps/attachments/views.py create mode 100644 apps/core/__init__.py create mode 100644 apps/core/admin.py create mode 100644 apps/core/apps.py create mode 100644 apps/core/context_processors.py create mode 100644 apps/core/iran_locations.py create mode 100644 apps/core/management/__init__.py create mode 100644 apps/core/management/commands/__init__.py create mode 100644 apps/core/management/commands/bootstrap.py create mode 100644 apps/core/management/commands/normalize_persian_data.py create mode 100644 apps/core/middleware.py create mode 100644 apps/core/migrations/0001_initial.py create mode 100644 apps/core/migrations/0002_seed_branding_and_page_titles.py create mode 100644 apps/core/migrations/0003_sitebranding_developer_credit_html_and_more.py create mode 100644 apps/core/migrations/0004_sitebranding_staff_sidebar_credit_html_and_more.py create mode 100644 apps/core/migrations/0005_sitebranding_company_logo_and_more.py create mode 100644 apps/core/migrations/__init__.py create mode 100644 apps/core/models.py create mode 100644 apps/core/persian_text.py create mode 100644 apps/core/templatetags/__init__.py create mode 100644 apps/core/templatetags/jalali.py create mode 100644 apps/customers/__init__.py create mode 100644 apps/customers/admin.py create mode 100644 apps/customers/apps.py create mode 100644 apps/customers/migrations/0001_initial.py create mode 100644 apps/customers/migrations/0002_customerprofile_city_customerprofile_province.py create mode 100644 apps/customers/migrations/0003_contactgroup_customerprofile_organization_name_and_more.py create mode 100644 apps/customers/migrations/0004_contactgroup_unit_customerprofile_groups_and_more.py create mode 100644 apps/customers/migrations/__init__.py create mode 100644 apps/customers/models.py create mode 100644 apps/integrations/__init__.py create mode 100644 apps/integrations/bale/__init__.py create mode 100644 apps/integrations/bale/providers.py create mode 100644 apps/integrations/sms/__init__.py create mode 100644 apps/integrations/sms/base.py create mode 100644 apps/integrations/sms/behin.py create mode 100644 apps/integrations/sms/console.py create mode 100644 apps/integrations/sms/factory.py create mode 100644 apps/integrations/sms/http.py create mode 100644 apps/integrations/sms/providers.py create mode 100644 apps/integrations/sms/sms0098.py create mode 100644 apps/messaging/__init__.py create mode 100644 apps/messaging/admin.py create mode 100644 apps/messaging/apps.py create mode 100644 apps/messaging/migrations/0001_initial.py create mode 100644 apps/messaging/migrations/0002_initial.py create mode 100644 apps/messaging/migrations/0003_messagereadreceipt.py create mode 100644 apps/messaging/migrations/__init__.py create mode 100644 apps/messaging/models.py create mode 100644 apps/messaging/services.py create mode 100644 apps/messaging/urls.py create mode 100644 apps/messaging/views.py create mode 100644 apps/notifications/__init__.py create mode 100644 apps/notifications/admin.py create mode 100644 apps/notifications/apps.py create mode 100644 apps/notifications/management/__init__.py create mode 100644 apps/notifications/management/commands/__init__.py create mode 100644 apps/notifications/management/commands/process_notifications.py create mode 100644 apps/notifications/migrations/0001_initial.py create mode 100644 apps/notifications/migrations/0002_smstemplate.py create mode 100644 apps/notifications/migrations/0003_notificationpolicy_notificationjob_status.py create mode 100644 apps/notifications/migrations/0004_customer_reply_bale_policy.py create mode 100644 apps/notifications/migrations/0005_smstemplate_unit_alter_smstemplate_event_type_and_more.py create mode 100644 apps/notifications/migrations/0006_alter_smstemplate_allowed_tags.py create mode 100644 apps/notifications/migrations/0007_notificationjob_notif_due_idx_and_more.py create mode 100644 apps/notifications/migrations/0008_notificationjob_provider_message_id.py create mode 100644 apps/notifications/migrations/0009_ensure_global_sms_templates.py create mode 100644 apps/notifications/migrations/__init__.py create mode 100644 apps/notifications/models.py create mode 100644 apps/notifications/services.py create mode 100644 apps/staff/__init__.py create mode 100644 apps/staff/admin.py create mode 100644 apps/staff/apps.py create mode 100644 apps/staff/forms.py create mode 100644 apps/staff/migrations/0001_initial.py create mode 100644 apps/staff/migrations/0002_staffprofile_first_name_staffprofile_job_title_and_more.py create mode 100644 apps/staff/migrations/0003_staffprofile_role.py create mode 100644 apps/staff/migrations/0004_organizationalunit_staffprofile_primary_unit_and_more.py create mode 100644 apps/staff/migrations/0005_organizationalunit_accepts_external_tickets.py create mode 100644 apps/staff/migrations/0006_alter_organizationalunit_options_and_more.py create mode 100644 apps/staff/migrations/0007_organizationalunit_show_in_customer_form.py create mode 100644 apps/staff/migrations/0008_unitmembership_ticket_scope_and_roles.py create mode 100644 apps/staff/migrations/0009_alter_staffprofile_sort_order.py create mode 100644 apps/staff/migrations/__init__.py create mode 100644 apps/staff/models.py create mode 100644 apps/tickets/__init__.py create mode 100644 apps/tickets/admin.py create mode 100644 apps/tickets/apps.py create mode 100644 apps/tickets/category_catalog.py create mode 100644 apps/tickets/customer_urls.py create mode 100644 apps/tickets/forms.py create mode 100644 apps/tickets/migrations/0001_initial.py create mode 100644 apps/tickets/migrations/0002_alter_ticketreadstate_ticket.py create mode 100644 apps/tickets/migrations/0003_ticket_city_ticket_province.py create mode 100644 apps/tickets/migrations/0004_tickettag_ticket_assigned_unit_ticket_created_by_and_more.py create mode 100644 apps/tickets/migrations/0005_bootstrap_legacy_staff_unit.py create mode 100644 apps/tickets/migrations/0006_assign_legacy_contacts_to_sales.py create mode 100644 apps/tickets/migrations/0007_alter_ticketcategory_options_ticketcategory_unit.py create mode 100644 apps/tickets/migrations/0008_assign_categories_to_default_unit.py create mode 100644 apps/tickets/migrations/0009_seed_customer_facing_units_and_categories.py create mode 100644 apps/tickets/migrations/0010_alter_ticketcategory_options_and_more.py create mode 100644 apps/tickets/migrations/0011_ticketcategory_show_in_customer_form.py create mode 100644 apps/tickets/migrations/0012_groupdispatch_operation_permissions_and_seed_org.py create mode 100644 apps/tickets/migrations/0013_merge_legacy_accounting_unit.py create mode 100644 apps/tickets/migrations/0014_merge_legacy_warehouse_units.py create mode 100644 apps/tickets/migrations/0015_seed_customer_category_catalog.py create mode 100644 apps/tickets/migrations/__init__.py create mode 100644 apps/tickets/models.py create mode 100644 apps/tickets/policies.py create mode 100644 apps/tickets/selectors.py create mode 100644 apps/tickets/services.py create mode 100644 apps/tickets/staff_urls.py create mode 100644 apps/tickets/views.py create mode 100644 config/__init__.py create mode 100644 config/asgi.py create mode 100644 config/env.py create mode 100644 config/settings/__init__.py create mode 100644 config/settings/base.py create mode 100644 config/settings/dev.py create mode 100644 config/settings/prod.py create mode 100644 config/urls.py create mode 100644 config/wsgi.py create mode 100644 docker-compose.yml create mode 100644 docker/entrypoint.sh create mode 100644 docker/nginx.conf create mode 100644 docs/DEPLOYMENT_COOLIFY.md create mode 100644 docs/ORGANIZATIONAL_TICKETING.md create mode 100644 docs/ORGANIZATIONAL_TICKETING_GAP_ANALYSIS.md create mode 100644 docs/QA_PERMISSION_MATRIX.md create mode 100644 docs/THEMING.md create mode 100644 manage.py create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 project_tree.py create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 static/css/app.css create mode 100644 static/css/dispatches.css create mode 100644 static/css/enhancements.css create mode 100644 static/css/operations.css create mode 100644 static/css/tailwind.css create mode 100644 static/css/ticket-composer-v2.css create mode 100644 static/css/ticket-composer.css create mode 100644 static/data/iran-locations.LICENSE.md create mode 100644 static/data/iran-locations.json create mode 100644 static/fonts/vazirmatn-arabic-wght-normal.woff2 create mode 100644 static/fonts/vazirmatn-latin-wght-normal.woff2 create mode 100644 static/images/company-logo.png create mode 100644 static/images/favicon.ico create mode 100644 static/images/pwa-icon.png create mode 100644 static/js/attachment-uploader.js create mode 100644 static/js/chat-coach.js create mode 100644 static/js/conversation.js create mode 100644 static/js/image-viewer.js create mode 100644 static/js/location-picker.js create mode 100644 static/js/pwa.js create mode 100644 static/js/staff-customer-picker.js create mode 100644 static/js/ticket-intent-search.js create mode 100644 static/js/ticket-unit-options.js create mode 100644 static/js/ui.js create mode 100644 static/js/voice-recorder.js create mode 100644 static/js/webotp.js create mode 100644 static/manifest.webmanifest create mode 100644 static/src/tailwind.css create mode 100644 static/vendor/alpine-3.14.8.min.js create mode 100644 static/vendor/compressor.min.js create mode 100644 static/vendor/htmx-2.0.3.min.js create mode 100644 templates/accounts/customer_login.html create mode 100644 templates/accounts/customer_password_login.html create mode 100644 templates/accounts/otp.html create mode 100644 templates/accounts/profile.html create mode 100644 templates/accounts/staff_login.html create mode 100644 templates/base.html create mode 100644 templates/includes/auth_brand.html create mode 100644 templates/includes/auth_footer.html create mode 100644 templates/messaging/revisions.html create mode 100644 templates/pwa/service-worker.js create mode 100644 templates/tickets/_image_viewer.html create mode 100644 templates/tickets/_messages.html create mode 100644 templates/tickets/_older_messages.html create mode 100644 templates/tickets/_staff_customer_results.html create mode 100644 templates/tickets/_staff_ticket_results.html create mode 100644 templates/tickets/customer_detail.html create mode 100644 templates/tickets/customer_list.html create mode 100644 templates/tickets/customer_new.html create mode 100644 templates/tickets/staff_contacts.html create mode 100644 templates/tickets/staff_create.html create mode 100644 templates/tickets/staff_customers.html create mode 100644 templates/tickets/staff_detail.html create mode 100644 templates/tickets/staff_dispatch_create.html create mode 100644 templates/tickets/staff_dispatch_detail.html create mode 100644 templates/tickets/staff_entity_form.html create mode 100644 templates/tickets/staff_groups.html create mode 100644 templates/tickets/staff_list.html create mode 100644 templates/tickets/staff_magic_audit.html create mode 100644 templates/tickets/staff_sms_templates.html create mode 100644 tests/e2e/README.md create mode 100644 tests/e2e/config.py create mode 100644 tests/e2e/conftest.py create mode 100644 tests/e2e/pages/__init__.py create mode 100644 tests/e2e/pages/auth.py create mode 100644 tests/e2e/pages/base.py create mode 100644 tests/e2e/pages/customer_new_ticket.py create mode 100644 tests/e2e/pages/tickets.py create mode 100644 tests/e2e/test_chat_coach.py create mode 100644 tests/e2e/test_customer_unit_categories_e2e.py create mode 100644 tests/e2e/test_external_environment_smoke.py create mode 100644 tests/e2e/test_organization_permissions_e2e.py create mode 100644 tests/e2e/test_public_ticket_workflows_e2e.py create mode 100644 tests/e2e/test_pwa_versioning.py create mode 100644 tests/e2e/test_staff_operations_ui.py create mode 100644 tests/js/chat-coach.test.mjs create mode 100644 tests/test_branding_operations.py create mode 100644 tests/test_customer_category_catalog.py create mode 100644 tests/test_customer_unit_categories.py create mode 100644 tests/test_deployment.py create mode 100644 tests/test_domain.py create mode 100644 tests/test_env.py create mode 100644 tests/test_jalali_templates.py create mode 100644 tests/test_organizational_ticketing.py create mode 100644 tests/test_permission_architecture.py create mode 100644 tests/test_persian_text.py create mode 100644 tests/test_role_workflow_regression.py create mode 100644 tests/test_views.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..43cc7e0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +.git +.venv +venv +.env* +*.sqlite3 +*.sqlite3-* +media +staticfiles +node_modules +graphify-out +test-results +playwright-report +.pytest_cache +__pycache__ +*.py[cod] +tests +docs +.idea +.vscode +npm-debug.log* +PROJECT_TREE.txt +project_tree.py +package-lock.json +package.json +requirements-dev.txt +.pytest_cache diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7ff98f5 --- /dev/null +++ b/.env.example @@ -0,0 +1,46 @@ +DJANGO_SETTINGS_MODULE=config.settings.prod +DJANGO_SECRET_KEY=replace-with-a-unique-random-secret-of-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 + +# Prefer DATABASE_URL in Coolify. Percent-encode URL-special password characters. +DATABASE_URL=postgresql://postgres:replace-me@managed-postgres-host:5432/customerTicket +# These fields are used only when DATABASE_URL is empty. +POSTGRES_DB=customerTicket +POSTGRES_USER=postgres +POSTGRES_PASSWORD= +POSTGRES_HOST= +POSTGRES_PORT=5432 +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=replace-with-a-separate-random-secret-of-at-least-32-characters + +SMS_PROVIDER_CLASS=apps.integrations.sms.sms0098.ZeroZeroNinetyEightSMSProvider +SMS0098_USERNAME= +SMS0098_PASSWORD= +SMS0098_FROM= +SMS0098_API_URL=http://www.0098sms.com/sendsmslink.aspx +BEHIN_SMS_USERNAME= +BEHIN_SMS_PASSWORD= +BEHIN_SMS_SPECIAL_NUMBER= +BEHIN_SMS_WSDL_URL=http://panel.behinsms.com/SMSWS/SOAP.asmx?wsdl +SMS_API_URL=https://sms-provider.example/api/messages +SMS_API_TOKEN=replace-me +SMS_SENDER= +BALE_API_BASE=https://tapi.bale.ai +BALE_BOT_TOKEN= +BALE_GROUP_CHAT_ID=5924558401 +INTEGRATION_HTTP_TIMEOUT_SECONDS=10 +MAX_NOTIFICATION_ATTEMPTS=5 +SECURE_SSL_REDIRECT=true +SECURE_HSTS_SECONDS=31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS=true +SECURE_HSTS_PRELOAD=false diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..1ce9572 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,12 @@ +# Copy to .env.local for direct local development. Never commit real tokens. +DJANGO_SETTINGS_MODULE=config.settings.dev +USE_SQLITE=true +DJANGO_DEBUG=true +PUBLIC_BASE_URL=http://127.0.0.1:8000 +DEV_OTP_MOBILE=+989202202129 +DEV_OTP_CODE=12345 +SMS_PROVIDER_CLASS=apps.integrations.sms.console.ConsoleSMSProvider +BALE_API_BASE=https://tapi.bale.ai +BALE_BOT_TOKEN= +BALE_GROUP_CHAT_ID=5924558401 +INTEGRATION_HTTP_TIMEOUT_SECONDS=10 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e65b9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# Python bytecode and interpreter caches +__pycache__/ +*.py[cod] +*$py.class +.python-version + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# Python packaging and build artifacts +build/ +dist/ +*.egg-info/ +.eggs/ +pip-wheel-metadata/ + +# Test, coverage, lint, and type-check caches +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +.tox/ +.nox/ +.mypy_cache/ +.ruff_cache/ +.hypothesis/ +test-results/ +playwright-report/ +blob-report/ + +# Django runtime data +*.sqlite3 +*.sqlite3-* +media/ +staticfiles/ +*.log + +# Local environment and secrets; keep documented templates +.env +.env.* +!.env.example +!.env.*.example + +# Node/npm generated and machine-local files +node_modules/ +.npm/ +.npmrc +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.parcel-cache/ + +# Graphify generated graph, cache, reports, and memories +graphify-out/ + +# IDE/editor settings and temporary files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Operating-system metadata +.DS_Store +Thumbs.db +Desktop.ini diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8146cd5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# Project operating rules + +## Development server + +- Always bind the Django development server to `0.0.0.0:8000` so the project is reachable from phones and other devices through the computer's local-network IP address. Do not start it on `127.0.0.1` unless the user explicitly requests localhost-only access. +- Before starting or restarting Django on port `8000`, inspect the port and identify every existing listener. +- Stop the previous project server and its parent/child process tree before launching the replacement. +- After launch, verify that exactly one process is listening on port `8000`. +- Verify the listener is bound to `0.0.0.0:8000`, then fetch the site locally through `127.0.0.1:8000` as a health check. +- Fetch a changed page/static asset from the live server and confirm the expected new content is actually being served. +- When frontend assets change, remind the user to perform a hard refresh, but do not treat browser cache as the cause until the live listener and served content have been verified. +- Keep the active server session identifier and shut it down cleanly before starting another instance. + +## Graphify queries + +- Treat questions about this codebase as Graphify queries first whenever `graphify-out/graph.json` exists. +- Before querying, expand the user's Persian question against the actual vocabulary in `graphify-out/graph.json`. +- Use the best combination of Persian and English terms that are present in the graph vocabulary; never rely on automatic translation, stemming, or synonyms inside Graphify. +- For code queries, prioritize the graph's English identifiers (class, function, model, template, route, and file names), because the structural code graph is predominantly English; add Persian terms when they actually exist in the graph, especially for requirements and documentation. +- Choose the traversal deliberately: BFS for broad relationships, DFS for dependency/data-flow traces, `path` for a shortest connection, and `explain` for one node and its neighbors. +- Validate important Graphify findings against the referenced source code or project documentation before acting on them. + +## Python package mirror + +- Use Liara's PyPI mirror for Python dependency resolution and installation: `https://package-mirror.liara.ir/repository/pypi/simple`. +- Prefer the persistent user-level pip configuration over repeating `--index-url` in individual commands. +- If the mirror is temporarily unavailable or missing a required package/version, report it before using the public PyPI index as a fallback. + +## Frontend and PWA cache safety + +- Every locally served frontend asset in templates must include `static_asset_version` in its URL. +- Keep the service-worker registration URL versioned with the same value and keep `updateViaCache: 'none'`; changing only the service-worker cache name is not sufficient. +- Any change to frontend caching or PWA lifecycle code must preserve the regression assertions in `tests/test_views.py` and pass `tests/e2e/test_pwa_versioning.py` with service workers enabled. +- Before blaming a user's browser cache, verify the live listener and served asset content according to the development-server rules above. + +## End-to-end browser tests + +- Put browser scenarios under `tests/e2e/`, mark them `e2e`, and use Django's transactional `live_server`. +- Use the managed Chromium and isolated BrowserContext provided by `tests/e2e/conftest.py`; do not launch browsers directly inside tests. Keep Playwright function-scoped because a session-scoped sync Playwright event loop conflicts with Django's synchronous test-database setup. +- Keep selectors and reusable browser actions in `tests/e2e/pages/` Page Objects, while tests remain focused on user-visible outcomes. +- Never use fixed sleeps for synchronization. Use Playwright locators, `expect`, navigation, response, or application-state waits. +- Preserve automatic screenshot and trace capture on failure. PWA scenarios must explicitly enable service workers and carry the `pwa` marker. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cee599a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# syntax=docker/dockerfile:1.7 +FROM python:3.11-slim AS builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_INDEX_URL=https://package-mirror.liara.ir/repository/pypi/simple +WORKDIR /app + +# Liara mirrors Debian and PyPI traffic inside Iran. The Debian mirror is a +# build argument so it can be replaced without editing the image recipe. +ARG DEBIAN_MIRROR_HOST=linux-mirror.liara.ir/repository +RUN sed -i "s|deb.debian.org|${DEBIAN_MIRROR_HOST}|g" /etc/apt/sources.list /etc/apt/sources.list.d/*.sources 2>/dev/null || true +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev && \ + rm -rf /var/lib/apt/lists/* +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +COPY requirements.txt . +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --default-timeout=1000 -r requirements.txt + +FROM python:3.11-slim AS runtime + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PATH="/opt/venv/bin:$PATH" +WORKDIR /app + +RUN useradd --create-home --uid 1000 django +ARG DEBIAN_MIRROR_HOST=linux-mirror.liara.ir/repository +RUN sed -i "s|deb.debian.org|${DEBIAN_MIRROR_HOST}|g" /etc/apt/sources.list /etc/apt/sources.list.d/*.sources 2>/dev/null || true +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends libmagic1 libpq5 && \ + rm -rf /var/lib/apt/lists/* && \ + mkdir -p /app/staticfiles /app/media && chown -R django:django /app + +COPY --from=builder --chown=django:django /opt/venv /opt/venv +COPY --chown=django:django . . +RUN chmod +x /app/docker/entrypoint.sh +USER django +EXPOSE 8000 +ENTRYPOINT ["/app/docker/entrypoint.sh"] +CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "60", "--graceful-timeout", "30", "--access-logfile", "-", "--error-logfile", "-"] diff --git a/PROJECT_TREE.txt b/PROJECT_TREE.txt new file mode 100644 index 0000000..152db31 --- /dev/null +++ b/PROJECT_TREE.txt @@ -0,0 +1,738 @@ +Project Root: customerTicket +Generated At: 2026-09-10T06:55:10+00:00 +Python Files: 174 +Django Files: 37 +JavaScript/TypeScript Files: 17 +HTML Files: 29 +Total Files: 648 +Total Directories: 80 + +customerTicket +├── apps +│ ├── accounts +│ │ ├── migrations +│ │ │ ├── 0001_initial.py +│ │ │ ├── 0002_initial.py +│ │ │ ├── 0003_magiclinkaccesslog_alter_ticketmagiclink_options_and_more.py +│ │ │ ├── 0004_ticketmagiclink_created_unit_and_more.py +│ │ │ ├── 0005_magiclinkaccesslog_magic_opened_idx_and_more.py +│ │ │ └── __init__.py +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── forms.py +│ │ ├── magic_access.py +│ │ ├── models.py +│ │ ├── urls.py +│ │ └── views.py +│ ├── attachments +│ │ ├── migrations +│ │ │ ├── 0001_initial.py +│ │ │ ├── 0002_initial.py +│ │ │ ├── 0003_alter_attachment_file.py +│ │ │ └── __init__.py +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── models.py +│ │ ├── services.py +│ │ ├── urls.py +│ │ └── views.py +│ ├── core +│ │ ├── management +│ │ │ ├── commands +│ │ │ │ ├── __init__.py +│ │ │ │ ├── bootstrap.py +│ │ │ │ └── normalize_persian_data.py +│ │ │ └── __init__.py +│ │ ├── migrations +│ │ │ ├── 0001_initial.py +│ │ │ ├── 0002_seed_branding_and_page_titles.py +│ │ │ ├── 0003_sitebranding_developer_credit_html_and_more.py +│ │ │ ├── 0004_sitebranding_staff_sidebar_credit_html_and_more.py +│ │ │ ├── 0005_sitebranding_company_logo_and_more.py +│ │ │ └── __init__.py +│ │ ├── templatetags +│ │ │ ├── __init__.py +│ │ │ └── jalali.py +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── context_processors.py +│ │ ├── iran_locations.py +│ │ ├── middleware.py +│ │ ├── models.py +│ │ └── persian_text.py +│ ├── customers +│ │ ├── migrations +│ │ │ ├── 0001_initial.py +│ │ │ ├── 0002_customerprofile_city_customerprofile_province.py +│ │ │ ├── 0003_contactgroup_customerprofile_organization_name_and_more.py +│ │ │ ├── 0004_contactgroup_unit_customerprofile_groups_and_more.py +│ │ │ └── __init__.py +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ └── models.py +│ ├── integrations +│ │ ├── bale +│ │ │ ├── __init__.py +│ │ │ └── providers.py +│ │ ├── sms +│ │ │ ├── __init__.py +│ │ │ ├── base.py +│ │ │ ├── behin.py +│ │ │ ├── console.py +│ │ │ ├── factory.py +│ │ │ ├── http.py +│ │ │ ├── providers.py +│ │ │ └── sms0098.py +│ │ └── __init__.py +│ ├── messaging +│ │ ├── migrations +│ │ │ ├── 0001_initial.py +│ │ │ ├── 0002_initial.py +│ │ │ ├── 0003_messagereadreceipt.py +│ │ │ └── __init__.py +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── models.py +│ │ ├── services.py +│ │ ├── urls.py +│ │ └── views.py +│ ├── notifications +│ │ ├── management +│ │ │ ├── commands +│ │ │ │ ├── __init__.py +│ │ │ │ └── process_notifications.py +│ │ │ └── __init__.py +│ │ ├── migrations +│ │ │ ├── 0001_initial.py +│ │ │ ├── 0002_smstemplate.py +│ │ │ ├── 0003_notificationpolicy_notificationjob_status.py +│ │ │ ├── 0004_customer_reply_bale_policy.py +│ │ │ ├── 0005_smstemplate_unit_alter_smstemplate_event_type_and_more.py +│ │ │ ├── 0006_alter_smstemplate_allowed_tags.py +│ │ │ ├── 0007_notificationjob_notif_due_idx_and_more.py +│ │ │ ├── 0008_notificationjob_provider_message_id.py +│ │ │ ├── 0009_ensure_global_sms_templates.py +│ │ │ └── __init__.py +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── models.py +│ │ └── services.py +│ ├── staff +│ │ ├── migrations +│ │ │ ├── 0001_initial.py +│ │ │ ├── 0002_staffprofile_first_name_staffprofile_job_title_and_more.py +│ │ │ ├── 0003_staffprofile_role.py +│ │ │ ├── 0004_organizationalunit_staffprofile_primary_unit_and_more.py +│ │ │ ├── 0005_organizationalunit_accepts_external_tickets.py +│ │ │ ├── 0006_alter_organizationalunit_options_and_more.py +│ │ │ ├── 0007_organizationalunit_show_in_customer_form.py +│ │ │ ├── 0008_unitmembership_ticket_scope_and_roles.py +│ │ │ ├── 0009_alter_staffprofile_sort_order.py +│ │ │ └── __init__.py +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── forms.py +│ │ └── models.py +│ ├── tickets +│ │ ├── migrations +│ │ │ ├── 0001_initial.py +│ │ │ ├── 0002_alter_ticketreadstate_ticket.py +│ │ │ ├── 0003_ticket_city_ticket_province.py +│ │ │ ├── 0004_tickettag_ticket_assigned_unit_ticket_created_by_and_more.py +│ │ │ ├── 0005_bootstrap_legacy_staff_unit.py +│ │ │ ├── 0006_assign_legacy_contacts_to_sales.py +│ │ │ ├── 0007_alter_ticketcategory_options_ticketcategory_unit.py +│ │ │ ├── 0008_assign_categories_to_default_unit.py +│ │ │ ├── 0009_seed_customer_facing_units_and_categories.py +│ │ │ ├── 0010_alter_ticketcategory_options_and_more.py +│ │ │ ├── 0011_ticketcategory_show_in_customer_form.py +│ │ │ ├── 0012_groupdispatch_operation_permissions_and_seed_org.py +│ │ │ ├── 0013_merge_legacy_accounting_unit.py +│ │ │ ├── 0014_merge_legacy_warehouse_units.py +│ │ │ ├── 0015_seed_customer_category_catalog.py +│ │ │ └── __init__.py +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── category_catalog.py +│ │ ├── customer_urls.py +│ │ ├── forms.py +│ │ ├── models.py +│ │ ├── policies.py +│ │ ├── selectors.py +│ │ ├── services.py +│ │ ├── staff_urls.py +│ │ └── views.py +│ └── __init__.py +├── config +│ ├── settings +│ │ ├── __init__.py +│ │ ├── base.py +│ │ ├── dev.py +│ │ └── prod.py +│ ├── __init__.py +│ ├── asgi.py +│ ├── env.py +│ ├── urls.py +│ └── wsgi.py +├── docker +│ ├── entrypoint.sh +│ └── nginx.conf +├── docs +│ ├── ORGANIZATIONAL_TICKETING.md +│ ├── ORGANIZATIONAL_TICKETING_GAP_ANALYSIS.md +│ ├── QA_PERMISSION_MATRIX.md +│ └── THEMING.md +├── graphify-out +│ ├── 2026-09-05 +│ │ ├── .graphify_analysis.json +│ │ ├── .graphify_labels.json +│ │ ├── graph.json +│ │ ├── GRAPH_REPORT.md +│ │ └── manifest.json +│ ├── 2026-09-06 +│ │ ├── .graphify_analysis.json +│ │ ├── .graphify_labels.json +│ │ ├── graph.json +│ │ ├── GRAPH_REPORT.md +│ │ └── manifest.json +│ ├── 2026-09-07 +│ │ ├── .graphify_analysis.json +│ │ ├── .graphify_labels.json +│ │ ├── graph.json +│ │ ├── GRAPH_REPORT.md +│ │ └── manifest.json +│ ├── 2026-09-08 +│ │ ├── .graphify_analysis.json +│ │ ├── .graphify_labels.json +│ │ ├── cost.json +│ │ ├── graph.json +│ │ ├── GRAPH_REPORT.md +│ │ └── manifest.json +│ ├── 2026-09-09 +│ │ ├── .graphify_analysis.json +│ │ ├── .graphify_labels.json +│ │ ├── cost.json +│ │ ├── graph.json +│ │ ├── GRAPH_REPORT.md +│ │ └── manifest.json +│ ├── cache +│ │ ├── ast +│ │ │ └── v0.9.56-s2 +│ │ │ ├── 01d84090a3b22716edb63a5cf9c0a56cc94c0f5207cdbd5005843f858bbb292f.json +│ │ │ ├── 027726034309b90d12137f6110b86751f2558dec73d338c93e190b2f9706caa4.json +│ │ │ ├── 039590a02c184d21a28caec4178b981a824ec4356a225d1ce546cc212d2f550f.json +│ │ │ ├── 058b4e36240cd25d158647c300472baad5e7101ebde0319883abb0edc8c9c74d.json +│ │ │ ├── 074e22579bb4a8d9c4f0c12cdf3cee2aa1dbc9e91778fdbbbbe22ce014511f47.json +│ │ │ ├── 08ff4daae812563107028890c2ae7257bb5f9a376a978244c814b7a4c1d10a98.json +│ │ │ ├── 091194a653ec9ff59e50d6d6121d12c0b0238b3f5cbb0467133a224b7eda59c8.json +│ │ │ ├── 0aafac1c4b289aeef9a6251e3a12fbf1710ae576dbd647d155aa3981008d50b7.json +│ │ │ ├── 0bc6d4b595803438408992e7a267aabdae34a6e30bcb947a90307114918928fb.json +│ │ │ ├── 0d837f2122843751eeb3e945c9c56992b5c410b54ca0dbceccf5413022601d27.json +│ │ │ ├── 0ef31d053655ee64599a0246f26c8eb61f27654ae1ffbd41b87500438fc9b06a.json +│ │ │ ├── 0fd49b978854a13276bcd9479e5d287e54f19e235d799825f049400153a78c6f.json +│ │ │ ├── 11d2b71feb1480622fb18ee3795cf070421b146e4273b5016fad81e7cc8e2c7e.json +│ │ │ ├── 12f95828a2c1818402459627e36481199020e31faa280d214f81700fb9ad520d.json +│ │ │ ├── 15539048155c760644fc70dd1fbff3855ceedf11bc6ef7b270d1c7280cb4df48.json +│ │ │ ├── 157a547bf11b52f36b8ae31334708f612d0d615ad75a9d7e42076806661217d7.json +│ │ │ ├── 161e1e32c695094d9d3ceb2a867fa497be0aaef0db27bdfdf94489c6f306e82b.json +│ │ │ ├── 16f67769b1d3e30239a9537cd98689a1f4448c60b2cc8bfdf8468ac9dc712ba6.json +│ │ │ ├── 17ab5521ea572ea85443db030bd5085f1443c20b20da9cbf808f21ad695eb319.json +│ │ │ ├── 17ff6234af168a40b807e5226bfdb41cf2011a7134c763aba3c69428b6879d66.json +│ │ │ ├── 18ba25713068baf53992eb4ea94f95212ecaeb931fdcf6769970fc2fd19b2f93.json +│ │ │ ├── 1b9e0fc2a347605b7d5ae84c15a0dc71fca5af90fa461763085e622f4c5877ec.json +│ │ │ ├── 1db075565e2fe1027675b7ee03d055150e6b741f3e9865bcd46d9a6113c538a8.json +│ │ │ ├── 1f440edc6feb5bf8c2a462b1b4a0b44bd22b4a763286fb2b340c797c282b5bcd.json +│ │ │ ├── 1f6cfd55d13bc48aa6c68558090ceeaf24a54c7bec690d51d95243c904daf0a6.json +│ │ │ ├── 20a6a77982f0ffa677ecf8b96662481ca9acccb7ef05274a31fd7ddbd217842a.json +│ │ │ ├── 211549439bc4b41e71f437e9909b8fc7bc306273615743fc61f53c8b8dc57e56.json +│ │ │ ├── 21f38c2f6a379ab402b4d3bd1b0809129c8554ebb01202a160c42a1e9b49b717.json +│ │ │ ├── 21f56aad8baf0dfcbc69c801ff4f738d0bcb84270150cde91f6290ebdef0e2d4.json +│ │ │ ├── 22534d744aadf9c9b61b908eccea0e9b3eaa30bc381bace54b3221ee0861a0ec.json +│ │ │ ├── 2447edb7f88778fbf1ff0ac017796eb52bb2ced4c91786eef12a341b37adfaf7.json +│ │ │ ├── 2609e627752bd20df923f6c69e090173a368757b3dbd2ce418e3cc9f67c697ba.json +│ │ │ ├── 2667c1ca679408fbaca6212c012d64f504eb8ddd574841569403de39c25dff3b.json +│ │ │ ├── 26c6832fa65fe38fa62f98bbfd8d544cf6a470cce547579e1d650390e8b9fded.json +│ │ │ ├── 26e3b3874f2dc2c3709945e2f6cf050e5c525933de9f09a8c91b244e3bfa8ca7.json +│ │ │ ├── 26f1fc81bffa8c22334482cd1cc638ac1f38647ae723ac8c360dd855281598e9.json +│ │ │ ├── 287fdc570ea3598f1b62d5764970740f7397df117fd25e9340f03b0f8d0e5171.json +│ │ │ ├── 28cefeff27c9cb5760aead796c37afdb1311bff0a5ff759136f0473ffa0152ff.json +│ │ │ ├── 2ac9a38700a628e196b14c23c8615d221fc2efdf0a9564722d4ddf4e293e70c2.json +│ │ │ ├── 2b104842fa1eaead6f52dc015e74fa38c6327cfa1a95f0858e649d093b1e98bd.json +│ │ │ ├── 2e17dd08443df447b74080559d8cfc6b782adfc04963abfb7473e8a59c21d701.json +│ │ │ ├── 2fd5953e7926f320bdeac2d559967758aef4632bf7dfa26a5ee50ad386fc9b67.json +│ │ │ ├── 3142f9d4517f137483c716dd61985811bbf006b85506791e9a2ee37d58b2f53e.json +│ │ │ ├── 32a4505d16f4fd1efc5bc0c2ea7c8900419856b21a41b06d988cc424258ea1c9.json +│ │ │ ├── 32acb58a89265f31e57697995a8939b1636afd415969541ff44be40b8dd198f0.json +│ │ │ ├── 32c40d17e90ccf2511e124b4fcc5b3674a7200dd5215ee8742d319f8090b04ba.json +│ │ │ ├── 334f218e8e74fa3ac3e285059183c77efdbfe99ccfb426df17b4eceef14627e2.json +│ │ │ ├── 3358bbaf4a10bb5102cd25fe7cdb5b8401880e8ec0d711b9b9e9b4c92404865e.json +│ │ │ ├── 36000938c9c3b13fbd92622f905f8c4f4bfbfdabe66e3357e88c162a4fb198ce.json +│ │ │ ├── 360e95dd7f9428a51961a1f4ae7968a6e3101b8df00027432c6c743f64ccf9f8.json +│ │ │ ├── 367d8bf55d8d85048571009c40b04ef43211cc9c630af6222fc5db65fb382cc8.json +│ │ │ ├── 37159060b34fd31f9b937b215a65baef4b2091c310ea6430960e1d4991a37c5c.json +│ │ │ ├── 3948717314f47afa835e760073df4764ac427f4858f5ec0ee6a2f298ac393d49.json +│ │ │ ├── 3b878a979cc2ea2ac926f8a72f24dbef940bbaafdfbdc76679f4054a3ebf791a.json +│ │ │ ├── 3c4a42b0b79b95acf81967479efdc4cd0a369e3b53b771fe87478d6327ddf0eb.json +│ │ │ ├── 3cec8055583c7d94e03ced535376a8bd9d3d335cd6c7e328f38dc2a9e6b1e397.json +│ │ │ ├── 408f6eb568e45e950fdc170776dabf9087829df0db630e16643fba8995318d26.json +│ │ │ ├── 434bb9df51f8929ca10167ce360dbaa9d93fdbfbd27053bdf11f3b171189edb8.json +│ │ │ ├── 44684e85104b3a520e0369a953041469689cb680c2144a6c3fd7107cadc9eed0.json +│ │ │ ├── 47730ea39698b58e7a30a7e5fce471272e9844e0a0c09a0d603699446667a9e2.json +│ │ │ ├── 478a054f8292cd197743a61b5e3e04487f67f8db76ae658a28b04e4aef670ab2.json +│ │ │ ├── 479992e96c0c5fac0a00bd4d9e0822dd52273158f5364c26884649b017e5b874.json +│ │ │ ├── 47fb67d01a9006c7f835ad87b75b95bb1365c27b342144f081d858e07fd309b3.json +│ │ │ ├── 4993b53d428cbbb1e334ea396aae6397586a1295a35f39da038cfe72f0b89fd4.json +│ │ │ ├── 49be8523d744110ec6038ec61ccc9e8f47894a392b60e970206c512c27e68178.json +│ │ │ ├── 4ae419f04a3a3b1221faef43b8526910f1958336162fbce432bab0e596ef2aa1.json +│ │ │ ├── 4bcdbebd8ab168095ad8130c68a697dd5ed64c3f52350fbd395c37408b9a7afa.json +│ │ │ ├── 4ca6d659e3dbcd21e9bfcdc394e19ad8b4dfb6a26665f3395e5a0d5f41e611ce.json +│ │ │ ├── 4cf64fe0b79aec18c49355f7d82ab04ae66067042a6c364e89842109427ffb5d.json +│ │ │ ├── 4dae69d87cc12f2afcaf76a8124cbb0ed4ed5b1b196563ead3179384bf5cb5a8.json +│ │ │ ├── 4ece1fa07b9b0b7d50f5efe48a083265e3d8efac191ed4a14875c8399522db42.json +│ │ │ ├── 4ed0e6e63784e85d5e256cb6eda996383dad486161209fda91200caa511e8907.json +│ │ │ ├── 4f4799a971a5d6701087428e8c709396c0d947f7ce5bcf7a048cd542d5551c57.json +│ │ │ ├── 52cd74ad5803b940489587043429b741434ae931fa4d244f5b0dc53582cee050.json +│ │ │ ├── 53705e64605d8b06954f91829d03ceef80bfacf943427a2d51079c554e41a42b.json +│ │ │ ├── 552bcabe2c76a5db8c1f071dbd2b19c31aeee0b07cf605cd5f2b031ea3b6440f.json +│ │ │ ├── 57507a7cdc8d6a5af4629b2714e7f0226805f833900d496b938e8f03dfc617ab.json +│ │ │ ├── 576178824e99e86dee1ca5fcdf46f61962380f6b8a708b046fbdfc766eb650ce.json +│ │ │ ├── 5794ed0ca9011299d935a3be468c921c65ab616e78f0c6509ec22e3a77cdb794.json +│ │ │ ├── 57bc96866536f0d67300ed29e56bd0f45fa9119c787fac805f228adcab18dc6f.json +│ │ │ ├── 596a13e9b322f8c3332a3ae214ca7bf368ba6158ff3adcc7db4582f102ea51dc.json +│ │ │ ├── 5a34ded8b2dd4c10c4251d287580918b88690c7058dd2299d2d670170104241d.json +│ │ │ ├── 5bb0895d417e210a10ebf2c63ecf3185dca651057d952203e2e39abc249180a8.json +│ │ │ ├── 5d43f2c9afc3451745d2a1b5f1ed17bc138c9b19caff61beadbc40a4e970b525.json +│ │ │ ├── 5dd23426751ac130946faa8dff76b2200a8a6c5515863890c15ac5c6c3a542d3.json +│ │ │ ├── 60ef2d6fded9e070e35cfed119e8325010aa76bce63aa74ca0f25005d8ed9517.json +│ │ │ ├── 62dab578f0debe93a8df61a1776953980abd254ae6f1eea77afb00dfefd28f15.json +│ │ │ ├── 62e55882d2ce81dcfe5df1d58395e767ded55d6e39997d82cc3489c5e02ae910.json +│ │ │ ├── 6469cbb1dbfa0b98762fde08c6fd05cc831e1cf21bd61aebb56b34a7e916bb1a.json +│ │ │ ├── 6506009802a6a268be82b680ace7580613f18dc702ed27aaff929398a85760f3.json +│ │ │ ├── 688d055d3333b15b1beed13c74d14b507a1db1400918f22f01c3bc37bdb4c84e.json +│ │ │ ├── 68f74aa5f41fe3d0e77d36da903e26c49346ee9226c8218a71f7b44c2c41bd93.json +│ │ │ ├── 696bb846cc6a3735e085b502a1008e270903c81c7a7adcc7676abcf1930e926a.json +│ │ │ ├── 6a7b027b7f77736aea87ce048be3247c43cf4878e8a1105811247839427ebcf8.json +│ │ │ ├── 6b60f8a4e1c97f193edc097890ea2759ec246b8ea09035b2afe8b0b32181f345.json +│ │ │ ├── 6bd49215f8d2739a9e365dcc8855b95cfa34352c067a9e9ff48a623318c4ddaa.json +│ │ │ ├── 6c69494ba124d4bc564bcd3a3df1f33ae0ee0769001a2dfc46825ef8b44dc3e2.json +│ │ │ ├── 6e56bc8cfd4ce0c21b56fe2b18b7923c24b34cc09ff39276d1438666c54b074f.json +│ │ │ ├── 6ee9154979fb497a8ec229a7177660af53a487e1aa17c7b555cdc67797615162.json +│ │ │ ├── 7093cbd0a855750457814388b6e527fd3b612163f7a2b898ed6ce470dae74843.json +│ │ │ ├── 71e1e6a563535dd1761f4048b6638e891580152d186b926416df47f8f2948782.json +│ │ │ ├── 72d77ebab0d6c84d4c8e54af7eaf7fae0344a476dbc91cceedae586a5e54f3c6.json +│ │ │ ├── 72de23f55766289c980e6ccf604ca5d162e0989a222271b7496775ba4d749e3a.json +│ │ │ ├── 749d284b3c2e0b313c52afd8d5707b846e0e87e537e3c5fc2455c825f43b3947.json +│ │ │ ├── 74c2a0c0f83087673b100b7555e506c71d2a6a941677e388a1de59371b6d9afe.json +│ │ │ ├── 7592b56384d6034a7fc29e92b9e6d8b494cff9ff5536a9880a95856d28942151.json +│ │ │ ├── 75fad6124616f6d9e50ce9a475d4e989955c5fae7c5566b99992d986ef9a1f93.json +│ │ │ ├── 77916c3b035a577b24da1a1dbcf47db7307af4e0774fecc549f7c95b91994a20.json +│ │ │ ├── 77f3cfc32bc9234033fcc789ff327a4a98809057bd34d1539b7f2883ccf74ab3.json +│ │ │ ├── 7805df77950548e276a1dac439cf36a3620025bdb8a5739e7a84237f38d01a8c.json +│ │ │ ├── 780c2256439479b3eadb498309a4309b20313a36633aa8bec2a0dfcfce3a3d32.json +│ │ │ ├── 78381b49b02bd2a6248eb0363cbceaafe887dbf8e6b19dc513739b35b87cca95.json +│ │ │ ├── 79cb5047a177592ad737ee438a5e0de370e5dcd28b164b5a48234a6e4f554b1e.json +│ │ │ ├── 7aff3a4d59fae95d952044a679293bff08c7305212ff12bd7be1d314e1f25a3a.json +│ │ │ ├── 7b6916f499ae0ceae3eb3d509b51f42a7eaa98e43e161fe87c44b9a5bb01287b.json +│ │ │ ├── 7c9789f4b51a967448da1de52e4ac3bfb4b342284bab1257343262aaa32b0d6d.json +│ │ │ ├── 7d276789b80c8d9d1677556b3fb33b2da05988e97f6bf28903a044bb9fd64894.json +│ │ │ ├── 7e1c0a38a3ed90205d1829bdef528887ef12c928d4da797e06a79b3e7a1a92ba.json +│ │ │ ├── 80ee0e1cf4fb023c632197378ec4601f72c438df11001fe2648f59ae2aa89199.json +│ │ │ ├── 8206f93340496777fb5c0c2b431493c2d243c12fb74dc8cac46123d15f9d3f44.json +│ │ │ ├── 835c014cd4e3398cae6e9466790acda90cd37183b9a777ddece3881351026c3c.json +│ │ │ ├── 84579b3014e1a83e4201862df1e93378f5f953fbb547fea30965e335be5e7b1e.json +│ │ │ ├── 84dedd4165eed360c4051690b2aa84f6ecc08f3f053f4129ca6f3e33c334ba35.json +│ │ │ ├── 876b8154980e87a39dbec815f7b9810aa93ab2f5af2c8c483a2dfddca662dc37.json +│ │ │ ├── 8790d1fdf629fe1dddd54df9f0f37c212fb3308b574efeada0108e83ec8b33ec.json +│ │ │ ├── 88ade181c6611ec33d9bcf5fe739dc809d81875b6571b9f60f87dafb4f5941b2.json +│ │ │ ├── 897df8bbfa3d5a2c272916d47f3326de2b1cad7c3046015c949a46f69e9b8247.json +│ │ │ ├── 899b16856382d32c4e5b39d02cb251bdedaf35c5a81bede5c19bfaff092208e9.json +│ │ │ ├── 8aeea7acc875fd642cb75f8766dc1fa84e8f91f027c3ced3bf44b8faa1a2798f.json +│ │ │ ├── 8b45a75ab824f6e8b21a94f6828c0da172fd242fadeb1f61b32de34b80f62cee.json +│ │ │ ├── 8c258f28e7a23d569a250b17e2f591ea2b1a5b9072ab1e0ddff1b535468badb7.json +│ │ │ ├── 8fd86558bd11e7a00f6b28cfad545707ef94afd87a9095ab532588b36c49f842.json +│ │ │ ├── 8fe2c3236c2b05a4814f32898daea17b341332cd0cc887df7a154ad999480d75.json +│ │ │ ├── 9141ebe765f15cd7e3b24431f315af03e41a165ff0a54c33bec2c4884515eff2.json +│ │ │ ├── 9326d87e36b2dc24e6173aa3346f46e7df9fc3353fdb409304af4363c286a41e.json +│ │ │ ├── 939d30ca6a3e0c8379078425d21de30c3e8c2743ddf3e13efd984becbf68d109.json +│ │ │ ├── 9697c37cf3a88d2b4eb5419b2381951a9aca966172316db79e7b4c32557d5e87.json +│ │ │ ├── 972650f72e30ee3a72cd37dedff2f0ba823b355300dd68c5e196f87717bd5bbe.json +│ │ │ ├── 9729a8690e1957a356322dc991c0618b49605bd58709dcf8ce3ab1af7d254a1c.json +│ │ │ ├── 97506546de142970349e57e8a14064280e66a4fcdbced5dac55fc263630a2247.json +│ │ │ ├── 9931a5b7d7ab58794d003cce1d0719c201c623a844c7f834f66c88e9390c824e.json +│ │ │ ├── 996411678d23c3cf8a0e6c1c5076701e8bf5f3dad21ca907ec4116d15366a668.json +│ │ │ ├── 9a6a5b3e4a7ac5efbe662c2a44a7d9400c2c46f18f15c5dd3627deab4bd7c8d7.json +│ │ │ ├── 9b8bd2aff50e08b6f053b74a409c9439db41e692e29aabca5b67057884d945af.json +│ │ │ ├── 9bc00a73e1e7393f535b9f58edc50d42169c407c58af664ffdf838c533bd29cf.json +│ │ │ ├── 9ce16f69c6b04f703adb24ec7498a2d2ecbf6f7524c459deb25111247e550698.json +│ │ │ ├── 9d0e2a1fb14529aabd31c82c8d8f5543c0068366932934038ba93e234f291e22.json +│ │ │ ├── 9e5144f3933fd4424f1e271f60b78ae110855d238a7921b499b1586d21ad16a9.json +│ │ │ ├── 9eb524ba1a6374b2d29429652e7e8bf087d839fecd65494380982eab349382a8.json +│ │ │ ├── 9ed5e291859abba38b07725bc6cf0275a1548e937888a5ba177674afaa48b96f.json +│ │ │ ├── 9ef0e904ba6d1b295c75a1d5741851f571339a8e22c7de96bf5e2508f764ee43.json +│ │ │ ├── a05b3a5bb6e91642075fb765d43a0a6e4dc595bb9131be86643e6d06e6fc3cd9.json +│ │ │ ├── a063cce1553aca090b61f3b2b621a089d1c128ff347e11f836857b77ef5f3d56.json +│ │ │ ├── a1d213c52d246524be10c084f9134e77ff2e2a9aa4361b71dc4bedc89f461cdf.json +│ │ │ ├── a3c770875ffabc18c595532501255df82b1a09246ee54b831e7c2221932d4f88.json +│ │ │ ├── a4a27e7b3842713c361895d6d9c5fc9e4341cce71c5842f014748e1c1c23a197.json +│ │ │ ├── a5231d68fe35e6b02ff4ff70e02a3229c08d9b6c2bae774a1b3227823b95e15a.json +│ │ │ ├── a7729f0b6bf20ded62ff7b6ee678fdaae9516dcd80c2a8f9e9f3d9299f6e22a0.json +│ │ │ ├── a93b84a0def4d2e0e97ff35032871b0ce64786ff468912aed21fd958b9fe782b.json +│ │ │ ├── aa56d5609649d9327e5f4cf6d02b5e8afb7749388f6d4f8776dcf910e5453935.json +│ │ │ ├── ab1648cc3b4d4bba5754e19eeb80ea160fd20207d5530ac371647168f9ed211a.json +│ │ │ ├── ac454f6bfed5913148eb8c9caaa0e30f99bac381936b44612bdee8274bb27b29.json +│ │ │ ├── ad1175b041ab42a2615856a41fbab394ae63ef9dd991901c0658c3fb0846ad3f.json +│ │ │ ├── aee78d4e87210ef46180d47e7015345bd66a7763cd140d84cd86539e720e8e67.json +│ │ │ ├── aff120c654cfc097d07e9078775b97e4f066e7c539b21b49397a372086d45543.json +│ │ │ ├── b390f7bae9f80c48e703a6664d9de6ec9c141cdf8ac2f012d67ac2b4d8a25b4e.json +│ │ │ ├── b39d104e50bfbd13e83fdf946668708536d98cd0c6228eded5fabd5bcf6ddd18.json +│ │ │ ├── b4446ca11a1f4f6ea45256e755ed56d6ba7c330278e15ead8cae2de28d5e18d8.json +│ │ │ ├── b4db8b4f6749eb80079ccf2d1a2e81dae05804168c415767ed58a23f16ae2ef8.json +│ │ │ ├── b51c7386a49f53f1e51e3c0534090d4b3e068752cc09b3bf1c07acae4897b44e.json +│ │ │ ├── b5349ed182daa82709b46b433d85ac4c9a610994647244f39a895138f087c4cc.json +│ │ │ ├── b58aa068f1fd9b9bbdc1f9acfd10920458e7ce8cc56676580d1970e1375e7c61.json +│ │ │ ├── b6a1d3264457641619e6f60d3f961556b765901b600688661d4795c974cc1eaa.json +│ │ │ ├── b6e731246dc10854a0fa0455c3c5816e29887b1a8c436d518926e1c2a5e36239.json +│ │ │ ├── b7034e69b8b9437fdf34faa4adc116aaca2fcee50e966a400972c365076e88fc.json +│ │ │ ├── b736fc83f0aef50bd9c65d26cabc1cadf09c0469465e44fb8fca8b9b30bee7a4.json +│ │ │ ├── b922fd1a31f3cf602f197c872f3bac6e69c79255e502ddcbf1626f846ec3baee.json +│ │ │ ├── bafb94885159908a9055100ceaf4034f3822bbb4f78672fa552b0a276a0c895f.json +│ │ │ ├── bb3d09cc5d1bc380e841e2eb19cb497391117101c73c358e6db03db4d6ae51e7.json +│ │ │ ├── bd5b6283310ced47ad086af119cad6259d88c1d2857a6aed4f4f9d8a86233de8.json +│ │ │ ├── be6cc64f9c2a8edbdbbd10a1a31e3b3a746eb4bddf806c71d525eff5719b9217.json +│ │ │ ├── beb1b14e942397ae4b9f2e8a743282f127cc9f8c6e7222db7a28185de4493e57.json +│ │ │ ├── c1060dca2a3f437f386324975be659215fe9f7e689137dc85e823879b462d11f.json +│ │ │ ├── c1943dfe9eee1a9f88ea571dc5aa06a3f521241fffa51d55b1414c675e63c236.json +│ │ │ ├── c251d835538bf3c08a96268c8b1feaae8084a9c298d634ce2f2ee3b4b15188ce.json +│ │ │ ├── c4878d8d8ae19e705821ae40afc1ede4be1a64b559dfdf89f898cb4383718c93.json +│ │ │ ├── c8c8647cd740fbc96327657baebd4b4a16bfa1d8e7d4cfe3f845a0a5ab7c98f7.json +│ │ │ ├── ca42945436e914d9530e20dc780794451ec6ab51e17414e8c9107ccad3125fd5.json +│ │ │ ├── ca8e9d2e76f32d42a0da88c9477956b47dca6cdb27504d8d126e0075306eae98.json +│ │ │ ├── cb744c0a1cb5fa65e239993bee622ad4a60990c628cd1d4503f9e8806d1d2c92.json +│ │ │ ├── cb87cd70f59a8e0950529ae42a2229a259ff8b305e4319422fb1a9dbc41e63e2.json +│ │ │ ├── cc5e32b33a50eb2225359c15bceebc406d04a07218cda65de3e6f79603d67416.json +│ │ │ ├── ccd15ad60b63cd29e2bd2762133a74e2047a2894fd67206dff7f7c7fd082b399.json +│ │ │ ├── cdeaabefa47597c8be9310653d52fb6870d859162b49c8dae6db8f4f699dc4cc.json +│ │ │ ├── cf1725a1db0d7ecffa63d8b1b693854170e55385eaa4b0cc604f9d6ab204eb4e.json +│ │ │ ├── d083dc75811629ffa77ea30c1965e67ef3bc2bf65e5b20a1a35cb0f80a46949c.json +│ │ │ ├── d210ed7569b9e06acf7037d9a7d15ccac0f6f95ef7d3a0707741db8629443177.json +│ │ │ ├── d2513d4f5ff7e6088864ff497613f47d4083c1ca378649a3f1db6d45a3f3fb2b.json +│ │ │ ├── d26a8fc663e9e5fcf33c87592f8c0456e7fe9ec3390bcb902631144d455c2922.json +│ │ │ ├── d48290356397f2b79e05fc0b6bc6f9c327da366096ff14c15e90516fad7218a1.json +│ │ │ ├── d55e8987ca5ff35748b62290dc14150b1df53163c6f5a5cbe19d03058368f1c6.json +│ │ │ ├── d561ececcdd4e7ff59595389fa499591f4e7f3c5324a58dbdd749ccf29e90cd1.json +│ │ │ ├── d9c81db9cf9f2fee8a4985367d33b1c0ab1d7e6901e85473070c3bfd6bfe32c6.json +│ │ │ ├── daa187dac6877c746c02269141a7e7db178ac32a0b3a4bfeaaa8ed046b6dcffb.json +│ │ │ ├── db04dbc0e2dca073d2b145db8ba6c2c5299d3fd93742e1022c611c938fcb4d80.json +│ │ │ ├── dc90ac5e8d6766f8001bc2a40d21c203717d7a9f469956c2f6d9bedf3ae51aea.json +│ │ │ ├── dfc9279b0d9023afc1dbe55536fc1cd7a556a6a437775128b9e41615431b4733.json +│ │ │ ├── e0373f56a7e20838a75d2c1920393433e8d9a6ffa919334851b70f210ed4dbf3.json +│ │ │ ├── e15f9a7e0698366b7d8b4bee6102d351241860a078550f47a1eb27503e521bbc.json +│ │ │ ├── e2c372f92146217844d34664334cb0d7f01fe262ff98edf009a66c4bf4743b5c.json +│ │ │ ├── e42f6a96ad8938a6ab248b342feca7ec6ecf2a734d25141f62dbc9a6c52ecf6b.json +│ │ │ ├── e53a0dfeecd1b547e38896f8e7920e6801d84dbf61bb4513541892d44d42ae99.json +│ │ │ ├── e7c0ad70ad7e7f3a1f6f35e55f4cf87f6c0850abc63de71a418e81b88ec826b7.json +│ │ │ ├── e831be2a985dcf2094a014d182ea02e4ee10b94fbd35b44e3c69a98a7acb3451.json +│ │ │ ├── e90aa709aa08fcf3294352b588ce8c8c902f3ef59a503d4b8d9a292a9c478096.json +│ │ │ ├── e974cd0006d8160a6ef1316083bac9e699adfd87acc7edb404ae7eba1c799b68.json +│ │ │ ├── ea42f92a2b1964458b3c1060b13a607581b30e9b43d5e031f8a263cae90ba581.json +│ │ │ ├── ea56445d1314587e18b1992d9599da03e1419ea7bc4929854b9039d6947de33b.json +│ │ │ ├── eb1dd65efa5a99811ccca80ff03a73cb155ad86d13635010a44c09c13953604d.json +│ │ │ ├── edfca154606b071ac823ab03572797546c6044eda88073e9e3f55b4960cb3400.json +│ │ │ ├── ee19f3245c11f658fb60d32050a8bc4864ea289071ff5f829717ecacbac1d3e4.json +│ │ │ ├── f12416bacd2eea21842fe89fcfc2c02ffe84619d7ce380d13679b64c6ec7496d.json +│ │ │ ├── f150f764dc46f341076a8b373709a9cfefe48476672882b2a979df4624aa2626.json +│ │ │ ├── f2069429b170b19df531b051a2f5fa81854a7bedf1a036aab1a46c89a9ba4c92.json +│ │ │ ├── f2287c55ccdc0e5e9ab12f79895b1a1f46b65b2d14f70915ceb09adfc624e3b8.json +│ │ │ ├── f411c6e74a1dc71e5f7a16f159d065410a8562fd545dd209179b54ae2e848e10.json +│ │ │ ├── f4436ad45c3f2a27b4461333ae2ed21af157a3945e152fca7f43d640dc39f63b.json +│ │ │ ├── f58927b2be96cb8769bb26a2ede3d8c05796e7a79fefd7fdcdc89d658609fc66.json +│ │ │ ├── f71ec68c7b2a4467358d618354305f3f4e99cc0dbe60fa00d39478b686fe8bb2.json +│ │ │ ├── f8fc55a028176d1ea9a1b580a8653d4c45ae7466445f37e6c23867f12821c44c.json +│ │ │ ├── f92455d5b493a0d7e9565d2c7463a1aa3fe62fa20ea19e635e66c35582ab685e.json +│ │ │ ├── fa19a1bc8027144f14106a6f90d4ddab0277580b8bcdb6e9e3ba67496c043d62.json +│ │ │ ├── fc313334fba5b89ae7b30c44e1b0bd43e185bfd06c1e9804ad1b9b6de65efad8.json +│ │ │ ├── fde577f1231f9b033560bfe075896efc6bf8d6079a030a159035584c7d39843b.json +│ │ │ ├── fe77fbd662d91d04f2efc69788d30e9859584c8d87d1fabd60ca66661568091e.json +│ │ │ ├── fe93d0fb278c013183276f7bf5c5e2d0e62fe9c75f6695390f5994ec0b9c8dec.json +│ │ │ └── ff1bf9257c3d267b5a04560ed07ac6d4442a145b3e6db5a0f8e63e8e7f386533.json +│ │ ├── semantic +│ │ │ └── pa567fc138e3a +│ │ │ ├── 00825b46909baea25186d0f8351fc2c3fae63520319b5e7bede4ed3e9f53e54d.json +│ │ │ ├── 09f927cff0791d0e91365f50b4dba1a6c8cc0496ef70847d2ba9f1726092d00c.json +│ │ │ ├── 1753a19b402251a9e18e9c4113a5f1750a4168e9b0bedb06b4d6f35f2aa5980c.json +│ │ │ ├── 195617b9b33d83896a4ffa16edb1d849e0da5cb7a1aa2b64d82d1bafed070f80.json +│ │ │ ├── 1e1ba446fa646a9dbd2244d5138ad84e217e8f238f2508603cfe4c290423dde8.json +│ │ │ ├── 1f916be2dc79752fb2aa8e0c386470f43cc983c9021de885b62ca962ffea960f.json +│ │ │ ├── 2233df96365dd9e332d4744056d02289ae3a003a6fb8f3342a205860e03f8665.json +│ │ │ ├── 22569dac279084bd62c922dbc391f9a5a7cc2e1051ef043bde5cfc72df0ad864.json +│ │ │ ├── 23f032018f0612c58670d22539577856b5479f6107878d558e5e062bac9da30b.json +│ │ │ ├── 28f2ce9d1005a70b7f624b04888ce9c511251d8da243d6c1c57a630eb14c1220.json +│ │ │ ├── 29a4f053d88deb1406e9e7fd20dac4fc1a3c518382af3adcb25fc0d6415e6c31.json +│ │ │ ├── 2b0ca95b4d2205396255ef9ad6880126d69de168f4349406796caf7cee711791.json +│ │ │ ├── 3451c7089bb943a2571e2fd451467aa5e18fc642a64edaee34380e7b59f54baa.json +│ │ │ ├── 379031c1044f245cdf1ded7504608f5861a11d5110845436678be4e121251062.json +│ │ │ ├── 481e0ef213e9eb7ac3679e2fdf54cfe7bafde152b3de57b0c9306416529ab904.json +│ │ │ ├── 485a9d1cf66322f5cb2886aac1f388a2846aba6fd444b87b251edb03c51ff00a.json +│ │ │ ├── 57240e5b1ee2390b9f26b44f11253b97b1c6b279affdedc34cbeeadc74181504.json +│ │ │ ├── 660dafe22e562cecf6aaf4afdf658996dd024a1436c57e9815c82caa0c3e1782.json +│ │ │ ├── 697c16d4c7fadca75d73c67dcfea3aefc8c3b91394718b1d09188e7e41d8811a.json +│ │ │ ├── 6d5ac5656791048028b6bec9a94588cede8731bd7ca2151f13f7819e2819f2e7.json +│ │ │ ├── 7322f205d129029de2b4bb0be04fc6de2798fa17122738ae5d5f300e66ee5409.json +│ │ │ ├── 77b69c31898165440ab0d856d50bbe29495f04c804d5ca4104be340e0ed39cc7.json +│ │ │ ├── 78aa9c4767c326ca3364fd87dfabda624b9bdeb9a14dfc7b678d0bcd9c00e6f7.json +│ │ │ ├── 8042771a592072bef73f5634ecc39cd2b83ba3d808e806a69bacde795ff1d2dc.json +│ │ │ ├── 80cbf2282ae0d907d07bf110760fa70e985fa748f41bcc90533f6ff46eaf46c9.json +│ │ │ ├── 853a41742ac5792c2f6d6762d095b7170499a35c16efaa4fa567ee936b4ccb3c.json +│ │ │ ├── 87083d9b896625e7efbbfda3a8b4c0983a3914779adc1a9f1fd4775200a98843.json +│ │ │ ├── 8a7c3cffb298a6af2f6f497e70513330244117b87649ee61c964461dbcaa05e8.json +│ │ │ ├── 8e62122fceb99e49a7dd46a86cc46cbe3218cd86773d310fe15135a6ad4d6998.json +│ │ │ ├── 9281775c35930ece9d1382446b2e7d54cb3ec777fbc045ce46cdce5287f589d9.json +│ │ │ ├── 95dc06be19ee8e1bbc725384f6e55bbafaab3b6c9f8deea714356fbd0669545a.json +│ │ │ ├── a7b56fb6165dabba4d71f0757463855dfca3d85741975d62820b05abee14890f.json +│ │ │ ├── b631ef1705c4bbcc263ee039213e6929538d85401b648d0a675849fbeb60db11.json +│ │ │ ├── bc16443a4c12a2ca6a8a74c6378c2f66b27425fd52be8ca5c938c2141c886a53.json +│ │ │ ├── ca98ecfba0b2ad600c63c4f743e41528e2add0a2b48774b065aa0f60dcd905e3.json +│ │ │ ├── e00a1732e6e7df91a7aabb9bd609e2a5985d5cf7215635cde4a7495a0112efec.json +│ │ │ ├── e10d4f4e825fa6fd025ab71d55be4304798b707016c9b0b0f6cc0da6589a8435.json +│ │ │ ├── e3fb6d5f912be572c627f840a143729a58015403907a2616b32d3ebbe9933763.json +│ │ │ ├── e5f6e9aa1524a7e8c45c4ba6043bbf5798a8f8bec760ed991d69359cd416a08c.json +│ │ │ └── ec9174249dd1b030999fd9c188e1713659650afd92dbe8fcd7a1f29666023a81.json +│ │ ├── last_query_stamp +│ │ └── stat-index.json +│ ├── memory +│ │ ├── query_20260905_170246_63845aa1_افزودن_استان_و_شهر_قابل_جستجو_به_پروفایل_مشتری_و_ف.md +│ │ ├── query_20260906_045047_18cd50cb_بهبود_ui_ux_پنل_کارشناسان_و_رفع_active_نشدن_لینک_ه.md +│ │ ├── query_20260906_051057_e5dbb774_professional_high_contrast_typography_filters_meta.md +│ │ ├── query_20260906_104407_a2b8e7e3_staff_customer_directory_with_name_mobile_search.md +│ │ ├── query_20260906_105257_8eb55f7c_professional_responsive_ui_ux_redesign_for_the_sta.md +│ │ ├── query_20260906_105823_f78ba220_why_did_frontend_css_require_ctrl_f5_after_every_c.md +│ │ ├── query_20260906_111126_ebdc56bf_fix_staff_avatar_upload_in_django_admin_and_displa.md +│ │ ├── query_20260906_112825_ea391369_replace_the_project_wide_sms_implementation_with_t.md +│ │ ├── query_20260907_122020_4b046e4e_why_does_staff_ticket_polling_return_stale_changed.md +│ │ ├── query_20260907_123737_6134b5c8_آیا_با_هر_پاسخ_کارشناس_پیامک_برای_مشتری_ارسال_می_ش.md +│ │ ├── query_20260908_073243_f5d33807_graphify_build_full_py_برای_چه_بود__آیا_دیگر_بهش_ن.md +│ │ ├── query_20260908_080123_0dda7692_در_میزکار_staff_تیکت_های_قابل_اقدام_بر_اساس_جدیدتر.md +│ │ ├── query_20260908_144036_ded077cf_صفحات_staff_contacts_و_sms_templates_خطای_403_دارن.md +│ │ ├── query_20260908_150032_76389667_بازطراحی_ui_صفحات_staff_و_ادغام_customers_و_contac.md +│ │ ├── query_20260908_150749_7f7a3ef0_آیا_workerهای_sms_و_bale_در_حال_اجرا_هستند.md +│ │ ├── query_20260908_155640_736abe4d_بررسی_و_تکمیل_مرحله_به_مرحله_شکاف_های_سامانه_جامع.md +│ │ ├── query_20260909_035506_5355f42d_لطفا_تغییرات_عنوان_و_header_دیتابیس_محور__حذف_متن.md +│ │ ├── query_20260909_050053_deff48c3_در_صفحه_login_متن_بخش_auth_intro__زیرعنوان_درخواست.md +│ │ ├── query_20260909_050734_e5645542_متن_امضای_توسعه_دهنده_در_پایین_sidebar_پنل_staff_ج.md +│ │ ├── query_20260909_052451_568b2e38_متون_staff_login_و_صفحه_tickets_از_دیتابیس_مدیریت.md +│ │ ├── query_20260909_161708_1f0b7523_اصلاح_معماری_دسترسی_واحد_سازمانی_نقش_permission_و.md +│ │ ├── query_20260909_164343_d087dac1_چرا_در_صفحه_tickets_new_با_تغییر_واحد_کارشناسان_آن.md +│ │ ├── query_20260909_170048_f114f91f_چرا_تغییر_واحد_در_ui_فورا__خطای_دریافت_کارشناسان_م.md +│ │ ├── query_20260910_053611_613a8cc4_qa_role_organization_unit_permission_scope_workflo.md +│ │ ├── query_20260910_060824_3faac489_in__tickets_new__place_unit_and_category_side_by_s.md +│ │ ├── query_20260910_064145_f562e795_برای_واحدهای_فروش_و_مالی_حسابداری_دسته_بندی_های_مش.md +│ │ └── query_20260910_065345_ff4c3175_جستجوی_دسته_بندی_های__tickets_new_search_را_حرفه_ا.md +│ ├── reflections +│ │ └── LESSONS.md +│ ├── .graphify_analysis.json +│ ├── .graphify_ast.json +│ ├── .graphify_detect.json +│ ├── .graphify_extract.json +│ ├── .graphify_labels.json +│ ├── .graphify_labels.json.sig +│ ├── .graphify_learning.json +│ ├── .graphify_python +│ ├── .graphify_root +│ ├── .graphify_semantic.json +│ ├── .vocab.txt +│ ├── cost.json +│ ├── graph.html +│ ├── graph.json +│ ├── GRAPH_REPORT.md +│ └── manifest.json +├── static +│ ├── css +│ │ ├── app.css +│ │ ├── dispatches.css +│ │ ├── enhancements.css +│ │ ├── operations.css +│ │ ├── tailwind.css +│ │ ├── ticket-composer-v2.css +│ │ └── ticket-composer.css +│ ├── data +│ │ ├── iran-locations.json +│ │ └── iran-locations.LICENSE.md +│ ├── fonts +│ │ ├── vazirmatn-arabic-wght-normal.woff2 +│ │ └── vazirmatn-latin-wght-normal.woff2 +│ ├── images +│ │ ├── company-logo.png +│ │ ├── favicon.ico +│ │ └── pwa-icon.png +│ ├── js +│ │ ├── attachment-uploader.js +│ │ ├── chat-coach.js +│ │ ├── conversation.js +│ │ ├── image-viewer.js +│ │ ├── location-picker.js +│ │ ├── pwa.js +│ │ ├── staff-customer-picker.js +│ │ ├── ticket-intent-search.js +│ │ ├── ticket-unit-options.js +│ │ ├── ui.js +│ │ ├── voice-recorder.js +│ │ └── webotp.js +│ ├── src +│ │ └── tailwind.css +│ ├── vendor +│ │ ├── alpine-3.14.8.min.js +│ │ ├── compressor.min.js +│ │ └── htmx-2.0.3.min.js +│ └── manifest.webmanifest +├── templates +│ ├── accounts +│ │ ├── customer_login.html +│ │ ├── customer_password_login.html +│ │ ├── otp.html +│ │ ├── profile.html +│ │ └── staff_login.html +│ ├── includes +│ │ ├── auth_brand.html +│ │ └── auth_footer.html +│ ├── messaging +│ │ └── revisions.html +│ ├── pwa +│ │ └── service-worker.js +│ ├── tickets +│ │ ├── _image_viewer.html +│ │ ├── _messages.html +│ │ ├── _older_messages.html +│ │ ├── _staff_customer_results.html +│ │ ├── _staff_ticket_results.html +│ │ ├── customer_detail.html +│ │ ├── customer_list.html +│ │ ├── customer_new.html +│ │ ├── staff_contacts.html +│ │ ├── staff_create.html +│ │ ├── staff_customers.html +│ │ ├── staff_detail.html +│ │ ├── staff_dispatch_create.html +│ │ ├── staff_dispatch_detail.html +│ │ ├── staff_entity_form.html +│ │ ├── staff_groups.html +│ │ ├── staff_list.html +│ │ ├── staff_magic_audit.html +│ │ └── staff_sms_templates.html +│ └── base.html +├── test-results +│ ├── e2e +│ │ ├── tests_e2e_test_chat_coach.py__test_customer_sees_chat_guidance_after_two_rapid_messages +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_chat_coach.py__test_staff_sees_chat_guidance_after_two_rapid_replies +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_customer_unit_categories_e2e.py__test_customer_natural_language_search_selects_helpful_catalog_route[invoice] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_customer_unit_categories_e2e.py__test_customer_natural_language_search_selects_helpful_catalog_route[shipping] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_customer_unit_categories_e2e.py__test_customer_natural_language_search_selects_helpful_catalog_route[supplier] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_customer_unit_categories_e2e.py__test_customer_switches_unit_and_sees_its_categories +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_organization_permissions_e2e.py__test_e2e_05_06_limited_operation_owner_and_ceo_view_all[BULK-03] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_organization_permissions_e2e.py__test_e2e_05_06_limited_operation_owner_and_ceo_view_all[INQUIRY-04] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_07_sales_general_queue_creates_verifiable_ticket +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_08_repeated_staff_changes_store_only_last_choice +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_09_10_11_attachment_variants_are_saved_and_rendered[jpg-images0-documents0-expected0] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_09_10_11_attachment_variants_are_saved_and_rendered[mixed-images2-documents2-expected2] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_09_10_11_attachment_variants_are_saved_and_rendered[multi-file-images3-documents3-expected3] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_09_10_11_attachment_variants_are_saved_and_rendered[pdf-images1-documents1-expected1] +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_11_removed_attachment_is_not_submitted +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_public_ticket_workflows_e2e.py__test_e2e_12_sequential_unit_staff_category_and_attachment_state +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ ├── tests_e2e_test_pwa_versioning.py__test_pwa_registers_versioned_worker_and_matching_cache +│ │ │ ├── failure.png +│ │ │ └── trace.zip +│ │ └── tests_e2e_test_staff_operations_ui.py__test_staff_operations_pages_are_authorized_and_visually_styled +│ │ ├── failure.png +│ │ └── trace.zip +│ ├── customer-chat-guidance.png +│ └── staff-chat-guidance.png +├── tests +│ ├── e2e +│ │ ├── pages +│ │ │ ├── __init__.py +│ │ │ ├── auth.py +│ │ │ ├── base.py +│ │ │ ├── customer_new_ticket.py +│ │ │ └── tickets.py +│ │ ├── config.py +│ │ ├── conftest.py +│ │ ├── README.md +│ │ ├── test_chat_coach.py +│ │ ├── test_customer_unit_categories_e2e.py +│ │ ├── test_external_environment_smoke.py +│ │ ├── test_organization_permissions_e2e.py +│ │ ├── test_public_ticket_workflows_e2e.py +│ │ ├── test_pwa_versioning.py +│ │ └── test_staff_operations_ui.py +│ ├── js +│ │ └── chat-coach.test.mjs +│ ├── test_branding_operations.py +│ ├── test_customer_category_catalog.py +│ ├── test_customer_unit_categories.py +│ ├── test_domain.py +│ ├── test_env.py +│ ├── test_jalali_templates.py +│ ├── test_organizational_ticketing.py +│ ├── test_permission_architecture.py +│ ├── test_persian_text.py +│ ├── test_role_workflow_regression.py +│ └── test_views.py +├── .dockerignore +├── .env +├── .env.example +├── .env.local +├── .env.local.example +├── .gitignore +├── .npmrc +├── AGENTS.md +├── db.sqlite3 +├── docker-compose.yml +├── Dockerfile +├── manage.py +├── package-lock.json +├── package.json +├── project_tree.py +├── pytest.ini +├── README.md +├── requirements-dev.txt +├── requirements.txt +└── Software Requirements & Architecture Specification — سیستم تیکتینگ فروش.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..732fa36 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# سامانه تیکتینگ فروش + +پیاده‌سازی Django Modular Monolith بر اساس سند نیازمندی همین مخزن. رابط فارسی و RTL است و جریان‌های مشتری، کارشناس، دسترسی خصوصی، پیام، فایل، OTP، Magic Link و Notification Outbox را پوشش می‌دهد. + +## اجرای محلی + +```powershell +python -m venv .venv +.\.venv\Scripts\python -m pip install -r requirements.txt +.\.venv\Scripts\python manage.py migrate +.\.venv\Scripts\python manage.py bootstrap +.\.venv\Scripts\python manage.py createsuperuser +.\.venv\Scripts\python manage.py runserver +``` + +در محیط توسعه SQLite فعال است. OTP در ترمینال با پیشوند `[DEV OTP]` چاپ می‌شود. برای تست سریع، شماره `09202202129` با کد ثابت `12345` فقط در تنظیمات Development قابل ورود است؛ تنظیمات Production این میان‌بر را غیرفعال نگه می‌دارند. برای ایجاد کارشناس، User با `account_type=STAFF` و StaffProfile بسازید و او را عضو گروه `Sales Expert` یا `Sales Manager` کنید. + +## اجرای Production با Docker + +```powershell +Copy-Item .env.example .env +docker volume create customer_ticket_static +docker volume create customer_ticket_media +docker compose up --build +docker compose exec web python manage.py createsuperuser +``` + +قبل از استقرار مقادیر secret و host در `.env` را تغییر دهید. TLS و proxy اپلیکیشن توسط Traefik خود Coolify مدیریت می‌شود. migration، bootstrap و collectstatic هنگام شروع web به شکل خودکار اجرا می‌شوند. Nginx فقط static را سرو می‌کند و فایل‌های آپلودی خصوصی پس از کنترل مجوز مستقیماً توسط Django تحویل داده می‌شوند. + +راهنمای کامل تنظیم Coolify، volumeها، PostgreSQL خارجی و worker در [docs/DEPLOYMENT_COOLIFY.md](docs/DEPLOYMENT_COOLIFY.md) است. + +## تست + +```powershell +.\.venv\Scripts\python -m pytest -q +``` + +قواعد کلیدی در `apps/tickets/policies.py` متمرکز شده‌اند و عملیات حساس در `apps/tickets/services.py` تراکنشی هستند. +## Environment files + +- Direct development with `config.settings.dev` automatically loads `.env.local`. +- Production with `config.settings.prod` can load `.env`; Docker Compose also injects that file into its services. +- Set `DJANGO_ENV_FILE` in the process environment to select another file, such as `/etc/customer-ticket/server.env`. +- Existing process variables always take precedence over values in dotenv files. +- Copy `.env.local.example` to `.env.local` for a safe local starting point; real dotenv files are excluded from Git and Docker images. diff --git a/Software Requirements & Architecture Specification — سیستم تیکتینگ فروش.md b/Software Requirements & Architecture Specification — سیستم تیکتینگ فروش.md new file mode 100644 index 0000000..17106ac --- /dev/null +++ b/Software Requirements & Architecture Specification — سیستم تیکتینگ فروش.md @@ -0,0 +1,4029 @@ +# مشخصات کامل فنی و محصول +## سیستم تیکتینگ مشتریان و کارشناسان فروش + +**نسخه سند:** 1.0 +**نوع پروژه:** Web Application +**معماری:** Django Modular Monolith +**Backend:** Django + PostgreSQL +**Frontend:** Django Templates + HTMX + Alpine.js + Vanilla JavaScript + Tailwind CSS +**Deployment:** Docker / Docker Compose / Linux +**File Storage:** Local Server Storage +**Real-time:** نیاز نیست +**Redis:** نیاز نیست در نسخه اول +**Celery:** نیاز نیست در نسخه اول +**WebSocket:** نیاز نیست +**Microservice:** ممنوع/غیرضروری در V1 + +--- + +# 1. هدف پروژه + +هدف، طراحی و پیاده‌سازی یک سیستم تیکتینگ ساده، سریع، امن و قابل توسعه برای ارتباط مشتریان با کارشناسان فروش شرکت است. + +مشتریان عمدتاً برای مواردی مانند: + +- استعلام قیمت +- درصد تخفیف +- شرایط فروش +- موجودی +- شرایط ارسال +- درخواست نمایندگی +- مشاوره فروش +- سؤالات مربوط به محصولات +- شکایات +- سایر مسائل فروش + +با کارشناسان ارتباط برقرار می‌کنند. + +سیستم نباید تبدیل به یک Messenger دائمی شبیه WhatsApp یا Telegram شود. + +هر Ticket باید نماینده یک موضوع یا مسئله مشخص باشد. + +مشتری برای موضوع جدید باید Ticket جدید ایجاد کند. + +داخل یک Ticket امکان چند مرحله سؤال و جواب وجود دارد، اما فلسفه سیستم بر اساس Ticketing باقی می‌ماند. + +--- + +# 2. اهداف اصلی + +سیستم باید: + +1. ورود مشتری با شماره موبایل و OTP را فراهم کند. +2. ثبت‌نام اجباری سنتی نداشته باشد. +3. تعیین نام، نام خانوادگی و Password اختیاری باشد. +4. مشتری بتواند چند Ticket همزمان داشته باشد. +5. مشتری بتواند کارشناس موردنظر خود را انتخاب کند. +6. در صورت عدم انتخاب کارشناس، Ticket وارد صف عمومی کارشناسان شود. +7. کارشناس بتواند Ticket عمومی را برای خودش بردارد. +8. بعد از برداشتن Ticket، اطلاعات آن از سایر کارشناسان مخفی شود. +9. مدیران مجاز همیشه بتوانند همه Ticketها را مشاهده کنند. +10. امکان پاسخ متنی و Attachmentهای مختلف وجود داشته باشد. +11. Voice مستقیماً داخل مرورگر ضبط شود. +12. امکان Edit پیام وجود داشته باشد. +13. تاریخچه Edit کاملاً حفظ شود. +14. حذف پیام کارشناس به صورت Soft Delete باشد. +15. مدیر بتواند متن اصلی پیام حذف‌شده را مشاهده کند. +16. امکان Internal Note وجود داشته باشد. +17. امکان Transfer Ticket وجود داشته باشد. +18. امکان Share کردن Ticket با Expert دیگر وجود داشته باشد. +19. امکان Mention کارشناس وجود داشته باشد. +20. Notification مشتری از طریق SMS انجام شود. +21. Notification کارشناسان از طریق Dashboard و Bale انجام شود. +22. مشتری از طریق Magic Link بتواند سریع Ticket را باز کند. +23. Permission System منعطف باشد. +24. Priority و SLA قابل استفاده باشند. +25. فایل‌ها روی Storage محلی Server ذخیره شوند. + +--- + +# 3. محدوده کاربران + +حداکثر بار مورد انتظار در نسخه فعلی: + +- حدود 200 مشتری +- حداکثر 20 کارشناس فروش +- یک شرکت +- بدون Multi-Tenancy +- حجم پیام و Ticket نسبتاً پایین + +در نتیجه نیازی به معماری پیچیده Distributed وجود ندارد. + +--- + +# 4. معماری کلان + +معماری سیستم باید: + +**Modular Monolith** + +باشد. + +تمام Business Logic داخل یک Django Project قرار می‌گیرد ولی Domainها به Appهای منطقی جدا تقسیم می‌شوند. + +ساختار پیشنهادی: + +```text +project/ +│ +├── config/ +│ ├── settings/ +│ ├── urls.py +│ ├── asgi.py +│ └── wsgi.py +│ +├── apps/ +│ │ +│ ├── accounts/ +│ ├── customers/ +│ ├── staff/ +│ ├── tickets/ +│ ├── messaging/ +│ ├── attachments/ +│ ├── notifications/ +│ ├── integrations/ +│ │ ├── sms/ +│ │ └── bale/ +│ │ +│ └── core/ +│ +├── templates/ +├── static/ +├── media/ +├── tests/ +│ +├── docker/ +├── docker-compose.yml +├── Dockerfile +└── manage.py +``` + +--- + +# 5. معماری لایه‌ای داخل Appها + +Business Logic نباید مستقیماً داخل Viewها نوشته شود. + +ساختار پیشنهادی برای Appهایی مثل tickets: + +```text +tickets/ +├── models.py +├── forms.py +├── urls.py +├── selectors.py +├── policies.py +├── services/ +│ ├── create_ticket.py +│ ├── claim_ticket.py +│ ├── transfer_ticket.py +│ ├── ticket_access.py +│ └── status.py +├── views/ +├── admin.py +└── tests/ +``` + +### Responsibility + +`views` + +فقط: + +- HTTP +- Forms +- Redirect +- HTMX response + +را مدیریت کند. + +`services` + +Business Logic را اجرا کند. + +`selectors` + +Queryهای خواندن اطلاعات را مدیریت کند. + +`policies` + +Permission و authorization را مدیریت کند. + +--- + +# 6. اصل بسیار مهم Authorization + +هیچ Permission مهمی نباید فقط در: + +- Template +- JavaScript +- HTMX +- CSS + +اعمال شود. + +تمام Permissionها باید Server Side کنترل شوند. + +مخفی‌کردن یک Button در UI به معنی جلوگیری از عملیات نیست. + +--- + +# 7. User Model + +از ابتدای پروژه باید Custom User Model ایجاد شود. + +بعداً تغییر `AUTH_USER_MODEL` بسیار هزینه‌بر است. + +مدل مفهومی: + +```text +User +--------- +id +public_id +username +mobile +email +password +account_type +is_active +is_staff +created_at +updated_at +last_login +``` + +`account_type`: + +```text +CUSTOMER +STAFF +``` + +کاربر Customer می‌تواند Password نداشته باشد. + +در این حالت: + +```python +user.set_unusable_password() +``` + +استفاده شود. + +--- + +# 8. CustomerProfile + +```text +CustomerProfile +---------------- +user +first_name +last_name +customer_type +created_at +updated_at +``` + +`customer_type` اختیاری است. + +مقادیر اولیه: + +```text +PERSON +CONSUMER +SHOP +REPRESENTATIVE +COMPANY +OTHER +``` + +نمایش فارسی: + +```text +شخص +مصرف‌کننده نهایی +فروشگاه +نمایندگی +شرکت +سایر +``` + +--- + +# 9. StaffProfile + +```text +StaffProfile +------------- +user +display_name +avatar +mobile +bale_chat_id +bale_notifications_enabled +is_customer_selectable +sort_order +is_active +created_at +updated_at +``` + +فقط کارشناسانی که: + +```text +is_customer_selectable = True +``` + +دارند در صفحه انتخاب کارشناس به مشتری نمایش داده شوند. + +--- + +# 10. احراز هویت مشتری + +روش اصلی: + +```text +Mobile + ↓ +OTP + ↓ +Login +``` + +Password اجباری نیست. + +پس از ورود موفق، سیستم می‌تواند پیشنهاد دهد: + +> برای ورود آسان‌تر در آینده می‌توانید رمز عبور تعیین کنید. + +ولی این درخواست نباید Blocking باشد. + +--- + +# 11. ورود با Password + +در صورت تعیین Password، مشتری می‌تواند در آینده با: + +```text +mobile + password +``` + +هم وارد شود. + +با این حال روش OTP همچنان باید در صفحه Login در اولویت UX باشد. + +--- + +# 12. Staff Login + +کارشناسان و مدیران صفحه Login مستقل داشته باشند. + +مثلاً: + +```text +/staff/login/ +``` + +Customer login: + +```text +/login/ +``` + +Staff Login با Username/Password انجام شود. + +--- + +# 13. OTP Architecture + +سیستم SMS نباید وابسته به Provider خاص باشد. + +از Adapter Pattern و Protocol استفاده شود. + +Interface مفهومی: + +```python +class SMSProvider(Protocol): + + def send_otp( + self, + mobile: str, + code: str, + ) -> SMSResult: + ... + + def send_message( + self, + mobile: str, + message: str, + ) -> SMSResult: + ... +``` + +Providerهای آینده می‌توانند شامل: + +```text +KavenegarProvider +SMSIrProvider +IPPanelProvider +FarazSMSProvider +CustomSMSProvider +``` + +باشند. + +Business Logic حق ندارد مستقیم API Provider را Call کند. + +--- + +# 14. OTPChallenge Model + +```text +OTPChallenge +------------- +id +mobile +purpose +code_hash +expires_at +attempt_count +max_attempts +verified_at +created_at +request_ip +user_agent +``` + +Raw OTP نباید داخل Database ذخیره شود. + +--- + +# 15. تنظیمات OTP + +مقادیر اولیه پیشنهادی: + +```text +OTP Length: 6 digits +Expire: 2 minutes +Resend cooldown: 60 seconds +Max verification attempts: 5 +``` + +تمام موارد باید از Settings قابل تغییر باشند. + +Rate Limiting نیز باید برای: + +- mobile +- IP + +اعمال شود. + +Redis برای این حجم کاربر لازم نیست. + +Rate Limit می‌تواند با PostgreSQL انجام شود. + +--- + +# 16. شماره موبایل + +شماره‌ها باید قبل از ذخیره Normalize شوند. + +برای مثال: + +```text +09121234567 +``` + +به شکل Canonical: + +```text ++989121234567 +``` + +ذخیره شود. + +تمام جستجو و Unique Constraint بر اساس شماره Normalize‌شده باشد. + +--- + +# 17. TicketCategory + +Category در Database ذخیره شود. + +```text +TicketCategory +--------------- +id +title +slug +description +icon +sort_order +is_active +created_at +updated_at +``` + +مثال: + +```text +استعلام قیمت +درصد تخفیف +شرایط فروش +موجودی +شرایط ارسال +درخواست نمایندگی +شکایت +مشاوره +سایر +``` + +انتخاب Category برای Customer اجباری است. + +Category غیرفعال برای Ticketهای قدیمی باقی بماند ولی در Ticket جدید نمایش داده نشود. + +--- + +# 18. Ticket Model + +مدل اصلی: + +```text +Ticket +---------------------- +id +public_id +ticket_number +customer +category +subject + +status +priority + +selected_expert +owner + +origin_visibility + +created_at +updated_at +last_activity_at + +first_response_at +last_customer_message_at +last_staff_message_at + +first_response_due_at +resolution_due_at + +resolved_at +closed_at +reopened_at +``` + +--- + +# 19. Ticket Number + +Ticket باید شماره قابل خواندن داشته باشد. + +مثلاً: + +```text +TK-1405-001254 +``` + +اما Permission و URL نباید بر اساس این شماره امنیت ایجاد کنند. + +URLها از `public_id` یا UUID استفاده کنند. + +مثلاً: + +```text +/tickets/9d7b.../ +``` + +--- + +# 20. Subject + +Subject اجباری است. + +محدوده پیشنهادی: + +```text +5 تا 200 character +``` + +نمونه: + +> استعلام قیمت لوله 110 + +--- + +# 21. اولین پیام Ticket + +Ticket علاوه بر Subject باید یک Initial Message داشته باشد. + +Initial Message می‌تواند شامل: + +- Text +- Attachment +- Text + Attachments + +باشد. + +UX باید مشتری را تشویق کند مسئله را کامل توضیح دهد. + +--- + +# 22. Ticket Status + +Statusهای اصلی: + +```text +NEW +OPEN +WAITING_CUSTOMER +WAITING_STAFF +RESOLVED +CLOSED +REOPENED +``` + +--- + +# 23. معنی Statusها + +### NEW + +Ticket ایجاد شده ولی هنوز رسیدگی واقعی شروع نشده است. + +### OPEN + +کارشناس Ticket را برداشته یا رسیدگی به آن را شروع کرده است. + +### WAITING_CUSTOMER + +کارشناس پاسخ داده و اکنون انتظار پاسخ مشتری وجود دارد. + +### WAITING_STAFF + +مشتری پاسخ داده و سیستم منتظر پاسخ کارشناس است. + +### RESOLVED + +کارشناس مسئله را حل‌شده اعلام کرده است. + +### CLOSED + +Ticket بسته شده است. + +### REOPENED + +Ticket بسته یا Resolve شده بوده و Customer دوباره پیام داده است. + +--- + +# 24. Reopen + +اگر Ticket: + +```text +RESOLVED +``` + +یا: + +```text +CLOSED +``` + +باشد و Customer پیام جدید ارسال کند: + +Ticket خودکار به: + +```text +REOPENED +``` + +تغییر وضعیت دهد. + +این عملیات باید Transactional باشد. + +--- + +# 25. Priority + +Priorityهای اولیه: + +```text +NORMAL +IMPORTANT +URGENT +``` + +Default: + +```text +NORMAL +``` + +Customer اجازه تغییر Priority ندارد. + +کارشناس یا کاربران دارای Permission می‌توانند Priority را تغییر دهند. + +--- + +# 26. منطق انتخاب Expert توسط Customer + +سه حالت مهم وجود دارد. + +## حالت اول: Customer Expert انتخاب می‌کند + +مثلاً Customer: + +```text +احمد +``` + +را انتخاب می‌کند. + +در این حالت: + +```text +selected_expert = Ahmad +owner = NULL +``` + +و فقط: + +- Ahmad +- مدیران دارای دسترسی کامل + +می‌توانند Ticket را مشاهده کنند. + +سایر کارشناسان نباید حتی محتوای Ticket را ببینند. + +این Ticket وارد Public Queue نمی‌شود. + +--- + +# 27. عدم انتخاب Expert + +اگر Customer کارشناسی انتخاب نکند: + +```text +selected_expert = NULL +owner = NULL +``` + +Ticket وارد Public Queue می‌شود. + +تمام کارشناسان مجاز می‌توانند: + +- عنوان +- محتوا +- Attachment +- Customer +- سابقه مجاز + +را مشاهده کنند. + +--- + +# 28. Claim / Lock Ticket + +در Public Queue هر Expert می‌تواند: + +> برداشتن تیکت + +یا: + +> اختصاص به من + +را انتخاب کند. + +پس: + +```text +owner = current_user +``` + +خواهد شد. + +--- + +# 29. Race Condition در Claim + +ممکن است دو Expert همزمان روی: + +> اختصاص به من + +کلیک کنند. + +Claim باید با: + +```python +transaction.atomic() +``` + +و: + +```python +select_for_update() +``` + +پیاده‌سازی شود. + +فقط اولین Expert موفق شود. + +Expert دوم پیام مناسب دریافت کند: + +> این تیکت لحظاتی قبل توسط «احمد رضایی» برداشته شد. + +--- + +# 30. رفتار Ticket بعد از Claim + +بعد از Claim: + +فقط: + +- Owner +- کارشناسان Shared +- مدیران دارای Permission + +حق مشاهده محتوای Ticket را دارند. + +سایر کارشناسان فقط اطلاعات محدود زیر را ببینند: + +```text +Ticket Title +Owner +Locked/Assigned indicator +``` + +و نباید قادر باشند: + +- Message را بخوانند +- Attachment ببینند +- Customer information ببینند +- Reply کنند +- Internal Note ببینند + +--- + +# 31. Ticket انتخاب‌شده توسط Customer + +یک تفاوت مهم: + +اگر Customer از ابتدا Expert خاصی انتخاب کرده باشد، سایر کارشناسان حتی Metadata آن را در Public Queue نبینند. + +این Ticket خصوصی محسوب می‌شود. + +--- + +# 32. TicketAccessGrant + +برای دادن دسترسی به Expertهای دیگر مدل جداگانه ایجاد شود. + +```text +TicketAccessGrant +------------------ +id +ticket +user +granted_by +permission_level +created_at +revoked_at +``` + +در V1 می‌توان `permission_level` را ساده نگه داشت: + +```text +FULL +``` + +ولی فیلد از ابتدا وجود داشته باشد تا بعداً: + +```text +VIEW_ONLY +REPLY +FULL +``` + +قابل توسعه باشد. + +--- + +# 33. Share Ticket + +Owner یا Manager دارای Permission بتواند Expert دیگری را به Ticket اضافه کند. + +عملیات UI: + +> اشتراک با کارشناس + +پس از Share، Expert موردنظر: + +- محتوای Ticket +- پیام‌ها +- فایل‌ها + +را مشاهده می‌کند و در صورت FULL بودن دسترسی پاسخ می‌دهد. + +--- + +# 34. Revoke Access + +Owner یا مدیر مجاز بتواند Access یک Expert را حذف کند. + +Manager دارای Full Access هرگز توسط Ticket Grant محدود نمی‌شود. + +--- + +# 35. Transfer Ticket + +Owner بتواند Ticket را بدون نیاز به تأیید مدیر به Expert دیگری Transfer کند. + +مثلاً: + +```text +Ahmad → Reza +``` + +پس: + +```text +owner = Reza +``` + +شود. + +Transfer باید Transactional باشد. + +--- + +# 36. دسترسی Owner قبلی بعد از Transfer + +Default Behavior: + +پس از Transfer، Owner قبلی دسترسی اختصاصی خود را از دست می‌دهد. + +در صورت نیاز باید به عنوان Shared Expert به Ticket اضافه شود. + +این رفتار از نشت ناخواسته اطلاعات جلوگیری می‌کند. + +--- + +# 37. TicketEvent + +Audit Log عمومی در V1 لازم نیست. + +اما Domain History لازم است. + +مدل: + +```text +TicketEvent +------------ +id +ticket +event_type +actor +metadata +created_at +``` + +Eventها: + +```text +CREATED +CLAIMED +TRANSFERRED +SHARED +ACCESS_REVOKED +STATUS_CHANGED +PRIORITY_CHANGED +REOPENED +RESOLVED +CLOSED +``` + +این مدل برای: + +- Timeline +- SLA +- Debug +- تاریخچه Transfer + +استفاده می‌شود. + +این سیستم جای Audit Log جامع را نمی‌گیرد. + +--- + +# 38. Permissions + +Role names نباید داخل Business Logic Hard-Code شوند. + +یعنی کد نباید بگوید: + +```python +if user.role == "manager": +``` + +بلکه: + +```python +user.has_perm(...) +``` + +یا Ticket Policy Service استفاده شود. + +--- + +# 39. Django Groups + +برای ساده نگه‌داشتن معماری از: + +- Django Groups +- Django Permissions + +استفاده شود. + +مثلاً Groupهای اولیه: + +```text +Sales Expert +Sales Manager +Administrator +``` + +نام Group Business Logic نیست. + +Permission تعیین‌کننده رفتار است. + +--- + +# 40. Permissions پیشنهادی + +```text +tickets.view_all_tickets +tickets.view_private_ticket +tickets.claim_ticket +tickets.transfer_ticket +tickets.share_ticket +tickets.change_ticket_status +tickets.change_ticket_priority + +tickets.view_customer_history + +messaging.add_internal_note +messaging.edit_own_message +messaging.delete_own_message +messaging.view_deleted_message_content +messaging.view_message_revisions + +attachments.download_attachment + +staff.manage_staff +tickets.manage_categories +attachments.manage_attachment_policy +``` + +--- + +# 41. مدیران + +مدیرانی که: + +```text +tickets.view_all_tickets +``` + +دارند باید بدون استثناء تمام Ticketها را مشاهده کنند. + +فرقی نمی‌کند: + +- Customer Expert انتخاب کرده باشد. +- Ticket Locked شده باشد. +- Ticket Shared نشده باشد. +- Ticket متعلق به Expert دیگری باشد. + +--- + +# 42. TicketPolicyService + +Permission Ticket باید در یک نقطه مرکزی باشد. + +مثلاً: + +```python +TicketPolicy.can_view(user, ticket) + +TicketPolicy.can_reply(user, ticket) + +TicketPolicy.can_claim(user, ticket) + +TicketPolicy.can_transfer(user, ticket) + +TicketPolicy.can_share(user, ticket) + +TicketPolicy.can_view_deleted_content(user, ticket) +``` + +هیچ View نباید منطق Permission را دوباره بنویسد. + +--- + +# 43. Message Model + +```text +Message +--------------- +id +public_id +ticket +author + +body +message_type + +is_internal + +created_at +updated_at +edited_at + +deleted_at +deleted_by +``` + +--- + +# 44. انواع Message + +```text +PUBLIC +INTERNAL_NOTE +``` + +Customer فقط PUBLIC را مشاهده می‌کند. + +--- + +# 45. Internal Note + +Expert بتواند Note داخلی ایجاد کند. + +مثال: + +> این مشتری تخفیف ویژه درخواست کرده، قبل از اعلام قیمت با مدیر هماهنگ شود. + +Internal Note: + +- در Timeline کارشناسان نمایش داده شود. +- از پیام‌های معمولی کاملاً متمایز باشد. +- Customer هرگز آن را نبیند. +- در SMS Customer اثر نداشته باشد. + +--- + +# 46. Edit Message + +Customer و Expert بتوانند Message خودشان را Edit کنند. + +بعد از Edit: + +```text +edited_at +``` + +ثبت شود. + +در UI: + +> ویرایش شده + +نمایش داده شود. + +--- + +# 47. MessageRevision + +قبل از تغییر متن، نسخه قبلی ذخیره شود. + +```text +MessageRevision +---------------- +id +message +body +edited_by +created_at +``` + +مثلاً: + +نسخه 1: + +```text +قیمت 1,200,000 تومان +``` + +نسخه جدید: + +```text +قیمت 1,250,000 تومان +``` + +هر دو باید قابل بازیابی برای Manager مجاز باشند. + +--- + +# 48. مشاهده Revision + +Customer الزاماً Revision History را نمی‌بیند. + +Manager یا فرد دارای: + +```text +messaging.view_message_revisions +``` + +می‌تواند تاریخچه را مشاهده کند. + +--- + +# 49. Delete Message + +Expert می‌تواند Message خودش را Delete کند. + +اما: + +**هیچ Hard Delete انجام نشود.** + +ممنوع: + +```python +message.delete() +``` + +به جای آن: + +```text +deleted_at +deleted_by +``` + +ثبت شود. + +Body اصلی در Database باقی بماند. + +--- + +# 50. نمایش پیام حذف‌شده + +Customer و کارشناسان عادی ببینند: + +> این پیام توسط فرستنده حذف شده است. + +Manager دارای Permission بتواند: + +- Original Body +- Attachments +- Author +- Delete time + +را مشاهده کند. + +--- + +# 51. Attachments روی Message حذف‌شده + +Attachmentهای Message حذف‌شده نباید از Disk حذف شوند. + +مدیر باید همچنان بتواند آن‌ها را مشاهده کند. + +--- + +# 52. MessageMention + +Mention نباید فقط Regex روی متن باشد. + +مدل: + +```text +MessageMention +--------------- +message +mentioned_user +created_at +``` + +در Frontend Autocomplete Expert نمایش داده شود. + +--- + +# 53. Mention و Access + +Mention به‌تنهایی نباید Permission امنیتی را دور بزند. + +اگر Expert Mention‌شده به Ticket دسترسی ندارد: + +سیستم باید ابتدا Access مناسب ایجاد کند یا UI به فرستنده اعلام کند: + +> برای Mention کردن این کارشناس ابتدا باید دسترسی تیکت به او داده شود. + +Owner و Manager می‌توانند: + +> Share + Mention + +را در یک Flow انجام دهند. + +--- + +# 54. Bale Notification برای Mention + +پس از Mention، Expert از طریق Bale Notification دریافت کند. + +مثال: + +```text +تیکت TK-1405-1254 + +علی رضایی شما را Mention کرد: + +«لطفاً شرایط تخفیف این مشتری را بررسی کنید.» + +مشاهده تیکت: +https://support.example.com/... +``` + +--- + +# 55. Attachment Model + +```text +Attachment +---------------- +id +public_id +message + +original_name +stored_name +relative_path + +attachment_type + +mime_type +extension +file_size + +uploaded_by +created_at +``` + +--- + +# 56. Attachment Types + +سیستم باید حداقل پشتیبانی کند: + +```text +IMAGE +VIDEO +VOICE +PDF +WORD +EXCEL +ZIP +RAR +OTHER +``` + +--- + +# 57. AttachmentPolicy + +محدودیت‌ها نباید Hard-Code باشند. + +مدل: + +```text +AttachmentPolicy +----------------- +attachment_type +is_enabled + +max_file_size +max_files_per_message + +allowed_extensions +allowed_mime_types + +sort_order +updated_at +``` + +--- + +# 58. محدودیت اولیه پیشنهادی + +این اعداد فقط Default هستند و باید قابل تغییر باشند. + +```text +IMAGE 10 MB +VOICE 25 MB +VIDEO 100 MB +PDF 30 MB +WORD 30 MB +EXCEL 30 MB +ZIP 50 MB +RAR 50 MB +``` + +--- + +# 59. Multiple Attachments + +هر Message باید بتواند چند Attachment داشته باشد. + +مثلاً: + +```text +متن ++ 4 تصویر ++ یک PDF ++ یک Voice +``` + +مجاز باشد. + +--- + +# 60. Attachment Composer UI + +قبل از ارسال، UI باید نشان دهد: + +- Thumbnail تصویر +- نام فایل +- Size +- Type +- Progress +- حذف قبل از ارسال +- Error فایل نامعتبر + +--- + +# 61. File Validation + +صرفاً Extension کافی نیست. + +Server باید بررسی کند: + +1. Extension +2. MIME Type +3. File Signature + +برای File Signature استفاده از `libmagic` یا ابزار مشابه توصیه می‌شود. + +--- + +# 62. ZIP و RAR + +ZIP/RAR نباید روی Server Extract شوند. + +فقط به عنوان Attachment ذخیره شوند. + +این موضوع برای جلوگیری از: + +- path traversal +- zip bombs +- فایل‌های اجرایی داخلی + +مهم است. + +--- + +# 63. File Name Security + +نام واقعی فایل کاربر نباید مستقیماً نام فایل Storage باشد. + +مثلاً: + +```text +invoice.pdf +``` + +روی Disk به: + +```text +1b3ef72c-....pdf +``` + +تبدیل شود. + +`original_name` فقط Metadata است. + +--- + +# 64. Media Storage + +Storage کاملاً Local Server است. + +ساختار پیشنهادی: + +```text +/media/ +└── tickets/ + └── 2026/ + └── 09/ + └── / + └── / + ├── file_uuid.jpg + ├── file_uuid.pdf + └── file_uuid.webm +``` + +--- + +# 65. Private Media + +فایل‌های Ticket نباید مستقیماً از Public `/media/` قابل دسترسی باشند. + +این URL ناامن است: + +```text +https://site.com/media/tickets/xxx.pdf +``` + +چون Permission را دور می‌زند. + +Download باید ابتدا Django Authorization را بررسی کند. + +--- + +# 66. Nginx X-Accel-Redirect + +روش پیشنهادی: + +1. درخواست Download وارد Django می‌شود. +2. Django Permission را بررسی می‌کند. +3. در صورت مجاز بودن Header مخصوص Nginx برگردانده می‌شود. +4. Nginx فایل را Efficient ارسال می‌کند. + +Location فایل‌ها در Nginx باید: + +```text +internal +``` + +باشد. + +--- + +# 67. Voice Recording + +Voice با: + +```text +MediaDevices.getUserMedia() +MediaRecorder +``` + +پیاده‌سازی شود. + +نیازی به Plugin خارجی نیست. + +--- + +# 68. Voice UI + +Voice Recorder شامل: + +```text +Record +Stop +Pause +Resume +Timer +Preview +Delete +Re-record +Send +``` + +باشد. + +حداقل امکانات: + +```text +🎤 ضبط +⏱ مدت ضبط +▶ پخش +🗑 حذف +➤ ارسال +``` + +--- + +# 69. Browser Compatibility Voice + +Format ضبط ممکن است بین Browserها متفاوت باشد. + +مثلاً: + +```text +audio/webm +audio/ogg +audio/mp4 +``` + +Frontend باید با: + +```javascript +MediaRecorder.isTypeSupported() +``` + +Format مناسب را انتخاب کند. + +Server نباید فقط یک MIME Type را قبول کند. + +--- + +# 70. Video + +در V1 نیازی به Transcoding Video نیست. + +Video همان Format مجاز Upload می‌شود. + +در Browser در صورت پشتیبانی: + +```html +