diff --git a/AI_PERSON_INTEGRATION.md b/AI_PERSON_INTEGRATION.md new file mode 100644 index 00000000..0ea5125d --- /dev/null +++ b/AI_PERSON_INTEGRATION.md @@ -0,0 +1,127 @@ +# هوش مصنوعی حسابیکس - یکپارچه‌سازی با اطلاعات اشخاص + +## خلاصه + +این پروژه قابلیت‌های جدیدی به سیستم هوش مصنوعی حسابیکس اضافه کرده است که به کاربران امکان دسترسی پویا به اطلاعات اشخاص را می‌دهد. هوش مصنوعی حالا می‌تواند به سوالات مربوط به اشخاص، موجودی‌ها و تراکنش‌های مالی پاسخ دهد. + +## ویژگی‌های جدید + +### 1. دسترسی به اطلاعات اشخاص +- نمایش اطلاعات کامل اشخاص شامل نام، کد، آدرس، تلفن و غیره +- محاسبه و نمایش موجودی مالی اشخاص +- نمایش تراکنش‌های اخیر هر شخص +- نمایش کارت‌های بانکی و اطلاعات مالی + +### 2. جستجوی هوشمند +- جستجو بر اساس نام، کد یا شماره تلفن +- پیشنهادات جستجو +- فیلتر بر اساس نوع اشخاص (مشتری، تامین‌کننده، کارمند) + +### 3. امنیت و حریم خصوصی +- هر کاربر فقط به اطلاعات اشخاص کسب و کار خود دسترسی دارد +- بررسی دسترسی‌ها قبل از نمایش اطلاعات +- محافظت از اطلاعات حساس + +## ساختار فایل‌ها + +### Backend (PHP/Symfony) + +#### سرویس‌های جدید: +- `PersonDataService.php`: مدیریت داده‌های اشخاص +- `AIService.php`: به‌روزرسانی شده برای پشتیبانی از اطلاعات اشخاص + +#### کنترلرهای جدید: +- `wizardController.php`: اضافه شدن endpoint های جدید برای اشخاص + +#### API Endpoints جدید: +- `POST /api/wizard/persons/search`: جستجوی اشخاص +- `GET /api/wizard/persons/{personId}`: دریافت اطلاعات شخص +- `GET /api/wizard/persons/{personId}/transactions`: دریافت تراکنش‌های شخص + +### Frontend (Vue.js) + +#### کامپوننت‌های جدید: +- `PersonInfo.vue`: نمایش اطلاعات کامل شخص +- `home.vue`: به‌روزرسانی شده برای پشتیبانی از قابلیت‌های جدید + +## نحوه استفاده + +### 1. سوالات مربوط به اشخاص +کاربران می‌توانند سوالاتی مانند موارد زیر بپرسند: +- "اطلاعات شخص احمد محمدی" +- "موجودی مشتری علی رضایی" +- "تراکنش‌های تامین‌کننده شرکت ABC" +- "لیست کارمندان" + +### 2. جستجوی مستقیم +- استفاده از پیشنهادات موجود در رابط کاربری +- تایپ نام یا کد شخص در چت + +### 3. نمایش اطلاعات +- اطلاعات شخص در دیالوگ جداگانه نمایش داده می‌شود +- شامل موجودی مالی، تراکنش‌ها و اطلاعات تماس +- امکان مشاهده جزئیات کامل + +## امنیت + +### بررسی دسترسی‌ها: +- هر درخواست ابتدا بررسی می‌شود که کاربر دسترسی لازم را داشته باشد +- اطلاعات فقط برای کسب و کار مربوطه نمایش داده می‌شود +- API endpoints محافظت شده با سیستم احراز هویت + +### محافظت از داده‌ها: +- شماره کارت‌های بانکی ماسک می‌شوند +- اطلاعات حساس فیلتر می‌شوند +- لاگ تمام درخواست‌ها ثبت می‌شود + +## تنظیمات + +### پرامپ هوش مصنوعی: +سیستم به طور خودکار اطلاعات اشخاص را به پرامپ اضافه می‌کند تا هوش مصنوعی بتواند به سوالات مربوطه پاسخ دهد. + +### محدودیت‌ها: +- حداکثر 20 نتیجه در جستجو +- حداکثر 10 تراکنش در نمایش +- محدودیت دسترسی بر اساس کسب و کار + +## نمونه استفاده + +```javascript +// جستجوی شخص +const persons = await this.searchPersons('احمد محمدی'); + +// دریافت اطلاعات شخص +const personDetails = await this.getPersonDetails(personId); + +// دریافت تراکنش‌ها +const transactions = await this.getPersonTransactions(personId, 10); +``` + +## آینده‌نگری + +### قابلیت‌های پیشنهادی: +1. گزارش‌گیری پیشرفته از اشخاص +2. تحلیل روند تراکنش‌ها +3. پیش‌بینی موجودی بر اساس الگوهای گذشته +4. یکپارچه‌سازی با سیستم اعلان‌ها +5. پشتیبانی از تصاویر پروفایل اشخاص + +### بهبودهای فنی: +1. کش کردن اطلاعات پرکاربرد +2. بهینه‌سازی کوئری‌های دیتابیس +3. پشتیبانی از pagination برای لیست‌های بزرگ +4. اضافه کردن فیلترهای پیشرفته + +## عیب‌یابی + +### مشکلات رایج: +1. **خطای دسترسی**: بررسی کنید که کاربر دسترسی AI داشته باشد +2. **عدم یافتن شخص**: نام یا کد را بررسی کنید +3. **خطای شبکه**: اتصال اینترنت را بررسی کنید + +### لاگ‌ها: +تمام خطاها در console مرورگر و لاگ‌های سرور ثبت می‌شوند. + +## پشتیبانی + +برای گزارش مشکلات یا درخواست ویژگی‌های جدید، لطفاً با تیم توسعه تماس بگیرید. \ No newline at end of file diff --git a/hesabixCore/config/services.yaml b/hesabixCore/config/services.yaml index 1567675d..af0b248c 100644 --- a/hesabixCore/config/services.yaml +++ b/hesabixCore/config/services.yaml @@ -95,3 +95,8 @@ services: App\Twig\NumberFormatExtension: tags: ['twig.extension'] + + App\Cog\PersonService: + arguments: + $entityManager: '@doctrine.orm.entity_manager' + $access: '@App\Service\Access' diff --git a/hesabixCore/docs/AI_Tools_System.md b/hesabixCore/docs/AI_Tools_System.md new file mode 100644 index 00000000..14eb3d7f --- /dev/null +++ b/hesabixCore/docs/AI_Tools_System.md @@ -0,0 +1,261 @@ +# سیستم هوشمند هوش مصنوعی حسابیکس - نسخه 2.0 + +## مقدمه + +سیستم جدید هوشمند هوش مصنوعی حسابیکس با رویکردی کاملاً متفاوت طراحی شده است. در این نسخه، به جای تشخیص دستی دستورات، به هوش مصنوعی گفته می‌شود که چه ابزارهایی دارد و اجازه داده می‌شود خودش تصمیم بگیرد که از کدام ابزار استفاده کند. + +## ویژگی‌های کلیدی + +### 🔧 تشخیص خودکار ابزارها +- هوش مصنوعی خودش تشخیص می‌دهد که چه ابزاری مناسب است +- نیازی به تشخیص دستی دستورات نیست +- انعطاف‌پذیری بالا در درک درخواست‌های کاربر + +### 📝 پرامپ‌های سیستمی هوشمند +- پرامپ‌های جامع که تمام ابزارها را معرفی می‌کنند +- مثال‌های کاربردی برای هر ابزار +- قوانین و محدودیت‌های استفاده + +### 🎯 تعامل چندمرحله‌ای +- امکان انجام عملیات پیچیده در چند مرحله +- جمع‌آوری اطلاعات تدریجی +- تجربه کاربری بهتر + +## معماری سیستم + +### 1. AIService (سرویس اصلی) +```php +class AIService { + // پرامپ سیستمی هوشمند + private function getSystemPrompt(): string + + // پردازش پاسخ هوش مصنوعی + private function processAIResponse(string $aiResponse, ?Business $business, $user): array + + // استخراج دستورات ابزار + private function extractToolCommands(string $aiResponse): array + + // اجرای دستورات ابزار + private function executeToolCommand(array $command, ?Business $business, $user): array +} +``` + +### 2. PersonManagementService (مدیریت اشخاص) +```php +class PersonManagementService { + // ابزارهای مدیریت اشخاص + public function addPerson(array $params, Business $business, $user): array + public function deletePerson(array $params, Business $business, $user): array + public function editPerson(array $params, Business $business, $user): array + public function showPerson(array $params, Business $business, $user): array + public function searchPersons(array $params, Business $business): array +} +``` + +## ابزارهای موجود + +### مدیریت اشخاص (person_management) + +#### 1. افزودن شخص جدید +```bash +add_person{name:نام شخص} +``` + +**مثال‌ها:** +- `add_person{name:علی}` +- `add_person{name:احمد محمدی}` + +#### 2. حذف شخص +```bash +delete_person{name:نام شخص} +``` + +**مثال‌ها:** +- `delete_person{name:علی}` +- `delete_person{name:محسن محمودی}` + +#### 3. ویرایش شخص +```bash +edit_person{name:نام شخص, phone:موبایل, address:آدرس, email:ایمیل} +``` + +**مثال‌ها:** +- `edit_person{name:علی, phone:09123456789}` +- `edit_person{name:احمد, address:تهران، خیابان ولیعصر}` + +#### 4. نمایش مشخصات +```bash +show_person{name:نام شخص} +``` + +**مثال‌ها:** +- `show_person{name:علی}` +- `show_person{name:محسن محمودی}` + +#### 5. جستجوی اشخاص +```bash +search_persons{search:متن جستجو, limit:تعداد نتایج} +``` + +**مثال‌ها:** +- `search_persons{search:علی}` +- `search_persons{search:محمد, limit:5}` + +## پرامپ سیستمی + +پرامپ سیستمی شامل موارد زیر است: + +### معرفی ابزارها +``` +شما یک دستیار هوشمند برای سیستم حسابداری حسابیکس هستید. شما دسترسی به ابزارهای زیر دارید: + +🔧 ابزارهای موجود: + +1. **مدیریت اشخاص** (person_management): + - افزودن شخص جدید: add_person{name:نام شخص} + - حذف شخص: delete_person{name:نام شخص} + - ویرایش شخص: edit_person{name:نام شخص, phone:موبایل, address:آدرس, email:ایمیل} + - نمایش مشخصات: show_person{name:نام شخص} + - جستجوی اشخاص: search_persons{search:متن جستجو, limit:تعداد نتایج} +``` + +### قوانین استفاده +``` +📋 قوانین استفاده: +- اگر کاربر درخواست عملیات مدیریت اشخاص دارد، از دستورات بالا استفاده کنید +- نام شخص می‌تواند نام مستعار یا نام کامل باشد +- برای عملیات پیچیده، ابتدا اطلاعات را جمع‌آوری کنید +- همیشه پاسخ فارسی و واضح ارائه دهید +``` + +### مثال‌های کاربردی +``` +💡 مثال‌های استفاده: +- 'علی رو حذف کن' → delete_person{name:علی} +- 'شخص جدید با نام احمد اضافه کن' → add_person{name:احمد} +- 'مشخصات محسن رو نشون بده' → show_person{name:محسن} +``` + +## جریان کار + +### 1. دریافت درخواست کاربر +``` +کاربر: "شخص علی را حذف کن" +``` + +### 2. ساخت پرامپ هوشمند +``` +پرامپ = پرامپ سیستمی + اطلاعات کسب و کار + سوال کاربر +``` + +### 3. ارسال به هوش مصنوعی +``` +هوش مصنوعی پرامپ را دریافت کرده و تصمیم می‌گیرد که از ابزار مناسب استفاده کند +``` + +### 4. تشخیص دستورات ابزار +``` +پاسخ هوش مصنوعی: "برای حذف شخص علی، از دستور delete_person{name:علی} استفاده می‌کنم." +``` + +### 5. استخراج و اجرای دستورات +``` +دستور استخراج شده: delete_person{name:علی} +نتیجه اجرا: "شخص علی با موفقیت حذف شد." +``` + +### 6. ساخت پاسخ نهایی +``` +پاسخ نهایی = پاسخ هوش مصنوعی + نتایج ابزارها +``` + +## مزایای سیستم جدید + +### 🚀 هوشمندی بیشتر +- هوش مصنوعی خودش تصمیم می‌گیرد +- نیازی به تشخیص دستی دستورات نیست +- انعطاف‌پذیری بالا در درک درخواست‌ها + +### 🔧 قابلیت توسعه +- افزودن ابزارهای جدید آسان است +- پرامپ‌ها قابل به‌روزرسانی هستند +- معماری مقیاس‌پذیر + +### 🎯 تجربه کاربری بهتر +- تعامل طبیعی‌تر +- پاسخ‌های هوشمندانه‌تر +- پشتیبانی از عملیات پیچیده + +### 🛡️ امنیت و کنترل +- تمام عملیات در لاگ ثبت می‌شود +- بررسی دسترسی کاربران +- کنترل خطاها + +## توسعه آینده + +### ابزارهای پیشنهادی +1. **مدیریت محصولات** + - افزودن، ویرایش، حذف محصولات + - مدیریت موجودی + - قیمت‌گذاری + +2. **مدیریت تراکنش‌ها** + - ثبت تراکنش‌های مالی + - گزارش‌گیری + - تحلیل داده‌ها + +3. **گزارش‌گیری هوشمند** + - گزارش‌های مالی + - تحلیل‌های آماری + - پیش‌بینی‌ها + +4. **مدیریت حساب‌ها** + - مدیریت حساب‌های بانکی + - صندوق‌ها + - حقوق‌ها + +### بهبودهای پیشنهادی +1. **یادگیری ماشین** + - بهبود تشخیص دستورات + - شخصی‌سازی پاسخ‌ها + - پیش‌بینی نیازهای کاربر + +2. **پشتیبانی چندزبانه** + - پشتیبانی از زبان‌های مختلف + - تشخیص خودکار زبان + - ترجمه خودکار + +3. **یکپارچه‌سازی پیشرفته** + - اتصال به سرویس‌های خارجی + - API های پیشرفته + - وب‌هوک‌ها + +## نکات فنی + +### مدیریت خطاها +- بررسی وجود کلیدهای مورد نیاز +- مدیریت خطاهای شبکه +- لاگ‌گیری کامل + +### بهینه‌سازی عملکرد +- کش‌گذاری پاسخ‌ها +- کاهش درخواست‌های تکراری +- بهینه‌سازی پرامپ‌ها + +### امنیت +- بررسی دسترسی کاربران +- اعتبارسنجی ورودی‌ها +- محافظت از داده‌های حساس + +## نتیجه‌گیری + +سیستم جدید هوشمند هوش مصنوعی حسابیکس با رویکردی نوآورانه و انعطاف‌پذیر طراحی شده است. این سیستم قابلیت توسعه بالایی دارد و می‌تواند به راحتی با نیازهای آینده سازگار شود. + +مزایای اصلی این سیستم عبارتند از: +- هوشمندی بیشتر در تشخیص دستورات +- انعطاف‌پذیری بالا +- قابلیت توسعه آسان +- تجربه کاربری بهتر +- امنیت و کنترل بیشتر + +این سیستم پایه‌ای محکم برای توسعه‌های آینده فراهم می‌کند و می‌تواند به عنوان یک دستیار هوشمند واقعی برای کاربران حسابیکس عمل کند. \ No newline at end of file diff --git a/hesabixCore/migrations/Version20241201000000.php b/hesabixCore/migrations/Version20241201000000.php new file mode 100644 index 00000000..dfaaa62b --- /dev/null +++ b/hesabixCore/migrations/Version20241201000000.php @@ -0,0 +1,37 @@ +addSql('CREATE TABLE postal_code_inquiry ( + id INT AUTO_INCREMENT NOT NULL, + postal_code VARCHAR(10) NOT NULL, + address_data JSON NOT NULL, + created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', + updated_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', + UNIQUE INDEX UNIQ_POSTAL_CODE (postal_code), + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE postal_code_inquiry'); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Cog/PersonService.php b/hesabixCore/src/Cog/PersonService.php new file mode 100644 index 00000000..cb29b8ec --- /dev/null +++ b/hesabixCore/src/Cog/PersonService.php @@ -0,0 +1,78 @@ +entityManager = $entityManager; + $this->access = $access; + } + + /** + * دریافت اطلاعات شخص + * + * @param string $code کد شخص + * @param array $acc اطلاعات دسترسی + * @return array اطلاعات شخص + * @throws \ReflectionException + */ + public function getPersonInfo(string $code, array $acc): array + { + $person = $this->entityManager->getRepository(Person::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code + ]); + + $types = $this->entityManager->getRepository(PersonType::class)->findAll(); + $response = Explore::ExplorePerson($person, $types); + + $rows = $this->entityManager->getRepository(HesabdariRow::class)->findBy([ + 'person' => $person, + 'bid' => $acc['bid'] + ]); + + $bs = 0; + $bd = 0; + + foreach ($rows as $row) { + try { + $doc = $row->getDoc(); + if ($doc && $doc->getMoney() && $doc->getYear() && + $doc->getMoney()->getId() == $acc['money']->getId() && + $doc->getYear()->getId() == $acc['year']->getId()) { + $bs += (float) $row->getBs(); // بستانکار + $bd += (float) $row->getBd(); // بدهکار + } + } catch (\Exception $e) { + // در صورت بروز خطا، این ردیف را نادیده می‌گیریم و به ردیف بعدی می‌رویم + continue; + } + } + + $response['bs'] = $bs; + $response['bd'] = $bd; + $response['balance'] = $bs - $bd; + + return $response; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/AIConversationController.php b/hesabixCore/src/Controller/AIConversationController.php new file mode 100644 index 00000000..31435124 --- /dev/null +++ b/hesabixCore/src/Controller/AIConversationController.php @@ -0,0 +1,245 @@ +hasRole('join'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $params = $request->getPayload()->all(); + $search = $params['search'] ?? ''; + $category = $params['category'] ?? ''; + + $conversationRepo = $entityManager->getRepository(AIConversation::class); + + if (!empty($search)) { + $conversations = $conversationRepo->searchByTitle($acc['user'], $acc['bid'], $search); + } elseif (!empty($category)) { + $conversations = $conversationRepo->findByCategory($acc['user'], $acc['bid'], $category); + } else { + $conversations = $conversationRepo->findActiveConversations($acc['user'], $acc['bid']); + } + + $result = []; + foreach ($conversations as $conversation) { + $messageRepo = $entityManager->getRepository(AIMessage::class); + $lastMessage = $messageRepo->findLastMessageByConversation($conversation); + $stats = $messageRepo->getConversationStats($conversation); + + $result[] = [ + 'id' => $conversation->getId(), + 'title' => $conversation->getTitle(), + 'category' => $conversation->getCategory(), + 'createdAt' => $jdate->jdate('Y/m/d H:i', $conversation->getCreatedAt()), + 'updatedAt' => $jdate->jdate('Y/m/d H:i', $conversation->getUpdatedAt()), + 'messageCount' => count($conversation->getMessages()), + 'lastMessage' => $lastMessage ? $lastMessage->getContent() : '', + 'stats' => $stats + ]; + } + + return $this->json($result); + } + + #[Route('/api/ai/conversations/create', name: 'api_ai_conversations_create', methods: ['POST'])] + public function createConversation( + Request $request, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('join'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $params = $request->getPayload()->all(); + $title = $params['title'] ?? 'گفتگوی جدید'; + $category = $params['category'] ?? 'عمومی'; + + $conversation = new AIConversation(); + $conversation->setUser($acc['user']); + $conversation->setBusiness($acc['bid']); + $conversation->setTitle($title); + $conversation->setCategory($category); + + $entityManager->persist($conversation); + $entityManager->flush(); + + return $this->json([ + 'id' => $conversation->getId(), + 'title' => $conversation->getTitle(), + 'category' => $conversation->getCategory(), + 'createdAt' => $conversation->getCreatedAt() + ]); + } + + #[Route('/api/ai/conversations/{id}/messages', name: 'api_ai_conversations_messages', methods: ['POST'])] + public function getConversationMessages( + int $id, + Access $access, + EntityManagerInterface $entityManager, + Jdate $jdate + ): JsonResponse { + $acc = $access->hasRole('join'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $conversation = $entityManager->getRepository(AIConversation::class)->find($id); + if (!$conversation) { + throw $this->createNotFoundException('گفتگو یافت نشد'); + } + + // بررسی دسترسی کاربر به این گفتگو + if ($conversation->getUser()->getId() !== $acc['user']->getId() || + $conversation->getBusiness()->getId() !== $acc['bid']->getId()) { + throw $this->createAccessDeniedException(); + } + + $messageRepo = $entityManager->getRepository(AIMessage::class); + $messages = $messageRepo->findByConversation($conversation); + + $result = []; + foreach ($messages as $message) { + $result[] = [ + 'id' => $message->getId(), + 'role' => $message->getRole(), + 'content' => $message->getContent(), + 'createdAt' => $jdate->jdate('Y/m/d H:i', $message->getCreatedAt()), + 'inputTokens' => $message->getInputTokens(), + 'outputTokens' => $message->getOutputTokens(), + 'inputCost' => $message->getInputCost(), + 'outputCost' => $message->getOutputCost(), + 'totalCost' => $message->getTotalCost(), + 'model' => $message->getModel(), + 'agentSource' => $message->getAgentSource() + ]; + } + + return $this->json($result); + } + + #[Route('/api/ai/conversations/{id}/update', name: 'api_ai_conversations_update', methods: ['POST'])] + public function updateConversation( + int $id, + Request $request, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('join'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $conversation = $entityManager->getRepository(AIConversation::class)->find($id); + if (!$conversation) { + throw $this->createNotFoundException('گفتگو یافت نشد'); + } + + // بررسی دسترسی کاربر به این گفتگو + if ($conversation->getUser()->getId() !== $acc['user']->getId() || + $conversation->getBusiness()->getId() !== $acc['bid']->getId()) { + throw $this->createAccessDeniedException(); + } + + $params = $request->getPayload()->all(); + + if (isset($params['title'])) { + $conversation->setTitle($params['title']); + } + + if (isset($params['category'])) { + $conversation->setCategory($params['category']); + } + + $conversation->setUpdatedAt(time()); + $entityManager->persist($conversation); + $entityManager->flush(); + + return $this->json(['success' => true]); + } + + #[Route('/api/ai/conversations/{id}/delete', name: 'api_ai_conversations_delete', methods: ['POST'])] + public function deleteConversation( + int $id, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('join'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $conversation = $entityManager->getRepository(AIConversation::class)->find($id); + if (!$conversation) { + throw $this->createNotFoundException('گفتگو یافت نشد'); + } + + // بررسی دسترسی کاربر به این گفتگو + if ($conversation->getUser()->getId() !== $acc['user']->getId() || + $conversation->getBusiness()->getId() !== $acc['bid']->getId()) { + throw $this->createAccessDeniedException(); + } + + // بررسی اینکه آیا گفتگو قبلاً حذف نشده است + if ($conversation->isDeleted()) { + return $this->json([ + 'success' => false, + 'error' => 'این گفتگو قبلاً حذف شده است' + ]); + } + + // حذف نرم گفتگو (تنها علامت‌گذاری به عنوان حذف شده) + $conversationRepo = $entityManager->getRepository(AIConversation::class); + $conversationRepo->softDelete($conversation, true); + + return $this->json(['success' => true]); + } + + #[Route('/api/ai/conversations/categories', name: 'api_ai_conversations_categories', methods: ['POST'])] + public function getCategories( + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('join'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $conversations = $entityManager->getRepository(AIConversation::class) + ->findActiveConversations($acc['user'], $acc['bid']); + + $categories = []; + foreach ($conversations as $conversation) { + $category = $conversation->getCategory(); + if ($category && !in_array($category, $categories)) { + $categories[] = $category; + } + } + + return $this->json($categories); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/AdminController.php b/hesabixCore/src/Controller/AdminController.php index 705f21b1..070bc01d 100644 --- a/hesabixCore/src/Controller/AdminController.php +++ b/hesabixCore/src/Controller/AdminController.php @@ -430,7 +430,7 @@ class AdminController extends AbstractController if (array_key_exists('rejectChequeInput', $params['plugAccpro'])) $registryMGR->update('sms', 'plugAccproRejectChequeInput', $params['plugAccpro']['rejectChequeInput']); } - + return $this->json(JsonResp::success()); } @@ -450,6 +450,26 @@ class AdminController extends AbstractController $resp['parsianGatewayAPI'] = $registryMGR->get('system', key: 'parsianGatewayAPI'); $resp['paypingKey'] = $registryMGR->get('system', key: 'paypingKey'); $resp['bitpayKey'] = $registryMGR->get('system', key: 'bitpayKey'); + $resp['inquiryPanel'] = $registryMGR->get('system', key: 'inquiryPanel'); + $resp['inquiryZohalAPIKey'] = $registryMGR->get('system', key: 'inquiryZohalAPIKey'); + $resp['enablePostalCodeToAddress'] = $registryMGR->get('system', key: 'enablePostalCodeToAddress'); + $resp['inquiryPanelEnable'] = $registryMGR->get('system', key: 'inquiryPanelEnable'); + $resp['postalCodeToAddressFee'] = $registryMGR->get('system', key: 'postalCodeToAddressFee'); + $resp['enableCardToSheba'] = $registryMGR->get('system', key: 'enableCardToSheba'); + $resp['cardToShebaFee'] = $registryMGR->get('system', key: 'cardToShebaFee'); + $resp['enableAccountToSheba'] = $registryMGR->get('system', key: 'enableAccountToSheba'); + $resp['accountToShebaFee'] = $registryMGR->get('system', key: 'accountToShebaFee'); + + // تنظیمات جادوگر هوش مصنوعی + $resp['aiEnabled'] = $registryMGR->get('system', key: 'aiEnabled'); + $resp['aiAgentSource'] = $registryMGR->get('system', key: 'aiAgentSource'); + $resp['aiModel'] = $registryMGR->get('system', key: 'aiModel'); + $resp['aiApiKey'] = $registryMGR->get('system', key: 'aiApiKey'); + $resp['localModelAddress'] = $registryMGR->get('system', key: 'localModelAddress'); + $resp['inputTokenPrice'] = $registryMGR->get('system', key: 'inputTokenPrice'); + $resp['outputTokenPrice'] = $registryMGR->get('system', key: 'outputTokenPrice'); + $resp['aiPrompt'] = $registryMGR->get('system', key: 'aiPrompt'); + return $this->json($resp); } @@ -474,6 +494,34 @@ class AdminController extends AbstractController $registryMGR->update('system', 'parsianGatewayAPI', $params['parsianGatewayAPI']); $registryMGR->update('system', 'paypingKey', $params['paypingKey']); $registryMGR->update('system', 'bitpayKey', $params['bitpayKey']); + $registryMGR->update('system', 'inquiryPanel', $params['inquiryPanel']); + $registryMGR->update('system', 'inquiryZohalAPIKey', $params['inquiryZohalAPIKey']); + $registryMGR->update('system', 'enablePostalCodeToAddress', $params['enablePostalCodeToAddress']); + $registryMGR->update('system', 'inquiryPanelEnable', $params['inquiryPanelEnable']); + $registryMGR->update('system', 'postalCodeToAddressFee', $params['postalCodeToAddressFee']); + $registryMGR->update('system', 'enableCardToSheba', $params['enableCardToSheba']); + $registryMGR->update('system', 'cardToShebaFee', $params['cardToShebaFee']); + $registryMGR->update('system', 'enableAccountToSheba', $params['enableAccountToSheba']); + $registryMGR->update('system', 'accountToShebaFee', $params['accountToShebaFee']); + + // ذخیره تنظیمات جادوگر هوش مصنوعی + if (array_key_exists('aiEnabled', $params)) + $registryMGR->update('system', 'aiEnabled', $params['aiEnabled']); + if (array_key_exists('aiAgentSource', $params)) + $registryMGR->update('system', 'aiAgentSource', $params['aiAgentSource']); + if (array_key_exists('aiModel', $params)) + $registryMGR->update('system', 'aiModel', $params['aiModel']); + if (array_key_exists('aiApiKey', $params)) + $registryMGR->update('system', 'aiApiKey', $params['aiApiKey']); + if (array_key_exists('localModelAddress', $params)) + $registryMGR->update('system', 'localModelAddress', $params['localModelAddress']); + if (array_key_exists('inputTokenPrice', $params)) + $registryMGR->update('system', 'inputTokenPrice', $params['inputTokenPrice']); + if (array_key_exists('outputTokenPrice', $params)) + $registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice']); + if (array_key_exists('aiPrompt', $params)) + $registryMGR->update('system', 'aiPrompt', $params['aiPrompt']); + $entityManager->persist($item); $entityManager->flush(); return $this->json(['result' => 1]); @@ -592,6 +640,7 @@ class AdminController extends AbstractController $temp['cardPan'] = $item->getCardPan(); $temp['refID'] = $item->getRefID(); $temp['shaba'] = $item->getShaba(); + $temp['amount'] = $item->getAmount(); $temp['dateSubmit'] = $jdate->jdate('Y/n/d H:i', $item->getDateSubmit()); $temp['gatePay'] = $item->getGatePay(); $resp[] = $temp; diff --git a/hesabixCore/src/Controller/BusinessController.php b/hesabixCore/src/Controller/BusinessController.php index 76b63ee9..01ff2689 100644 --- a/hesabixCore/src/Controller/BusinessController.php +++ b/hesabixCore/src/Controller/BusinessController.php @@ -545,6 +545,8 @@ class BusinessController extends AbstractController 'plugHrmDocs' => true, 'plugGhestaManager' => true, 'plugTaxSettings' => true, + 'inquiry' => true, + 'ai' => true, ]; } elseif ($perm) { $result = [ @@ -589,6 +591,8 @@ class BusinessController extends AbstractController 'plugHrmDocs' => $perm->isPlugHrmDocs(), 'plugGhestaManager' => $perm->isPlugGhestaManager(), 'plugTaxSettings' => $perm->isPlugTaxSettings(), + 'inquiry' => $perm->isInquiry(), + 'ai' => $perm->isAi(), ]; } return $this->json($result); @@ -658,6 +662,9 @@ class BusinessController extends AbstractController $perm->setPlugRepservice($params['plugRepservice']); $perm->setPlugHrmDocs($params['plugHrmDocs']); $perm->setPlugGhestaManager($params['plugGhestaManager']); + $perm->setPlugTaxSettings($params['plugTaxSettings']); + $perm->setInquiry($params['inquiry']); + $perm->setAi($params['ai']); $entityManager->persist($perm); $entityManager->flush(); $log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business); diff --git a/hesabixCore/src/Controller/CommodityController.php b/hesabixCore/src/Controller/CommodityController.php index c216f9a2..3415fdfe 100644 --- a/hesabixCore/src/Controller/CommodityController.php +++ b/hesabixCore/src/Controller/CommodityController.php @@ -143,7 +143,8 @@ class CommodityController extends AbstractController $count += $row->getCommdityCount(); } else { $count -= $row->getCommdityCount(); - } } + } + } $temp['count'] = $count; } return $temp; @@ -1104,17 +1105,41 @@ class CommodityController extends AbstractController if ($content = $request->getContent()) { $params = json_decode($content, true); } - if (!array_key_exists('upper', $params) || !array_key_exists('text', $params)) + + if (!array_key_exists('text', $params)) return $this->json(['result' => -1]); - + if ($this->isDefaultCategoryName($params['text'])) { return $this->json([ - 'result' => 4, + 'result' => 4, 'message' => 'این نام برای دسته‌بندی مجاز نیست', 'errorCode' => 'DEFAULT_CATEGORY_NAME' ]); } - + if (!array_key_exists('upper', $params)) { + $upper = $entityManager->getRepository(CommodityCat::class)->findOneBy([ + 'upper' => null, + 'bid' => $acc['bid'] + ]); + if (!$upper) { + $upper = new CommodityCat(); + $upper->setBid($acc['bid']); + $upper->setUpper(null); + $upper->setName('دسته بندی ها'); + $upper->setRoot(true); + $entityManager->persist($upper); + $entityManager->flush(); + } + $cat = new CommodityCat(); + $cat->setBid($acc['bid']); + $cat->setRoot(false); + $cat->setName($params['text']); + $cat->setUpper($upper->getId()); + $entityManager->persist($cat); + $entityManager->flush(); + return $this->json(['result' => 1, 'id' => $cat->getId()]); + } + $upper = $entityManager->getRepository(CommodityCat::class)->find($params['upper']); if ($upper) { if ($upper->getBid() == $acc['bid']) { @@ -1142,22 +1167,22 @@ class CommodityController extends AbstractController } if (!array_key_exists('id', $params) || !array_key_exists('text', $params)) return $this->json(['result' => -1]); - + if ($this->isDefaultCategoryName($params['text'])) { return $this->json([ - 'result' => 4, + 'result' => 4, 'message' => 'این نام برای دسته‌بندی مجاز نیست', 'errorCode' => 'DEFAULT_CATEGORY_NAME' ]); } - + $node = $entityManager->getRepository(CommodityCat::class)->find($params['id']); if ($node) { if ($node->getBid() == $acc['bid']) { // بررسی دسته‌بندی پیش‌فرض if ($this->isDefaultCategoryName($node->getName())) { return $this->json([ - 'result' => 4, + 'result' => 4, 'message' => 'ویرایش دسته‌بندی پیش‌فرض مجاز نیست', 'errorCode' => 'DEFAULT_CATEGORY_EDIT' ]); @@ -1627,7 +1652,7 @@ class CommodityController extends AbstractController // بررسی دسته‌بندی پیش‌فرض if ($this->isDefaultCategoryName($category->getName())) { return $this->json([ - 'Success' => false, + 'Success' => false, 'message' => 'حذف دسته‌بندی پیش‌فرض مجاز نیست', 'errorCode' => 'DEFAULT_CATEGORY_DELETE' ], 400); diff --git a/hesabixCore/src/Controller/PersonsController.php b/hesabixCore/src/Controller/PersonsController.php index 633de983..5f699835 100644 --- a/hesabixCore/src/Controller/PersonsController.php +++ b/hesabixCore/src/Controller/PersonsController.php @@ -18,6 +18,7 @@ use App\Entity\Storeroom; use App\Entity\StoreroomItem; use App\Entity\StoreroomTicket; use App\Service\Explore; +use App\Cog\PersonService; use Doctrine\ORM\EntityManagerInterface; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; @@ -61,40 +62,13 @@ class PersonsController extends AbstractController * @throws \ReflectionException */ #[Route('/api/person/info/{code}', name: 'app_persons_info')] - public function app_persons_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse + public function app_persons_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PersonService $personService): JsonResponse { $acc = $access->hasRole('person'); if (!$acc) throw $this->createAccessDeniedException(); - $person = $entityManager->getRepository(Person::class)->findOneBy([ - 'bid' => $acc['bid'], - 'code' => $code - ]); - $types = $entityManager->getRepository(PersonType::class)->findAll(); - $response = Explore::ExplorePerson($person, $types); - $rows = $entityManager->getRepository(HesabdariRow::class)->findBy([ - 'person' => $person, - 'bid' => $acc['bid'] - ]); - $bs = 0; - $bd = 0; - foreach ($rows as $row) { - try { - $doc = $row->getDoc(); - if ($doc && $doc->getMoney() && $doc->getYear() && - $doc->getMoney()->getId() == $acc['money']->getId() && - $doc->getYear()->getId() == $acc['year']->getId()) { - $bs += (float) $row->getBs(); // بستانکار - $bd += (float) $row->getBd(); // بدهکار - } - } catch (\Exception $e) { - // در صورت بروز خطا، این ردیف را نادیده می‌گیریم و به ردیف بعدی می‌رویم - continue; - } - } - $response['bs'] = $bs; - $response['bd'] = $bd; - $response['balance'] = $bs - $bd; + + $response = $personService->getPersonInfo($code, $acc); return $this->json($response); } diff --git a/hesabixCore/src/Controller/Plugins/inquiry/PlugInquiryMainController.php b/hesabixCore/src/Controller/Plugins/inquiry/PlugInquiryMainController.php new file mode 100644 index 00000000..2e6eccea --- /dev/null +++ b/hesabixCore/src/Controller/Plugins/inquiry/PlugInquiryMainController.php @@ -0,0 +1,327 @@ +entityManager = $entityManager; + } + + #[Route('/api/plugins/inquiry/settings/get', name: 'plugin_inquiry_settings_get', methods: ['GET'])] + public function plugin_inquiry_settings_get(registryMGR $registryMGR): JsonResponse + { + $resp['inquiryPanel'] = $registryMGR->get('system', key: 'inquiryPanel'); + $resp['enablePostalCodeToAddress'] = $registryMGR->get('system', key: 'enablePostalCodeToAddress'); + $resp['inquiryPanelEnable'] = $registryMGR->get('system', key: 'inquiryPanelEnable'); + $resp['postalCodeToAddressFee'] = $registryMGR->get('system', key: 'postalCodeToAddressFee'); + $resp['enableCardToSheba'] = $registryMGR->get('system', key: 'enableCardToSheba'); + $resp['cardToShebaFee'] = $registryMGR->get('system', key: 'cardToShebaFee'); + $resp['enableAccountToSheba'] = $registryMGR->get('system', key: 'enableAccountToSheba'); + $resp['accountToShebaFee'] = $registryMGR->get('system', key: 'accountToShebaFee'); + return $this->json($resp); + } + + #[Route('/api/plugins/inquiry/postalcode-to-address', name: 'plugin_inquiry_postalcode_to_address', methods: ['POST'])] + public function plugin_inquiry_postalcode_to_address(Inquiry $inquiry, Access $access, Request $request, registryMGR $registryMGR, Log $log): JsonResponse + { + $acc = $access->hasRole('inquiry'); + if (!$acc) { + return $this->json([ + 'success' => false, + 'message' => 'شما دسترسی به این سرویس را ندارید' + ]); + } + + // دریافت کد پستی از درخواست + $data = json_decode($request->getContent(), true); + $postalCode = $data['postal_code'] ?? null; + if (!$postalCode) { + return $this->json([ + 'success' => false, + 'message' => 'کد پستی ارسال نشده است' + ]); + } + + // فراخوانی سرویس استعلام کد پستی + $result = $inquiry->postalCodeToAddress($postalCode); + + // بررسی نتیجه و بازگرداندن پاسخ مناسب + if (isset($result['result']) && $result['result'] == 1) { + $isFromCache = isset($result['response_body']['message']) && + strpos($result['response_body']['message'], 'از کش') !== false; + + // ثبت لاگ بر اساس منبع داده + $logMessage = $isFromCache + ? "استعلام کد پستی {$postalCode} از کش (بدون کسر کارمزد)" + : "استعلام کد پستی {$postalCode} از API (کسر کارمزد: " . $registryMGR->get('system', key: 'postalCodeToAddressFee') . " ریال)"; + + $log->insert( + 'استعلام', + $logMessage, + $acc['user'], + $acc['bid'] + ); + + // فقط در صورت عدم وجود در کش، کارمزد کسر شود + if (!$isFromCache) { + if ($acc['bid']->getSmsCharge() < $registryMGR->get('system', key: 'postalCodeToAddressFee')) { + // ثبت لاگ عدم موجودی کافی + $log->insert( + 'استعلام', + "عدم موجودی کافی برای استعلام کد پستی {$postalCode}", + $acc['user'], + $acc['bid'] + ); + + return $this->json([ + 'success' => false, + 'message' => 'موجودی شما برای این سرویس کافی نیست' + ]); + } + + $business = $acc['bid']; + $business->setSmsCharge($business->getSmsCharge() - $registryMGR->get('system', key: 'postalCodeToAddressFee')); + $this->entityManager->persist($business); + $this->entityManager->flush(); + } + + return $this->json([ + 'success' => true, + 'data' => $result['response_body']['data']['address'] ?? null, + 'message' => $result['response_body']['message'] ?? 'موفق', + 'from_cache' => $isFromCache + ]); + } else { + // ثبت لاگ خطا + $errorMessage = $result['message'] ?? 'خطا در استعلام کد پستی'; + $log->insert( + 'استعلام', + "خطا در استعلام کد پستی {$postalCode}: {$errorMessage}", + $acc['user'], + $acc['bid'] + ); + + return $this->json([ + 'success' => false, + 'message' => $errorMessage, + 'error_code' => $result['error_code'] ?? null + ]); + } + } + + #[Route('/api/plugins/inquiry/card-to-sheba', name: 'plugin_inquiry_card_to_sheba', methods: ['POST'])] + public function plugin_inquiry_card_to_sheba(Inquiry $inquiry, Access $access, Request $request, registryMGR $registryMGR, Log $log): JsonResponse + { + $acc = $access->hasRole('inquiry'); + if (!$acc) { + return $this->json([ + 'success' => false, + 'message' => 'شما دسترسی به این سرویس را ندارید' + ]); + } + + // بررسی فعال بودن سرویس + if (!$registryMGR->get('system', key: 'enableCardToSheba')) { + return $this->json([ + 'success' => false, + 'message' => 'این سرویس در حال حاضر غیرفعال است' + ]); + } + + // دریافت شماره کارت از درخواست + $data = json_decode($request->getContent(), true); + $cardNumber = $data['card_number'] ?? null; + if (!$cardNumber) { + return $this->json([ + 'success' => false, + 'message' => 'شماره کارت ارسال نشده است' + ]); + } + + // فراخوانی سرویس استعلام کارت به شبا + $result = $inquiry->cardToSheba($cardNumber); + + // بررسی نتیجه و بازگرداندن پاسخ مناسب + if (isset($result['result']) && $result['result'] == 1) { + $isFromCache = isset($result['response_body']['message']) && + strpos($result['response_body']['message'], 'از حافظه موقت') !== false; + + // ثبت لاگ بر اساس منبع داده + $logMessage = $isFromCache + ? "استعلام کارت به شبا {$cardNumber} از حافظه موقت (بدون کسر کارمزد)" + : "استعلام کارت به شبا {$cardNumber} از API (کسر کارمزد: " . $registryMGR->get('system', key: 'cardToShebaFee') . " ریال)"; + + $log->insert( + 'استعلام', + $logMessage, + $acc['user'], + $acc['bid'] + ); + + // فقط در صورت عدم وجود در کش، کارمزد کسر شود + if (!$isFromCache) { + if ($acc['bid']->getSmsCharge() < $registryMGR->get('system', key: 'cardToShebaFee')) { + // ثبت لاگ عدم موجودی کافی + $log->insert( + 'استعلام', + "عدم موجودی کافی برای استعلام کارت به شبا {$cardNumber}", + $acc['user'], + $acc['bid'] + ); + + return $this->json([ + 'success' => false, + 'message' => 'موجودی شما برای این سرویس کافی نیست' + ]); + } + + $business = $acc['bid']; + $business->setSmsCharge($business->getSmsCharge() - $registryMGR->get('system', key: 'cardToShebaFee')); + $this->entityManager->persist($business); + $this->entityManager->flush(); + } + + return $this->json([ + 'success' => true, + 'data' => $result['response_body']['data'] ?? null, + 'message' => $result['response_body']['message'] ?? 'موفق', + 'from_cache' => $isFromCache + ]); + } else { + // ثبت لاگ خطا + $errorMessage = $result['message'] ?? 'خطا در استعلام کارت به شبا'; + $log->insert( + 'استعلام', + "خطا در استعلام کارت به شبا {$cardNumber}: {$errorMessage}", + $acc['user'], + $acc['bid'] + ); + + return $this->json([ + 'success' => false, + 'message' => $errorMessage, + 'error_code' => $result['error_code'] ?? null + ]); + } + } + + #[Route('/api/plugins/inquiry/account-to-sheba', name: 'plugin_inquiry_account_to_sheba', methods: ['POST'])] + public function plugin_inquiry_account_to_sheba(Inquiry $inquiry, Access $access, Request $request, registryMGR $registryMGR, Log $log): JsonResponse + { + $acc = $access->hasRole('inquiry'); + if (!$acc) { + return $this->json([ + 'success' => false, + 'message' => 'شما دسترسی به این سرویس را ندارید' + ]); + } + + // بررسی فعال بودن سرویس + if (!$registryMGR->get('system', key: 'enableAccountToSheba')) { + return $this->json([ + 'success' => false, + 'message' => 'این سرویس در حال حاضر غیرفعال است' + ]); + } + + // دریافت داده‌ها از درخواست + $data = json_decode($request->getContent(), true); + $bankCode = $data['bank_code'] ?? null; + $accountNumber = $data['account_number'] ?? null; + + if (!$bankCode || !$accountNumber) { + return $this->json([ + 'success' => false, + 'message' => 'کد بانک و شماره حساب الزامی است' + ]); + } + + // فراخوانی سرویس استعلام حساب به شبا + $result = $inquiry->accountToSheba($bankCode, $accountNumber); + + // بررسی نتیجه و بازگرداندن پاسخ مناسب + if (isset($result['result']) && $result['result'] == 1) { + $isFromCache = isset($result['response_body']['message']) && + strpos($result['response_body']['message'], 'از حافظه موقت') !== false; + + // ثبت لاگ بر اساس منبع داده + $logMessage = $isFromCache + ? "استعلام حساب به شبا {$bankCode}/{$accountNumber} از حافظه موقت (بدون کسر کارمزد)" + : "استعلام حساب به شبا {$bankCode}/{$accountNumber} از API (کسر کارمزد: " . $registryMGR->get('system', key: 'accountToShebaFee') . " ریال)"; + + $log->insert( + 'استعلام', + $logMessage, + $acc['user'], + $acc['bid'] + ); + + // فقط در صورت عدم وجود در کش، کارمزد کسر شود + if (!$isFromCache) { + if ($acc['bid']->getSmsCharge() < $registryMGR->get('system', key: 'accountToShebaFee')) { + // ثبت لاگ عدم موجودی کافی + $log->insert( + 'استعلام', + "عدم موجودی کافی برای استعلام حساب به شبا {$bankCode}/{$accountNumber}", + $acc['user'], + $acc['bid'] + ); + + return $this->json([ + 'success' => false, + 'message' => 'موجودی شما برای این سرویس کافی نیست' + ]); + } + + $business = $acc['bid']; + $business->setSmsCharge($business->getSmsCharge() - $registryMGR->get('system', key: 'accountToShebaFee')); + $this->entityManager->persist($business); + $this->entityManager->flush(); + } + + return $this->json([ + 'success' => true, + 'data' => $result['response_body']['data'] ?? null, + 'message' => $result['response_body']['message'] ?? 'موفق', + 'from_cache' => $isFromCache + ]); + } else { + // ثبت لاگ خطا + $errorMessage = $result['message'] ?? 'خطا در استعلام حساب به شبا'; + $log->insert( + 'استعلام', + "خطا در استعلام حساب به شبا {$bankCode}/{$accountNumber}: {$errorMessage}", + $acc['user'], + $acc['bid'] + ); + + return $this->json([ + 'success' => false, + 'message' => $errorMessage, + 'error_code' => $result['error_code'] ?? null + ]); + } + } +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/System/UpdateCoreController.php b/hesabixCore/src/Controller/System/UpdateCoreController.php index 33e8ddaa..73987384 100644 --- a/hesabixCore/src/Controller/System/UpdateCoreController.php +++ b/hesabixCore/src/Controller/System/UpdateCoreController.php @@ -23,8 +23,9 @@ final class UpdateCoreController extends AbstractController public function api_admin_updatecore_run(): JsonResponse { $projectDir = $this->getParameter('kernel.project_dir'); + $gitRoot = dirname($projectDir); // رفتن به ریشه پروژه $uuid = uniqid(); - $stateFile = $projectDir . '/../hesabixBackup/update_state_' . $uuid . '.json'; + $stateFile = $gitRoot . '/hesabixBackup/update_state_' . $uuid . '.json'; if (!file_exists(dirname($stateFile))) { mkdir(dirname($stateFile), 0755, true); @@ -41,7 +42,7 @@ final class UpdateCoreController extends AbstractController 'COMPOSER_HOME' => '/var/www/.composer', ]); - $process = new Process(['php', 'bin/console', 'hesabix:update', $stateFile], $projectDir, $env); + $process = new Process(['php', 'hesabixCore/bin/console', 'hesabix:update', $stateFile], $gitRoot, $env); $process->setTimeout(7200); // افزایش تایم‌اوت به 2 ساعت $process->start(function ($type, $buffer) use ($stateFile) { $state = json_decode(file_get_contents($stateFile), true) ?? ['uuid' => uniqid(), 'log' => '']; @@ -70,7 +71,9 @@ final class UpdateCoreController extends AbstractController ], 400); } - $stateFile = $this->getParameter('kernel.project_dir') . '/../hesabixBackup/update_state_' . $uuid . '.json'; + $projectDir = $this->getParameter('kernel.project_dir'); + $gitRoot = dirname($projectDir); + $stateFile = $gitRoot . '/hesabixBackup/update_state_' . $uuid . '.json'; if (!file_exists($stateFile)) { return new JsonResponse([ @@ -97,7 +100,7 @@ final class UpdateCoreController extends AbstractController } if (!$isRunning) { - $backupDir = $this->getParameter('kernel.project_dir') . '/../hesabixBackup'; + $backupDir = $gitRoot . '/hesabixBackup'; $stateFiles = glob($backupDir . '/update_state_*.json'); foreach ($stateFiles as $file) { if (is_file($file)) { @@ -128,7 +131,9 @@ final class UpdateCoreController extends AbstractController return new JsonResponse(['status' => 'error', 'message' => 'UUID is required'], 400); } - $stateFile = $this->getParameter('kernel.project_dir') . '/../hesabixBackup/update_state_' . $uuid . '.json'; + $projectDir = $this->getParameter('kernel.project_dir'); + $gitRoot = dirname($projectDir); + $stateFile = $gitRoot . '/hesabixBackup/update_state_' . $uuid . '.json'; return new StreamedResponse(function () use ($stateFile) { header('Content-Type: text/event-stream'); @@ -167,13 +172,14 @@ final class UpdateCoreController extends AbstractController public function api_admin_updatecore_commits(): JsonResponse { $projectDir = $this->getParameter('kernel.project_dir'); + $gitRoot = dirname($projectDir); // رفتن به ریشه پروژه - $currentProcess = new Process(['git', 'rev-parse', 'HEAD'], $projectDir); + $currentProcess = new Process(['git', 'rev-parse', 'HEAD'], $gitRoot); $currentProcess->setTimeout(7200); // افزایش تایم‌اوت $currentProcess->run(); $currentCommit = $currentProcess->isSuccessful() ? trim($currentProcess->getOutput()) : 'unknown'; - $targetProcess = new Process(['git', 'ls-remote', 'origin', 'HEAD'], $projectDir); + $targetProcess = new Process(['git', 'ls-remote', 'origin', 'HEAD'], $gitRoot); $targetProcess->setTimeout(7200); // افزایش تایم‌اوت $targetProcess->run(); $targetOutput = $targetProcess->isSuccessful() ? explode("\t", trim($targetProcess->getOutput()))[0] : 'unknown'; @@ -428,4 +434,170 @@ final class UpdateCoreController extends AbstractController ], 500); } } + + #[Route('/api/admin/updatecore/current-source', name: 'api_admin_updatecore_current_source', methods: ['GET'])] + public function api_admin_updatecore_current_source(): JsonResponse + { + $projectDir = $this->getParameter('kernel.project_dir'); + $gitRoot = dirname($projectDir); // رفتن به ریشه پروژه + $output = ''; + + try { + // بررسی اینکه آیا پروژه یک مخزن Git است + if (!is_dir($gitRoot . '/.git')) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'این پروژه یک مخزن Git نیست', + 'sourceUrl' => '', + ], 400); + } + + // دریافت آدرس مخزن origin فعلی + $process = new Process(['git', 'remote', 'get-url', 'origin'], $gitRoot); + $process->setTimeout(7200); // افزایش تایم‌اوت + $process->run(); + + if (!$process->isSuccessful()) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'خطا در دریافت آدرس مخزن: ' . $process->getErrorOutput(), + 'sourceUrl' => '', + ], 500); + } + + $sourceUrl = trim($process->getOutput()); + $output .= "آدرس مخزن فعلی: $sourceUrl\n"; + + return new JsonResponse([ + 'status' => 'success', + 'sourceUrl' => $sourceUrl, + 'output' => $output, + ]); + + } catch (\Exception $e) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'خطا در بررسی مخزن: ' . $e->getMessage(), + 'sourceUrl' => '', + ], 500); + } + } + + #[Route('/api/admin/updatecore/change-source', name: 'api_admin_updatecore_change_source', methods: ['POST'])] + public function api_admin_updatecore_change_source(Request $request): JsonResponse + { + $sourceUrl = $request->getPayload()->get('sourceUrl'); + $output = ''; + + if (!$sourceUrl || !filter_var($sourceUrl, FILTER_VALIDATE_URL) && !preg_match('/^git@[^:]+:[^\/]+\/[^\/]+\.git$/', $sourceUrl)) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'آدرس مخزن نامعتبر است. لطفاً یک آدرس HTTP یا SSH معتبر وارد کنید.', + 'output' => $output, + ], 400); + } + + $projectDir = $this->getParameter('kernel.project_dir'); + $gitRoot = dirname($projectDir); // رفتن به ریشه پروژه + + try { + // بررسی اینکه آیا پروژه یک مخزن Git است + if (!is_dir($gitRoot . '/.git')) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'این پروژه یک مخزن Git نیست', + 'output' => $output, + ], 400); + } + + $output .= "شروع تغییر آدرس مخزن...\n"; + + // دریافت آدرس مخزن فعلی + $currentProcess = new Process(['git', 'remote', 'get-url', 'origin'], $gitRoot); + $currentProcess->setTimeout(7200); + $currentProcess->run(); + $currentUrl = $currentProcess->isSuccessful() ? trim($currentProcess->getOutput()) : ''; + + if ($currentUrl) { + $output .= "آدرس مخزن فعلی: $currentUrl\n"; + } + + // تغییر آدرس مخزن origin + $changeProcess = new Process(['git', 'remote', 'set-url', 'origin', $sourceUrl], $gitRoot); + $changeProcess->setTimeout(7200); + $changeProcess->run(); + + if (!$changeProcess->isSuccessful()) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'خطا در تغییر آدرس مخزن: ' . $changeProcess->getErrorOutput(), + 'output' => $output, + ], 500); + } + + $output .= "آدرس مخزن به $sourceUrl تغییر یافت\n"; + + // بررسی اتصال به مخزن جدید + $testProcess = new Process(['git', 'remote', 'show', 'origin'], $gitRoot); + $testProcess->setTimeout(7200); + $testProcess->run(); + + if (!$testProcess->isSuccessful()) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'خطا در اتصال به مخزن جدید: ' . $testProcess->getErrorOutput(), + 'output' => $output, + ], 500); + } + + $output .= "اتصال به مخزن جدید با موفقیت برقرار شد\n"; + + // دریافت اطلاعات مخزن جدید + $fetchProcess = new Process(['git', 'fetch', 'origin'], $gitRoot); + $fetchProcess->setTimeout(7200); + $fetchProcess->run(); + + if (!$fetchProcess->isSuccessful()) { + $output .= "هشدار: خطا در دریافت اطلاعات از مخزن جدید: " . $fetchProcess->getErrorOutput() . "\n"; + } else { + $output .= "اطلاعات مخزن جدید با موفقیت دریافت شد\n"; + } + + // بررسی branch های موجود + $branchProcess = new Process(['git', 'branch', '-r'], $gitRoot); + $branchProcess->setTimeout(7200); + $branchProcess->run(); + + if ($branchProcess->isSuccessful()) { + $branches = trim($branchProcess->getOutput()); + if ($branches) { + $output .= "شاخه‌های موجود در مخزن جدید:\n$branches\n"; + } else { + $output .= "هیچ شاخه‌ای در مخزن جدید یافت نشد\n"; + } + } + + // پاک کردن کش Git + $cleanProcess = new Process(['git', 'gc', '--prune=now'], $gitRoot); + $cleanProcess->setTimeout(7200); + $cleanProcess->run(); + + if ($cleanProcess->isSuccessful()) { + $output .= "کش Git پاک شد\n"; + } + + return new JsonResponse([ + 'status' => 'success', + 'message' => 'آدرس مخزن با موفقیت تغییر یافت و اتصال برقرار شد', + 'output' => $output, + ]); + + } catch (\Exception $e) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'خطا در تغییر آدرس مخزن: ' . $e->getMessage(), + 'output' => $output, + ], 500); + } + } } \ No newline at end of file diff --git a/hesabixCore/src/Controller/wizardController.php b/hesabixCore/src/Controller/wizardController.php new file mode 100644 index 00000000..1cd2ab8e --- /dev/null +++ b/hesabixCore/src/Controller/wizardController.php @@ -0,0 +1,203 @@ +agiService = $agiService; + } + + #[Route('/api/wizard/talk', name: 'wizard_talk', methods: ['POST'])] + public function wizard_talk( + Request $request, + Access $access, + EntityManagerInterface $entityManager, + Log $log + ): JsonResponse + { + try { + $acc = $access->hasRole('join'); + if (!$acc) { + return $this->json([ + 'success' => false, + 'error' => 'دسترسی غیرمجاز', + 'debug_info' => [ + 'access' => $acc + ] + ]); + } + + // بررسی دسترسی هوش مصنوعی + if (!$acc['ai']) { + return $this->json([ + 'success' => false, + 'error' => 'شما دسترسی استفاده از هوش مصنوعی را ندارید', + 'debug_info' => [ + 'ai_access' => $acc['ai'] + ] + ]); + } + + $params = json_decode($request->getContent(), true) ?? []; + + if (!isset($params['message']) || empty($params['message'])) { + return $this->json([ + 'success' => false, + 'error' => 'پیام الزامی است', + 'debug_info' => [ + 'params' => $params + ] + ]); + } + + $message = $params['message']; + $options = $params['options'] ?? []; + $conversationId = $params['conversationId'] ?? null; + + // بررسی فعال بودن هوش مصنوعی + $aiStatus = $this->agiService->checkAIServiceStatus(); + if (!$aiStatus['isEnabled']) { + return $this->json([ + 'success' => false, + 'error' => 'سرویس هوش مصنوعی غیرفعال است', + 'debug_info' => [ + 'ai_status' => $aiStatus + ] + ]); + } + + // بررسی اعتبار کسب و کار + $business = $acc['bid']; + $currentBalance = (float) ($business->getSmsCharge() ?? 0); + $estimatedCost = 100; + if ($currentBalance < $estimatedCost) { + return $this->json([ + 'success' => false, + 'error' => "اعتبار شما کافی نیست (اعتبار فعلی: {$currentBalance} ریال). برای شارژ حساب خود به بخش شارژ مراجعه کنید.", + 'balance' => $currentBalance, + 'required' => $estimatedCost, + 'showChargeButton' => true, + 'debug_info' => [ + 'balance' => $currentBalance, + 'required' => $estimatedCost + ] + ]); + } + + // استفاده از AGIService برای مدیریت گفتگو و ارسال درخواست + $result = $this->agiService->sendRequest($message, $business, $acc['user'], $conversationId, $acc); + + if ($result['success']) { + $responseContent = $result['response'] ?? $result['message'] ?? 'عملیات با موفقیت انجام شد'; + + $response = [ + 'success' => true, + 'response' => $responseContent, + 'conversationId' => $result['conversation_id'] ?? null, + 'model' => $result['model'] ?? null, + 'usage' => $result['usage'] ?? null, + 'cost' => $result['cost'] ?? null, + 'debug_info' => $result['debug_info'] ?? null + ]; + + // محاسبه هزینه در صورت وجود اطلاعات usage + if (isset($result['cost'])) { + $cost = $result['cost']; + + // کسر اعتبار از کسب و کار + $totalCost = $cost['total_cost'] ?? 0; + if ($totalCost > 0) { + // گرد کردن هزینه به عدد صحیح + $totalCost = (int) round($totalCost); + $newBalance = (int) ($currentBalance - $totalCost); + $business->setSmsCharge((string) $newBalance); + $entityManager->persist($business); + + // ثبت لاگ کسر اعتبار + $log->insert( + 'هوش مصنوعی', + "کسر اعتبار برای استفاده از هوش مصنوعی: {$totalCost} ریال (اعتبار قبلی: {$currentBalance} ریال، اعتبار جدید: {$newBalance} ریال)", + $acc['user'], + $business + ); + + $entityManager->flush(); + } + } + + return $this->json($response); + } else { + return $this->json([ + 'success' => false, + 'error' => $result['error'] ?? 'خطای نامشخص در سرویس هوش مصنوعی', + 'debug_info' => $result['debug_info'] ?? ['fallback' => 'no debug info from service', 'result' => $result] + ]); + } + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'error' => 'خطا در پردازش درخواست: ' . $e->getMessage(), + 'debug_info' => [ + 'ai_response' => null, + 'tool_commands' => [], + 'has_commands' => false, + 'error_details' => $e->getMessage() + ] + ]); + } + } + + #[Route('/api/wizard/status', name: 'wizard_status', methods: ['GET'])] + public function wizard_status(): JsonResponse + { + try { + $serviceStatus = $this->agiService->checkAIServiceStatus(); + + // بررسی وضعیت کامل + $status = 'available'; + $message = 'سرویس هوش مصنوعی در دسترس است'; + + if (!$serviceStatus['isEnabled']) { + $status = 'disabled'; + $message = 'سرویس هوش مصنوعی غیرفعال است'; + } elseif (!$serviceStatus['hasApiKey']) { + $status = 'no_api_key'; + $message = 'کلید API تنظیم نشده است'; + } + + return $this->json([ + 'success' => true, + 'status' => $status, + 'service_name' => $serviceStatus['service'], + 'message' => $message, + 'connection_status' => $serviceStatus['connection_status'] ?? 'unknown', + 'connection_message' => $serviceStatus['connection_message'] ?? '' + ]); + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'error' => 'خطا در بررسی وضعیت: ' . $e->getMessage() + ]); + } + } + + +} diff --git a/hesabixCore/src/Entity/AIConversation.php b/hesabixCore/src/Entity/AIConversation.php new file mode 100644 index 00000000..d9502219 --- /dev/null +++ b/hesabixCore/src/Entity/AIConversation.php @@ -0,0 +1,174 @@ + "utf8mb4", "collation" => "utf8mb4_unicode_ci"])] + private ?string $title = null; + + #[ORM\Column(length: 255, nullable: true, options: ["charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci"])] + private ?string $category = null; + + #[ORM\Column] + private ?int $createdAt = null; + + #[ORM\Column] + private ?int $updatedAt = null; + + #[ORM\Column(nullable: true)] + private ?bool $isActive = true; + + #[ORM\Column(nullable: true)] + private ?bool $deleted = false; + + #[ORM\OneToMany(mappedBy: 'conversation', targetEntity: AIMessage::class, orphanRemoval: true)] + private Collection $messages; + + public function __construct() + { + $this->messages = new ArrayCollection(); + $this->createdAt = time(); + $this->updatedAt = time(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): static + { + $this->user = $user; + return $this; + } + + public function getBusiness(): ?Business + { + return $this->business; + } + + public function setBusiness(?Business $business): static + { + $this->business = $business; + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + return $this; + } + + public function getCategory(): ?string + { + return $this->category; + } + + public function setCategory(?string $category): static + { + $this->category = $category; + return $this; + } + + public function getCreatedAt(): ?int + { + return $this->createdAt; + } + + public function setCreatedAt(int $createdAt): static + { + $this->createdAt = $createdAt; + return $this; + } + + public function getUpdatedAt(): ?int + { + return $this->updatedAt; + } + + public function setUpdatedAt(int $updatedAt): static + { + $this->updatedAt = $updatedAt; + return $this; + } + + public function isActive(): ?bool + { + return $this->isActive; + } + + public function setIsActive(?bool $isActive): static + { + $this->isActive = $isActive; + return $this; + } + + public function isDeleted(): ?bool + { + return $this->deleted; + } + + public function setDeleted(?bool $deleted): static + { + $this->deleted = $deleted; + return $this; + } + + /** + * @return Collection + */ + public function getMessages(): Collection + { + return $this->messages; + } + + public function addMessage(AIMessage $message): static + { + if (!$this->messages->contains($message)) { + $this->messages->add($message); + $message->setConversation($this); + } + return $this; + } + + public function removeMessage(AIMessage $message): static + { + if ($this->messages->removeElement($message)) { + // set the owning side to null (unless already changed) + if ($message->getConversation() === $this) { + $message->setConversation(null); + } + } + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/AIMessage.php b/hesabixCore/src/Entity/AIMessage.php new file mode 100644 index 00000000..055e5cf1 --- /dev/null +++ b/hesabixCore/src/Entity/AIMessage.php @@ -0,0 +1,183 @@ + "utf8mb4", "collation" => "utf8mb4_unicode_ci"])] + private ?string $content = null; + + #[ORM\Column] + private ?int $createdAt = null; + + #[ORM\Column(nullable: true)] + private ?int $inputTokens = null; + + #[ORM\Column(nullable: true)] + private ?int $outputTokens = null; + + #[ORM\Column(nullable: true)] + private ?float $inputCost = null; + + #[ORM\Column(nullable: true)] + private ?float $outputCost = null; + + #[ORM\Column(nullable: true)] + private ?float $totalCost = null; + + #[ORM\Column(length: 255, nullable: true, options: ["charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci"])] + private ?string $model = null; + + #[ORM\Column(length: 255, nullable: true, options: ["charset" => "utf8mb4", "collation" => "utf8mb4_unicode_ci"])] + private ?string $agentSource = null; + + public function __construct() + { + $this->createdAt = time(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getConversation(): ?AIConversation + { + return $this->conversation; + } + + public function setConversation(?AIConversation $conversation): static + { + $this->conversation = $conversation; + return $this; + } + + public function getRole(): ?string + { + return $this->role; + } + + public function setRole(string $role): static + { + $this->role = $role; + return $this; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(string $content): static + { + $this->content = $content; + return $this; + } + + public function getCreatedAt(): ?int + { + return $this->createdAt; + } + + public function setCreatedAt(int $createdAt): static + { + $this->createdAt = $createdAt; + return $this; + } + + public function getInputTokens(): ?int + { + return $this->inputTokens; + } + + public function setInputTokens(?int $inputTokens): static + { + $this->inputTokens = $inputTokens; + return $this; + } + + public function getOutputTokens(): ?int + { + return $this->outputTokens; + } + + public function setOutputTokens(?int $outputTokens): static + { + $this->outputTokens = $outputTokens; + return $this; + } + + public function getInputCost(): ?float + { + return $this->inputCost; + } + + public function setInputCost(?float $inputCost): static + { + $this->inputCost = $inputCost; + return $this; + } + + public function getOutputCost(): ?float + { + return $this->outputCost; + } + + public function setOutputCost(?float $outputCost): static + { + $this->outputCost = $outputCost; + return $this; + } + + public function getTotalCost(): ?float + { + return $this->totalCost; + } + + public function setTotalCost(?float $totalCost): static + { + $this->totalCost = $totalCost; + return $this; + } + + public function getModel(): ?string + { + return $this->model; + } + + public function setModel(?string $model): static + { + $this->model = $model; + return $this; + } + + public function getAgentSource(): ?string + { + return $this->agentSource; + } + + public function setAgentSource(?string $agentSource): static + { + $this->agentSource = $agentSource; + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/APIToken.php b/hesabixCore/src/Entity/APIToken.php index 27f2d1e8..7c005080 100644 --- a/hesabixCore/src/Entity/APIToken.php +++ b/hesabixCore/src/Entity/APIToken.php @@ -26,6 +26,9 @@ class APIToken #[ORM\JoinColumn(nullable: false)] private ?User $submitter = null; + #[ORM\Column(type: 'boolean', nullable: true)] + private ?bool $isForAi = null; + public function getId(): ?int { return $this->id; @@ -78,4 +81,16 @@ class APIToken return $this; } + + public function isForAi(): ?bool + { + return $this->isForAi; + } + + public function setIsForAi(?bool $isForAi): static + { + $this->isForAi = $isForAi; + + return $this; + } } diff --git a/hesabixCore/src/Entity/AccountToShebaInquiry.php b/hesabixCore/src/Entity/AccountToShebaInquiry.php new file mode 100644 index 00000000..1e9667ac --- /dev/null +++ b/hesabixCore/src/Entity/AccountToShebaInquiry.php @@ -0,0 +1,63 @@ +id; + } + + public function getCacheKey(): ?string + { + return $this->cacheKey; + } + + public function setCacheKey(string $cacheKey): static + { + $this->cacheKey = $cacheKey; + return $this; + } + + public function getShebaData(): array + { + return $this->shebaData; + } + + public function setShebaData(array $shebaData): static + { + $this->shebaData = $shebaData; + return $this; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + public function setUpdatedAt(\DateTimeImmutable $updatedAt): static + { + $this->updatedAt = $updatedAt; + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/Business.php b/hesabixCore/src/Entity/Business.php index b91d8085..85952313 100644 --- a/hesabixCore/src/Entity/Business.php +++ b/hesabixCore/src/Entity/Business.php @@ -300,6 +300,9 @@ class Business #[ORM\OneToMany(mappedBy: 'business', targetEntity: PlugHrmDoc::class)] private Collection $plugHrmDocs; + #[ORM\OneToMany(mappedBy: 'business', targetEntity: AIConversation::class, orphanRemoval: true)] + private Collection $aiConversations; + public function __construct() { $this->logs = new ArrayCollection(); @@ -343,6 +346,7 @@ class Business $this->accountingPackageOrders = new ArrayCollection(); $this->PlugGhestaDocs = new ArrayCollection(); $this->plugHrmDocs = new ArrayCollection(); + $this->aiConversations = new ArrayCollection(); } public function getId(): ?int @@ -2086,4 +2090,34 @@ class Business } return $this; } + + /** + * @return Collection + */ + public function getAiConversations(): Collection + { + return $this->aiConversations; + } + + public function addAiConversation(AIConversation $aiConversation): static + { + if (!$this->aiConversations->contains($aiConversation)) { + $this->aiConversations->add($aiConversation); + $aiConversation->setBusiness($this); + } + + return $this; + } + + public function removeAiConversation(AIConversation $aiConversation): static + { + if ($this->aiConversations->removeElement($aiConversation)) { + // set the owning side to null (unless already changed) + if ($aiConversation->getBusiness() === $this) { + $aiConversation->setBusiness(null); + } + } + + return $this; + } } diff --git a/hesabixCore/src/Entity/CardToShebaInquiry.php b/hesabixCore/src/Entity/CardToShebaInquiry.php new file mode 100644 index 00000000..6c271407 --- /dev/null +++ b/hesabixCore/src/Entity/CardToShebaInquiry.php @@ -0,0 +1,63 @@ +id; + } + + public function getCardNumber(): ?string + { + return $this->cardNumber; + } + + public function setCardNumber(string $cardNumber): static + { + $this->cardNumber = $cardNumber; + return $this; + } + + public function getShebaData(): array + { + return $this->shebaData; + } + + public function setShebaData(array $shebaData): static + { + $this->shebaData = $shebaData; + return $this; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + public function setUpdatedAt(\DateTimeImmutable $updatedAt): static + { + $this->updatedAt = $updatedAt; + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/InvoiceType.php b/hesabixCore/src/Entity/InvoiceType.php index 0e553508..6734bade 100644 --- a/hesabixCore/src/Entity/InvoiceType.php +++ b/hesabixCore/src/Entity/InvoiceType.php @@ -6,6 +6,7 @@ use App\Repository\InvoiceTypeRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Ignore; #[ORM\Entity(repositoryClass: InvoiceTypeRepository::class)] class InvoiceType @@ -28,12 +29,14 @@ class InvoiceType private ?string $type = null; #[ORM\OneToMany(mappedBy: 'InvoiceLabel', targetEntity: HesabdariDoc::class)] + #[Ignore] private Collection $hesabdariDocs; /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'invoiceLabel', targetEntity: PreInvoiceDoc::class)] + #[Ignore] private Collection $preInvoiceDocs; public function __construct() diff --git a/hesabixCore/src/Entity/Permission.php b/hesabixCore/src/Entity/Permission.php index 47aedd1b..59550ab2 100644 --- a/hesabixCore/src/Entity/Permission.php +++ b/hesabixCore/src/Entity/Permission.php @@ -132,6 +132,12 @@ class Permission #[ORM\Column(nullable: true)] private ?bool $plugTaxSettings = null; + #[ORM\Column(nullable: true)] + private ?bool $inquiry = null; + + #[ORM\Column(nullable: true)] + private ?bool $ai = null; + public function getId(): ?int { return $this->id; @@ -605,4 +611,27 @@ class Permission return $this; } + public function isInquiry(): ?bool + { + return $this->inquiry; + } + + public function setInquiry(?bool $inquiry): static + { + $this->inquiry = $inquiry; + + return $this; + } + + public function isAi(): ?bool + { + return $this->ai; + } + + public function setAi(?bool $ai): static + { + $this->ai = $ai; + + return $this; + } } diff --git a/hesabixCore/src/Entity/PlugGhestaDoc.php b/hesabixCore/src/Entity/PlugGhestaDoc.php index fa27a762..5101e1b6 100644 --- a/hesabixCore/src/Entity/PlugGhestaDoc.php +++ b/hesabixCore/src/Entity/PlugGhestaDoc.php @@ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Ignore; #[ORM\Entity(repositoryClass: PlugGhestaDocRepository::class)] class PlugGhestaDoc @@ -18,9 +19,11 @@ class PlugGhestaDoc #[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?Business $bid = null; #[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')] + #[Ignore] private ?User $submitter = null; #[ORM\Column(length: 25)] @@ -43,16 +46,19 @@ class PlugGhestaDoc #[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?Person $person = null; /** * @var Collection */ #[ORM\OneToMany(targetEntity: PlugGhestaItem::class, mappedBy: 'doc', orphanRemoval: true)] + #[Ignore] private Collection $plugGhestaItems; #[ORM\ManyToOne(inversedBy: 'plugGhestaDocs')] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?HesabdariDoc $mainDoc = null; public function __construct() diff --git a/hesabixCore/src/Entity/PlugGhestaItem.php b/hesabixCore/src/Entity/PlugGhestaItem.php index 38ce692e..01bca933 100644 --- a/hesabixCore/src/Entity/PlugGhestaItem.php +++ b/hesabixCore/src/Entity/PlugGhestaItem.php @@ -4,6 +4,7 @@ namespace App\Entity; use App\Repository\PlugGhestaItemRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Ignore; #[ORM\Entity(repositoryClass: PlugGhestaItemRepository::class)] class PlugGhestaItem @@ -15,6 +16,7 @@ class PlugGhestaItem #[ORM\ManyToOne(inversedBy: 'plugGhestaItems')] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?PlugGhestaDoc $doc = null; #[ORM\Column(length: 25)] @@ -27,6 +29,7 @@ class PlugGhestaItem private ?int $num = null; #[ORM\ManyToOne(inversedBy: 'plugGhestaItems')] + #[Ignore] private ?HesabdariDoc $hesabdariDoc = null; public function getId(): ?int diff --git a/hesabixCore/src/Entity/PlugHrmDoc.php b/hesabixCore/src/Entity/PlugHrmDoc.php index 9a5a8f2c..5c51d679 100644 --- a/hesabixCore/src/Entity/PlugHrmDoc.php +++ b/hesabixCore/src/Entity/PlugHrmDoc.php @@ -6,6 +6,7 @@ use App\Repository\PlugHrmDocRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Ignore; #[ORM\Entity(repositoryClass: PlugHrmDocRepository::class)] class PlugHrmDoc @@ -23,19 +24,23 @@ class PlugHrmDoc #[ORM\ManyToOne(inversedBy: 'plugHrmDocs')] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?Business $business = null; #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?User $creator = null; #[ORM\Column] private ?int $createDate = null; #[ORM\OneToMany(mappedBy: 'doc', targetEntity: PlugHrmDocItem::class, orphanRemoval: true)] + #[Ignore] private Collection $items; #[ORM\ManyToOne(inversedBy: 'plugHrmDocs')] + #[Ignore] private ?HesabdariDoc $hesabdariDoc = null; public function __construct() diff --git a/hesabixCore/src/Entity/PlugHrmDocItem.php b/hesabixCore/src/Entity/PlugHrmDocItem.php index 1c156233..c6365dc9 100644 --- a/hesabixCore/src/Entity/PlugHrmDocItem.php +++ b/hesabixCore/src/Entity/PlugHrmDocItem.php @@ -4,6 +4,7 @@ namespace App\Entity; use App\Repository\PlugHrmDocItemRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Ignore; #[ORM\Entity(repositoryClass: PlugHrmDocItemRepository::class)] class PlugHrmDocItem @@ -15,10 +16,12 @@ class PlugHrmDocItem #[ORM\ManyToOne(inversedBy: 'items')] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?PlugHrmDoc $doc = null; #[ORM\ManyToOne] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?Person $person = null; #[ORM\Column] diff --git a/hesabixCore/src/Entity/PostalCodeInquiry.php b/hesabixCore/src/Entity/PostalCodeInquiry.php new file mode 100644 index 00000000..f4ab52d5 --- /dev/null +++ b/hesabixCore/src/Entity/PostalCodeInquiry.php @@ -0,0 +1,83 @@ +createdAt = new \DateTimeImmutable(); + $this->updatedAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getPostalCode(): ?string + { + return $this->postalCode; + } + + public function setPostalCode(string $postalCode): static + { + $this->postalCode = $postalCode; + return $this; + } + + public function getAddressData(): array + { + return $this->addressData; + } + + public function setAddressData(array $addressData): static + { + $this->addressData = $addressData; + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + return $this; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + public function setUpdatedAt(\DateTimeImmutable $updatedAt): static + { + $this->updatedAt = $updatedAt; + return $this; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/PreInvoiceItem.php b/hesabixCore/src/Entity/PreInvoiceItem.php index 3d982b62..1549cd8e 100644 --- a/hesabixCore/src/Entity/PreInvoiceItem.php +++ b/hesabixCore/src/Entity/PreInvoiceItem.php @@ -4,6 +4,7 @@ namespace App\Entity; use App\Repository\PreInvoiceItemRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Ignore; #[ORM\Entity(repositoryClass: PreInvoiceItemRepository::class)] class PreInvoiceItem @@ -15,6 +16,7 @@ class PreInvoiceItem #[ORM\ManyToOne(inversedBy: 'preInvoiceItems')] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?Commodity $commodity = null; #[ORM\Column(length: 100, nullable: true)] @@ -46,6 +48,7 @@ class PreInvoiceItem #[ORM\ManyToOne(inversedBy: 'preInvoiceItems')] #[ORM\JoinColumn(nullable: false)] + #[Ignore] private ?PreInvoiceDoc $doc = null; public function getId(): ?int diff --git a/hesabixCore/src/Entity/Registry.php b/hesabixCore/src/Entity/Registry.php index 7d8db42d..c9d5a822 100644 --- a/hesabixCore/src/Entity/Registry.php +++ b/hesabixCore/src/Entity/Registry.php @@ -19,7 +19,7 @@ class Registry #[ORM\Column(length: 255)] private ?string $name = null; - #[ORM\Column(length: 255)] + #[ORM\Column(type: "text", nullable: true)] private ?string $valueOfKey = null; public function getId(): ?int diff --git a/hesabixCore/src/Entity/User.php b/hesabixCore/src/Entity/User.php index ccc7f5d3..630f9ea9 100644 --- a/hesabixCore/src/Entity/User.php +++ b/hesabixCore/src/Entity/User.php @@ -137,6 +137,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'submitter')] private Collection $PlugGhestaDocs; + #[ORM\OneToMany(mappedBy: 'user', targetEntity: AIConversation::class, orphanRemoval: true)] + private Collection $aiConversations; + public function __construct() { $this->userTokens = new ArrayCollection(); @@ -162,6 +165,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $this->accountingPackageOrders = new ArrayCollection(); $this->backBuiltModules = new ArrayCollection(); $this->PlugGhestaDocs = new ArrayCollection(); + $this->aiConversations = new ArrayCollection(); } public function getId(): ?int @@ -997,4 +1001,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection + */ + public function getAiConversations(): Collection + { + return $this->aiConversations; + } + + public function addAiConversation(AIConversation $aiConversation): static + { + if (!$this->aiConversations->contains($aiConversation)) { + $this->aiConversations->add($aiConversation); + $aiConversation->setUser($this); + } + + return $this; + } + + public function removeAiConversation(AIConversation $aiConversation): static + { + if ($this->aiConversations->removeElement($aiConversation)) { + // set the owning side to null (unless already changed) + if ($aiConversation->getUser() === $this) { + $aiConversation->setUser(null); + } + } + + return $this; + } } diff --git a/hesabixCore/src/Repository/AIConversationRepository.php b/hesabixCore/src/Repository/AIConversationRepository.php new file mode 100644 index 00000000..f322bb41 --- /dev/null +++ b/hesabixCore/src/Repository/AIConversationRepository.php @@ -0,0 +1,122 @@ + + * + * @method AIConversation|null find($id, $lockMode = null, $lockVersion = null) + * @method AIConversation|null findOneBy(array $criteria, array $orderBy = null) + * @method AIConversation[] findAll() + * @method AIConversation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class AIConversationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AIConversation::class); + } + + public function save(AIConversation $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(AIConversation $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * حذف نرم گفتگو (تنها علامت‌گذاری به عنوان حذف شده) + */ + public function softDelete(AIConversation $entity, bool $flush = false): void + { + $entity->setDeleted(true); + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * دریافت گفتگوهای کاربر در یک کسب و کار خاص + */ + public function findByUserAndBusiness(User $user, Business $business, bool $activeOnly = true): array + { + $criteria = [ + 'user' => $user, + 'business' => $business, + 'deleted' => false + ]; + + if ($activeOnly) { + $criteria['isActive'] = true; + } + + return $this->findBy($criteria, ['updatedAt' => 'DESC']); + } + + /** + * دریافت گفتگوهای فعال کاربر در کسب و کار فعلی + */ + public function findActiveConversations(User $user, Business $business): array + { + return $this->findBy([ + 'user' => $user, + 'business' => $business, + 'isActive' => true, + 'deleted' => false + ], ['updatedAt' => 'DESC']); + } + + /** + * جستجو در گفتگوها بر اساس عنوان + */ + public function searchByTitle(User $user, Business $business, string $searchTerm): array + { + $qb = $this->createQueryBuilder('c') + ->where('c.user = :user') + ->andWhere('c.business = :business') + ->andWhere('c.isActive = :active') + ->andWhere('c.deleted = :deleted') + ->andWhere('c.title LIKE :search') + ->setParameter('user', $user) + ->setParameter('business', $business) + ->setParameter('active', true) + ->setParameter('deleted', false) + ->setParameter('search', '%' . $searchTerm . '%') + ->orderBy('c.updatedAt', 'DESC'); + + return $qb->getQuery()->getResult(); + } + + /** + * دریافت گفتگوها بر اساس دسته‌بندی + */ + public function findByCategory(User $user, Business $business, string $category): array + { + return $this->findBy([ + 'user' => $user, + 'business' => $business, + 'category' => $category, + 'isActive' => true, + 'deleted' => false + ], ['updatedAt' => 'DESC']); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/AIMessageRepository.php b/hesabixCore/src/Repository/AIMessageRepository.php new file mode 100644 index 00000000..277dcbb0 --- /dev/null +++ b/hesabixCore/src/Repository/AIMessageRepository.php @@ -0,0 +1,88 @@ + + * + * @method AIMessage|null find($id, $lockMode = null, $lockVersion = null) + * @method AIMessage|null findOneBy(array $criteria, array $orderBy = null) + * @method AIMessage[] findAll() + * @method AIMessage[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class AIMessageRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AIMessage::class); + } + + public function save(AIMessage $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(AIMessage $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * دریافت پیام‌های یک گفتگو + */ + public function findByConversation(AIConversation $conversation): array + { + return $this->findBy(['conversation' => $conversation], ['createdAt' => 'ASC']); + } + + /** + * دریافت آخرین پیام یک گفتگو + */ + public function findLastMessageByConversation(AIConversation $conversation): ?AIMessage + { + return $this->findOneBy(['conversation' => $conversation], ['createdAt' => 'DESC']); + } + + /** + * محاسبه آمار استفاده برای یک گفتگو + */ + public function getConversationStats(AIConversation $conversation): array + { + $qb = $this->createQueryBuilder('m') + ->select('SUM(m.inputTokens) as totalInputTokens') + ->addSelect('SUM(m.outputTokens) as totalOutputTokens') + ->addSelect('SUM(m.inputCost) as totalInputCost') + ->addSelect('SUM(m.outputCost) as totalOutputCost') + ->addSelect('SUM(m.totalCost) as totalCost') + ->where('m.conversation = :conversation') + ->setParameter('conversation', $conversation); + + $result = $qb->getQuery()->getSingleResult(); + + $totalInputTokens = $result['totalInputTokens'] ?? 0; + $totalOutputTokens = $result['totalOutputTokens'] ?? 0; + $totalTokens = $totalInputTokens + $totalOutputTokens; + + return [ + 'totalInputTokens' => $totalInputTokens, + 'totalOutputTokens' => $totalOutputTokens, + 'totalTokens' => $totalTokens, + 'totalInputCost' => $result['totalInputCost'] ?? 0, + 'totalOutputCost' => $result['totalOutputCost'] ?? 0, + 'totalCost' => $result['totalCost'] ?? 0, + ]; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/AccountToShebaInquiryRepository.php b/hesabixCore/src/Repository/AccountToShebaInquiryRepository.php new file mode 100644 index 00000000..45280bf4 --- /dev/null +++ b/hesabixCore/src/Repository/AccountToShebaInquiryRepository.php @@ -0,0 +1,46 @@ + + * + * @method AccountToShebaInquiry|null find($id, $lockMode = null, $lockVersion = null) + * @method AccountToShebaInquiry|null findOneBy(array $criteria, array $orderBy = null) + * @method AccountToShebaInquiry[] findAll() + * @method AccountToShebaInquiry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class AccountToShebaInquiryRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AccountToShebaInquiry::class); + } + + public function findByCacheKey(string $cacheKey): ?AccountToShebaInquiry + { + return $this->findOneBy(['cacheKey' => $cacheKey]); + } + + public function save(AccountToShebaInquiry $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(AccountToShebaInquiry $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/CardToShebaInquiryRepository.php b/hesabixCore/src/Repository/CardToShebaInquiryRepository.php new file mode 100644 index 00000000..6a2dd388 --- /dev/null +++ b/hesabixCore/src/Repository/CardToShebaInquiryRepository.php @@ -0,0 +1,46 @@ + + * + * @method CardToShebaInquiry|null find($id, $lockMode = null, $lockVersion = null) + * @method CardToShebaInquiry|null findOneBy(array $criteria, array $orderBy = null) + * @method CardToShebaInquiry[] findAll() + * @method CardToShebaInquiry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class CardToShebaInquiryRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, CardToShebaInquiry::class); + } + + public function findByCardNumber(string $cardNumber): ?CardToShebaInquiry + { + return $this->findOneBy(['cardNumber' => $cardNumber]); + } + + public function save(CardToShebaInquiry $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(CardToShebaInquiry $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } +} \ No newline at end of file diff --git a/hesabixCore/src/Repository/PostalCodeInquiryRepository.php b/hesabixCore/src/Repository/PostalCodeInquiryRepository.php new file mode 100644 index 00000000..bf550641 --- /dev/null +++ b/hesabixCore/src/Repository/PostalCodeInquiryRepository.php @@ -0,0 +1,46 @@ + + * + * @method PostalCodeInquiry|null find($id, $lockMode = null, $lockVersion = null) + * @method PostalCodeInquiry|null findOneBy(array $criteria, array $orderBy = null) + * @method PostalCodeInquiry[] findAll() + * @method PostalCodeInquiry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PostalCodeInquiryRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, PostalCodeInquiry::class); + } + + public function findByPostalCode(string $postalCode): ?PostalCodeInquiry + { + return $this->findOneBy(['postalCode' => $postalCode]); + } + + public function save(PostalCodeInquiry $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(PostalCodeInquiry $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/AGIService.php b/hesabixCore/src/Service/AGI/AGIService.php new file mode 100644 index 00000000..b8a8408b --- /dev/null +++ b/hesabixCore/src/Service/AGI/AGIService.php @@ -0,0 +1,838 @@ +em = $entityManager; + $this->registryMGR = $registryMGR; + $this->log = $log; + $this->provider = $provider; + $this->promptService = $promptService; + $this->httpClient = $httpClient; + $this->httpKernel = $httpKernel; + } + + /** + * ارسال درخواست به هوش مصنوعی + * @param string $message پیام کاربر + * @param Business|null $business کسب و کار + * @param mixed $user کاربر + * @param int|null $conversationId شناسه گفتگو (اختیاری) + * @param array|null $acc اطلاعات دسترسی + * @return array + */ + public function sendRequest(string $message, ?Business $business = null, $user = null, ?int $conversationId = null, ?array $acc = null): array + { + // بررسی فعال بودن هوش مصنوعی + $status = $this->checkAIServiceStatus(); + if (!$status['enabled']) { + return [ + 'success' => false, + 'error' => 'سرویس هوش مصنوعی غیرفعال است.', + 'debug_info' => [ + 'context' => 'sendRequest', + 'service_status' => $status, + 'inputs' => [ + 'message' => $message, + 'business' => $business, + 'user' => $user, + 'conversationId' => $conversationId + ] + ] + ]; + } + + try { + // مدیریت گفتگو و تاریخچه + $conversation = $this->manageConversation($conversationId, $business, $user, $message); + $conversationHistory = $this->getConversationHistory($conversation); + + // ذخیره پیام کاربر + $this->saveUserMessage($conversation, $message); + + // ساخت پرامپ هوشمند + $prompt = $this->buildSmartPrompt($message, $business, $conversationHistory); + $service = $this->getAIAgentSource(); + $apiKey = $this->getAIApiKey($service); + + if (!$apiKey) { + return [ + 'success' => false, + 'error' => 'کلید API برای سرویس هوش مصنوعی تنظیم نشده است.', + 'debug_info' => [ + 'context' => 'sendRequest', + 'service' => $service, + 'inputs' => [ + 'message' => $message, + 'business' => $business, + 'user' => $user, + 'conversationId' => $conversationId + ] + ] + ]; + } + + // ارسال درخواست با function calling + $result = $this->sendToAIServiceWithFunctionCalling($prompt, $apiKey, $service, $conversationHistory, $acc); + + if (!$result['success']) { + return $result; + } + + // استخراج پاسخ نهایی + $aiResponse = $this->extractAIResponse($result['data']); + $cost = $this->calculateCostFromResponse($result['data']); + + // ذخیره پاسخ هوش مصنوعی + $this->saveAIMessage($conversation, $aiResponse, $result['data'], $cost); + + return [ + 'success' => true, + 'response' => $aiResponse, + 'conversationId' => $conversation->getId(), + 'model' => $this->getAIModel(), + 'usage' => $result['data']['usage'] ?? [], + 'cost' => $cost, + 'debug_info' => [ + 'context' => 'sendRequest', + 'function_calls' => $result['function_calls'] ?? [], + 'tool_results' => $result['tool_results'] ?? [] + ] + ]; + + } catch (\Exception $e) { + $this->log->error('خطا در ارسال درخواست به هوش مصنوعی: ' . $e->getMessage(), [ + 'context' => 'AGIService::sendRequest', + 'message' => $message, + 'business' => $business, + 'user' => $user, + 'conversationId' => $conversationId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'error' => 'خطا در پردازش درخواست: ' . $e->getMessage(), + 'debug_info' => [ + 'context' => 'sendRequest', + 'exception' => $e->getMessage(), + 'inputs' => [ + 'message' => $message, + 'business' => $business, + 'user' => $user, + 'conversationId' => $conversationId + ] + ] + ]; + } + } + + /** + * ارسال درخواست به سرویس هوش مصنوعی با پشتیبانی از function calling + */ + private function sendToAIServiceWithFunctionCalling(string $prompt, string $apiKey, string $service, array $conversationHistory = [], ?array $acc = null): array + { + $urls = $this->getServiceUrls($service); + $model = $this->getAIModel(); + + // ساخت messages با تاریخچه + $messages = []; + + // اضافه کردن تاریخچه گفتگو + foreach ($conversationHistory as $historyItem) { + $messages[] = [ + 'role' => $historyItem['role'], + 'content' => $historyItem['content'] + ]; + } + + // اضافه کردن پیام فعلی + $messages[] = [ + 'role' => 'user', + 'content' => $prompt + ]; + + // تعریف ابزارهای موجود + $tools = $this->buildToolsFromPromptServices(); + + $data = [ + 'model' => $model, + 'messages' => $messages, + 'tools' => $tools, + 'tool_choice' => 'auto', // اجازه انتخاب ابزار به مدل + 'max_tokens' => 12000, + 'temperature' => 0.1 + ]; + + $maxIterations = 5; // حداکثر تعداد تکرار برای جلوگیری از حلقه بی‌نهایت + $iteration = 0; + $functionCalls = []; + $toolResults = []; + + while ($iteration < $maxIterations) { + $iteration++; + + // تلاش برای ارسال به URL های مختلف + $result = null; + foreach ($urls as $url) { + $result = $this->makeHttpRequest($url, $data, $apiKey); + if ($result['success']) { + break; + } + } + + if (!$result || !$result['success']) { + return [ + 'success' => false, + 'error' => 'خطا در ارتباط با سرور هوش مصنوعی.', + 'debug_info' => [ + 'context' => 'sendToAIServiceWithFunctionCalling', + 'url_list' => $urls, + 'data' => $data, + 'iteration' => $iteration + ] + ]; + } + + $responseData = $result['data']; + $choices = $responseData['choices'] ?? []; + + if (empty($choices)) { + return [ + 'success' => false, + 'error' => 'پاسخ نامعتبر از سرور هوش مصنوعی.', + 'debug_info' => [ + 'context' => 'sendToAIServiceWithFunctionCalling', + 'response_data' => $responseData, + 'iteration' => $iteration + ] + ]; + } + + $choice = $choices[0]; + $message = $choice['message'] ?? []; + $toolCalls = $message['tool_calls'] ?? []; + + // اگر ابزاری فراخوانی نشده، پاسخ نهایی است + if (empty($toolCalls)) { + return [ + 'success' => true, + 'data' => $responseData, + 'function_calls' => $functionCalls, + 'tool_results' => $toolResults + ]; + } + + // اجرای ابزارهای فراخوانی شده + $toolResults = []; + foreach ($toolCalls as $toolCall) { + $functionCall = $toolCall['function'] ?? []; + $functionName = $functionCall['name'] ?? ''; + $functionArgs = json_decode($functionCall['arguments'] ?? '{}', true); + if ($acc && !isset($functionArgs['acc'])) { + $functionArgs['acc'] = $acc; + } + $functionCalls[] = [ + 'name' => $functionName, + 'arguments' => $functionArgs + ]; + + // اجرای ابزار + $toolResult = $this->callTool($functionName, $functionArgs); + $toolResults[] = [ + 'tool_call_id' => $toolCall['id'] ?? '', + 'role' => 'tool', + 'content' => json_encode($toolResult, JSON_UNESCAPED_UNICODE) + ]; + } + + // اضافه کردن نتایج ابزارها به messages برای ارسال مجدد + $messages[] = [ + 'role' => 'assistant', + 'content' => null, + 'tool_calls' => $toolCalls + ]; + + foreach ($toolResults as $toolResult) { + $messages[] = $toolResult; + } + + // به‌روزرسانی data برای ارسال مجدد + $data['messages'] = $messages; + } + + // اگر به حداکثر تعداد تکرار رسیدیم + return [ + 'success' => false, + 'error' => 'تعداد تکرار بیش از حد مجاز.', + 'debug_info' => [ + 'context' => 'sendToAIServiceWithFunctionCalling', + 'max_iterations' => $maxIterations, + 'function_calls' => $functionCalls, + 'tool_results' => $toolResults + ] + ]; + } + + /** + * اجرای ابزار + */ + private function callTool(string $tool, array $params, array $context = []) + { + try { + switch ($tool) { + case 'getPersonInfo': + // استفاده مستقیم از سرویس Cog\PersonService + return $this->callGetPersonInfoFromCog($params); + default: + return [ + 'error' => 'ابزار ناشناخته: ' . $tool + ]; + } + } catch (\Exception $e) { + $this->log->error('خطا در اجرای ابزار: ' . $e->getMessage(), [ + 'context' => 'AGIService::callTool', + 'tool' => $tool, + 'params' => $params, + 'exception' => $e->getMessage() + ]); + + return [ + 'error' => 'خطا در اجرای ابزار: ' . $e->getMessage() + ]; + } + } + + /** + * اجرای ابزار getPersonInfo با استفاده از سرویس Cog\PersonService + */ + private function callGetPersonInfoFromCog(array $params) + { + $code = $params['code'] ?? null; + if (!$code) { + return [ + 'error' => 'کد شخص الزامی است' + ]; + } + + try { + // دریافت اطلاعات دسترسی (acc) از context یا پارامترها + $acc = $params['acc'] ?? null; + if (!$acc) { + return [ + 'error' => 'اطلاعات دسترسی (acc) الزامی است' + ]; + } + // استفاده از سرویس Cog\PersonService + $personService = new \App\Cog\PersonService($this->em, $this->provider->getAccessService()); + $result = $personService->getPersonInfo($code, $acc); + return $result; + } catch (\Exception $e) { + $this->log->error('خطا در دریافت اطلاعات شخص از Cog: ' . $e->getMessage(), [ + 'context' => 'AGIService::callGetPersonInfoFromCog', + 'code' => $code, + 'exception' => $e->getMessage() + ]); + return [ + 'error' => 'خطا در دریافت اطلاعات شخص: ' . $e->getMessage() + ]; + } + } + + /** + * ساخت پرامپ هوشمند + */ + private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string + { + // دریافت پرامپ‌های پایه از PromptService + $basePrompts = $this->promptService->getAllBasePrompts(); + $prompt = $basePrompts; + + // قوانین خروجی JSON و مثال‌ها از سرویس مدیریت پرامپت‌ها + $prompt .= $this->promptService->getOutputFormatPrompt(); + + // اضافه کردن اطلاعات کسب و کار + if ($business) { + $prompt .= "\n\nاطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}."; + } + // اضافه کردن تاریخچه گفتگو + if (!empty($conversationHistory)) { + $prompt .= "\n\n📜 تاریخچه گفتگو:\n"; + foreach ($conversationHistory as $historyItem) { + $role = $historyItem['role'] === 'user' ? 'کاربر' : 'دستیار'; + $prompt .= "{$role}: {$historyItem['content']}\n"; + } + $prompt .= "\n💡 نکته: لطفاً context گفتگو را حفظ کنید و به سوالات قبلی مراجعه کنید."; + } + $prompt .= "\n\nسوال کاربر: " . $message; + return $prompt; + } + + /** + * دریافت URL های سرویس + */ + private function getServiceUrls(string $service): array + { + return match ($service) { + 'gapgpt' => [ + 'https://api.gapgpt.ir/v1/chat/completions', + 'https://api.gapgpt.app/v1/chat/completions' + ], + 'avalai' => ['https://api.avalapis.ir/v1/chat/completions'], + 'local' => [$this->registryMGR->get('system', 'localModelAddress') ?? ''], + default => [] + }; + } + + /** + * انجام درخواست HTTP + */ + private function makeHttpRequest(string $url, array $data, string $apiKey): array + { + $debugInfo = [ + 'request_url' => $url, + 'request_data' => $data, + ]; + + try { + $response = $this->httpClient->request('POST', $url, [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $apiKey, + ], + 'json' => $data, + 'timeout' => 15, + 'max_redirects' => 3, + ]); + + $statusCode = $response->getStatusCode(); + $content = $response->getContent(false); // false: throw exception on 4xx/5xx نمی‌دهد + $debugInfo['http_code'] = $statusCode; + $debugInfo['raw_response'] = $content; + + if ($statusCode !== 200) { + $debugInfo['http_error_message'] = $this->getHttpErrorMessage($statusCode); + return [ + 'success' => false, + 'error' => 'خطای HTTP: ' . $this->getHttpErrorMessage($statusCode), + 'debug_info' => $debugInfo + ]; + } + + $responseData = json_decode($content, true); + + if (!$responseData) { + $debugInfo['json_error'] = json_last_error_msg(); + return [ + 'success' => false, + 'error' => 'پاسخ نامعتبر از سرور: ' . substr($content, 0, 200), + 'debug_info' => $debugInfo + ]; + } + + return [ + 'success' => true, + 'data' => $responseData, + 'debug_info' => $debugInfo + ]; + } catch (\Throwable $e) { + $debugInfo['exception'] = $e->getMessage(); + $debugInfo['exception_code'] = $e->getCode(); + $debugInfo['exception_trace'] = $e->getTraceAsString(); + return [ + 'success' => false, + 'error' => 'خطا در ارتباط با سرور: ' . $e->getMessage(), + 'debug_info' => $debugInfo + ]; + } + } + + /** + * دریافت پیام خطای HTTP + */ + private function getHttpErrorMessage(int $httpCode): string + { + $messages = [ + 400 => 'درخواست نامعتبر', + 401 => 'عدم احراز هویت', + 403 => 'دسترسی ممنوع', + 404 => 'منبع یافت نشد', + 429 => 'تعداد درخواست‌ها بیش از حد مجاز', + 500 => 'خطای داخلی سرور', + 502 => 'خطای دروازه', + 503 => 'سرویس در دسترس نیست', + 504 => 'خطای timeout دروازه' + ]; + + return $messages[$httpCode] ?? 'خطای نامشخص'; + } + + /** + * استخراج پاسخ از ساختارهای مختلف سرویس‌ها + */ + private function extractAIResponse(array $responseData): ?string + { + if (isset($responseData['choices'][0]['message']['content'])) { + return $responseData['choices'][0]['message']['content']; + } + + if (isset($responseData['response'])) { + return $responseData['response']; + } + + if (isset($responseData['content'])) { + return $responseData['content']; + } + + return null; + } + + /** + * محاسبه هزینه استفاده + */ + private function calculateCostFromResponse(array $responseData): array + { + $usage = $responseData['usage'] ?? []; + return $this->calculateCost($usage); + } + + /** + * محاسبه هزینه بر اساس usage + */ + public function calculateCost(array $usage): array + { + $promptTokens = $usage['prompt_tokens'] ?? 0; + $completionTokens = $usage['completion_tokens'] ?? 0; + + $inputCost = $promptTokens * $this->getInputTokenPrice(); + $outputCost = $completionTokens * $this->getOutputTokenPrice(); + + return [ + 'input_cost' => round($inputCost, 4), + 'output_cost' => round($outputCost, 4), + 'total_cost' => round($inputCost + $outputCost, 4) + ]; + } + + /** + * بررسی وضعیت سرویس هوش مصنوعی + */ + public function checkAIServiceStatus(): array + { + $enabled = $this->registryMGR->get('system', 'aiEnabled') ?? false; + $service = $this->registryMGR->get('system', 'aiAgentSource') ?? 'gapgpt'; + $apiKey = $this->getAIApiKey($service); + + return [ + 'isEnabled' => $enabled && $apiKey, + 'enabled' => $enabled && $apiKey, + 'service' => $this->getServiceDisplayName($service), + 'hasApiKey' => !empty($apiKey), + 'message' => $enabled && $apiKey ? 'سرویس فعال است' : 'سرویس غیرفعال است' + ]; + } + + /** + * دریافت نام نمایشی سرویس + */ + public function getServiceDisplayName(string $service): string + { + $displayNames = [ + 'gapgpt' => 'گپ‌جی‌پی‌تی', + 'avalai' => 'اول‌آی', + 'local' => 'سرویس محلی' + ]; + + return $displayNames[$service] ?? 'سرویس نامشخص'; + } + + /** + * دریافت کلید API برای سرویس مشخص + */ + private function getAIApiKey(string $service): ?string + { + return match ($service) { + 'gapgpt', 'avalai' => $this->registryMGR->get('system', 'aiApiKey') ?? null, + 'local' => $this->registryMGR->get('system', 'localModelAddress') ?? null, + default => null + }; + } + + /** + * دریافت مدل هوش مصنوعی + */ + public function getAIModel(): string + { + return $this->registryMGR->get('system', 'aiModel') ?? 'gpt-4o-2024-11-20'; + } + + /** + * دریافت منبع عامل هوش مصنوعی + */ + public function getAIAgentSource(): string + { + return $this->registryMGR->get('system', 'aiAgentSource') ?? 'gapgpt'; + } + + /** + * دریافت قیمت توکن ورودی + */ + public function getInputTokenPrice(): float + { + return (float) ($this->registryMGR->get('system', 'inputTokenPrice') ?? 0.00001); + } + + /** + * دریافت قیمت توکن خروجی + */ + public function getOutputTokenPrice(): float + { + return (float) ($this->registryMGR->get('system', 'outputTokenPrice') ?? 0.00003); + } + + /** + * بررسی فعال بودن هوش مصنوعی + */ + public function isAIEnabled(): bool + { + return (bool) ($this->registryMGR->get('system', 'aiEnabled') ?? false); + } + + /** + * دریافت پرامپ هوش مصنوعی + */ + public function getAIPrompt(): string + { + return $this->registryMGR->get('system', 'aiPrompt') ?? 'شما یک دستیار هوشمند برای سیستم حسابداری هستید.'; + } + + /** + * مدیریت گفتگو - ایجاد یا بازیابی گفتگوی موجود + */ + private function manageConversation(?int $conversationId, ?Business $business, $user, string $message): AIConversation + { + if ($conversationId) { + // بازیابی گفتگوی موجود + $conversation = $this->em->getRepository(AIConversation::class)->find($conversationId); + if ($conversation && $conversation->getUser() === $user && $conversation->getBusiness() === $business) { + // به‌روزرسانی زمان آخرین تغییر + $conversation->setUpdatedAt(time()); + $this->em->persist($conversation); + return $conversation; + } + } + + // ایجاد گفتگوی جدید + $conversation = new AIConversation(); + $conversation->setUser($user); + $conversation->setBusiness($business); + $conversation->setTitle($this->generateConversationTitle($message)); + $conversation->setCategory('عمومی'); + $conversation->setCreatedAt(time()); + $conversation->setUpdatedAt(time()); + $conversation->setIsActive(true); + $conversation->setDeleted(false); + + $this->em->persist($conversation); + return $conversation; + } + + /** + * دریافت تاریخچه گفتگو از دیتابیس + */ + private function getConversationHistory(AIConversation $conversation): array + { + $messages = $this->em->getRepository(AIMessage::class)->findBy( + ['conversation' => $conversation], + ['createdAt' => 'ASC'] + ); + + $history = []; + foreach ($messages as $message) { + $history[] = [ + 'role' => $message->getRole(), + 'content' => $message->getContent() + ]; + } + + return $history; + } + + /** + * ذخیره پیام کاربر + */ + private function saveUserMessage(AIConversation $conversation, string $message): void + { + $userMessage = new AIMessage(); + $userMessage->setConversation($conversation); + $userMessage->setRole('user'); + $userMessage->setContent($message); + $userMessage->setModel($this->getAIModel()); + $userMessage->setAgentSource($this->getAIAgentSource()); + $userMessage->setCreatedAt(time()); + + $this->em->persist($userMessage); + $this->em->flush(); + } + + /** + * ذخیره پاسخ هوش مصنوعی + */ + private function saveAIMessage(AIConversation $conversation, string $aiResponse, array $responseData, array $cost): void + { + $aiMessage = new AIMessage(); + $aiMessage->setConversation($conversation); + $aiMessage->setRole('assistant'); + $aiMessage->setContent($aiResponse); + $aiMessage->setModel($this->getAIModel()); + $aiMessage->setAgentSource($this->getAIAgentSource()); + $aiMessage->setCreatedAt(time()); + + // ذخیره اطلاعات usage + if (isset($responseData['usage'])) { + $aiMessage->setInputTokens($responseData['usage']['prompt_tokens'] ?? null); + $aiMessage->setOutputTokens($responseData['usage']['completion_tokens'] ?? null); + } + + // ذخیره هزینه‌ها + $aiMessage->setInputCost($cost['input_cost'] ?? null); + $aiMessage->setOutputCost($cost['output_cost'] ?? null); + $aiMessage->setTotalCost($cost['total_cost'] ?? null); + + $this->em->persist($aiMessage); + + // به‌روزرسانی زمان آخرین تغییر گفتگو + $conversation->setUpdatedAt(time()); + $this->em->persist($conversation); + + $this->em->flush(); + } + + /** + * تولید عنوان گفتگو بر اساس پیام اول + */ + private function generateConversationTitle(string $message): string + { + // حذف کاراکترهای اضافی و محدود کردن طول + $title = trim($message); + $title = preg_replace('/\s+/', ' ', $title); // حذف فاصله‌های اضافی + + // محدود کردن طول عنوان + if (mb_strlen($title, 'UTF-8') > 50) { + $title = mb_substr($title, 0, 47, 'UTF-8') . '...'; + } + + return $title ?: 'گفتگوی جدید'; + } + + /** + * دریافت گفتگوهای کاربر + */ + public function getUserConversations($user, ?Business $business, int $limit = 20): array + { + $conversations = $this->em->getRepository(AIConversation::class)->findBy( + [ + 'user' => $user, + 'business' => $business, + 'isActive' => true, + 'deleted' => false + ], + ['updatedAt' => 'DESC'], + $limit + ); + + $result = []; + foreach ($conversations as $conversation) { + $messageCount = count($conversation->getMessages()); + $lastMessage = $this->em->getRepository(AIMessage::class) + ->findLastMessageByConversation($conversation); + + $result[] = [ + 'id' => $conversation->getId(), + 'title' => $conversation->getTitle(), + 'category' => $conversation->getCategory(), + 'createdAt' => $conversation->getCreatedAt(), + 'updatedAt' => $conversation->getUpdatedAt(), + 'messageCount' => $messageCount, + 'lastMessage' => $lastMessage ? $lastMessage->getContent() : '' + ]; + } + + return $result; + } + + /** + * دریافت پیام‌های یک گفتگو + */ + public function getConversationMessages(int $conversationId, $user, ?Business $business): array + { + $conversation = $this->em->getRepository(AIConversation::class)->find($conversationId); + + if (!$conversation || $conversation->getUser() !== $user || $conversation->getBusiness() !== $business) { + return []; + } + + $messages = $this->em->getRepository(AIMessage::class)->findBy( + ['conversation' => $conversation], + ['createdAt' => 'ASC'] + ); + + $result = []; + foreach ($messages as $message) { + $result[] = [ + 'id' => $message->getId(), + 'role' => $message->getRole(), + 'content' => $message->getContent(), + 'createdAt' => $message->getCreatedAt(), + 'inputTokens' => $message->getInputTokens(), + 'outputTokens' => $message->getOutputTokens(), + 'inputCost' => $message->getInputCost(), + 'outputCost' => $message->getOutputCost(), + 'totalCost' => $message->getTotalCost(), + 'model' => $message->getModel(), + 'agentSource' => $message->getAgentSource() + ]; + } + + return $result; + } + + /** + * ساخت ابزارها از سرویس‌های پرامپ + */ + private function buildToolsFromPromptServices(): array + { + return $this->promptService->getAllTools(); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/BankPromptService.php b/hesabixCore/src/Service/AGI/Promps/BankPromptService.php new file mode 100644 index 00000000..793471e5 --- /dev/null +++ b/hesabixCore/src/Service/AGI/Promps/BankPromptService.php @@ -0,0 +1,111 @@ +em = $entityManager; + } + + /** + * دریافت تمام ابزارهای بخش بانک‌ها برای function calling + * @return array + */ + public function getTools(): array + { + $tools = []; + + // ابزار getBankAccountInfo + $bankAccountInfoPrompt = $this->getBankAccountInfoPrompt(); + $bankAccountInfoData = json_decode($bankAccountInfoPrompt, true); + + if ($bankAccountInfoData) { + $tools[] = [ + 'type' => 'function', + 'function' => [ + 'name' => $bankAccountInfoData['tool'], + 'description' => $bankAccountInfoData['description'], + 'parameters' => [ + 'type' => 'object', + 'properties' => $bankAccountInfoData['input'], + 'required' => array_keys($bankAccountInfoData['input']) + ] + ] + ]; + } + + return $tools; + } + + /** + * تولید تمام پرامپ‌های بخش بانک‌ها + * @return string + */ + public function getAllBankPrompts(): string + { + $prompts = []; + + // اضافه کردن تمام پرامپ‌های موجود + $prompts[] = $this->getBankAccountInfoPrompt(); + + // در آینده پرامپ‌های دیگر اضافه خواهند شد + // $prompts[] = $this->getCreateBankAccountPrompt(); + // $prompts[] = $this->getUpdateBankAccountPrompt(); + // $prompts[] = $this->getBankTransactionPrompt(); + + // ترکیب تمام پرامپ‌ها + return implode("\n\n", $prompts); + } + + /** + * پرامپ برای دریافت اطلاعات کامل حساب بانکی + * @return string + */ + public function getBankAccountInfoPrompt(): string + { + return '{ + "tool": "getBankAccountInfo", + "description": "Get complete bank account information by code", + "endpoint": "/api/bank/account/info/{code}", + "method": "GET", + "input": { + "code": "string - Bank account code (e.g., 1001, 1002)" + }, + "output": { + "id": "integer - Account ID", + "code": "string - Account code", + "name": "string - Account name", + "bankName": "string - Bank name", + "accountNumber": "string - Account number", + "shabaNumber": "string - Shaba number", + "cardNumber": "string - Card number", + "balance": "float - Current balance", + "currency": "string - Currency type", + "isActive": "boolean - Account active status", + "description": "string - Account description" + }, + "examples": { + "input": {"code": "1001"}, + "output": { + "id": 23, + "code": "1001", + "name": "حساب جاری اصلی", + "bankName": "ملت", + "accountNumber": "1234567890", + "shabaNumber": "IR123456789012345678901234", + "cardNumber": "6104337812345678", + "balance": 150000000, + "currency": "ریال", + "isActive": true, + "description": "حساب جاری اصلی شرکت" + } + } + }'; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/BasePromptService.php b/hesabixCore/src/Service/AGI/Promps/BasePromptService.php new file mode 100644 index 00000000..a7665ae3 --- /dev/null +++ b/hesabixCore/src/Service/AGI/Promps/BasePromptService.php @@ -0,0 +1,150 @@ +em = $entityManager; + $this->access = $access; + } + + /** + * پرامپ پایه برای معرفی سیستم + * @return string + */ + public function getSystemIntroductionPrompt(): string + { + // دسترسی فعلی + $acc = $this->access->hasRole('join'); + $apiToken = null; + if ($acc && isset($acc['bid']) && isset($acc['user'])) { + // جستجوی توکن AI معتبر برای این کاربر و کسب‌وکار + $now = time(); + $apiToken = $this->em->getRepository(APIToken::class)->findOneBy([ + 'bid' => $acc['bid'], + 'submitter' => $acc['user'], + 'isForAi' => true + ]); + if ($apiToken) { + $expire = $apiToken->getDateExpire(); + if ($expire && $expire != '0' && $now > (int)$expire) { + $apiToken = null; // منقضی شده + } + } + if (!$apiToken) { + // ساخت توکن جدید با اعتبار ۳۰ دقیقه + $apiToken = $this->access->createAiToken($acc['bid'], $acc['user'], 1800); + } + } + $apiKey = $apiToken ? $apiToken->getToken() : ''; + return '{ + "tool": "system_introduction", + "description": "System introduction and authentication requirements", + "content": "You are an AI assistant for Hesabix accounting system. This system manages businesses, persons, accounting entries, inventory, and financial reports. You can help users with various tasks using the available tools and APIs.", + "capabilities": [ + "Person management (customers, suppliers, employees)", + ], + "authentication": { + "method": "API Key or Session Token", + "required_headers": { + "api-key": "' . $apiKey . ' (این کد را در هدر api-key قرار بده)", + }, + }, + "language": "Persian (فارسی)", + "currency": "Iranian Rial (ریال)" +}'; + } + + /** + * پرامپ پایه برای خطاها + * @return string + */ + public function getErrorHandlingPrompt(): string + { + return '{ + "tool": "error_handling", + "description": "Error handling and authentication guidance", + "instructions": "When encountering errors, provide clear explanations in Persian and suggest solutions. Focus on authentication and access control issues.", + "error_types": { + "access_denied": "دسترسی غیرمجاز - کاربر فاقد مجوز لازم است", + "ai_permission_denied": "دسترسی هوش مصنوعی غیرمجاز - کاربر فاقد مجوز AI است", + "invalid_api_key": "کلید API نامعتبر - لطفاً کلید صحیح را وارد کنید", + "expired_token": "توکن منقضی شده - لطفاً توکن جدید دریافت کنید", + "invalid_business": "کسب و کار نامعتبر - شناسه کسب و کار صحیح نیست", + "invalid_year": "سال مالی نامعتبر - سال مالی انتخاب شده صحیح نیست", + "invalid_currency": "واحد پول نامعتبر - واحد پول انتخاب شده صحیح نیست", + "not_found": "مورد یافت نشد - کد یا شناسه وارد شده صحیح نیست", + "validation_error": "خطای اعتبارسنجی - اطلاعات وارد شده صحیح نیست", + "network_error": "خطای شبکه - اتصال اینترنت را بررسی کنید" + }, + "authentication_solutions": { + "missing_api_key": "کلید API را در هدر api-key قرار دهید", + "expired_token": "توکن جدید از مدیر سیستم دریافت کنید", + "no_ai_permission": "مجوز هوش مصنوعی از مدیر کسب و کار دریافت کنید", + "wrong_business": "شناسه کسب و کار صحیح را در هدر activeBid قرار دهید", + "wrong_year": "سال مالی صحیح را در هدر activeYear قرار دهید" + }, + "response_format": "Explain error in Persian, provide specific solution, and suggest next steps" +}'; + } + + /** + * پرامپ پایه برای راهنمایی کاربر + * @return string + */ + public function getHelpPrompt(): string + { + return '{ + "tool": "help", + "description": "User help and guidance", + "instructions": "Provide helpful guidance to users about available features and how to use them. Be concise and clear in Persian.", + "common_queries": { + "person_info": "برای دریافت اطلاعات شخص، کد شخص را وارد کنید", + }, + "response_format": "Provide step-by-step guidance in Persian with examples" +}'; + } + + /** + * پرامپ برای نمایش دامنه اصلی API + * @return string + */ + public function getApiBaseUrlPrompt(): string + { + // دریافت اولین رکورد تنظیمات + $settings = $this->em->getRepository(\App\Entity\Settings::class)->findAll(); + $appSite = isset($settings[0]) ? $settings[0]->getAppSite() : ''; + $domain = $appSite ? $appSite : '---'; + return '{ + "tool": "api_base_url", + "description": "آدرس پایه API", + "content": "تمام اندپوینت‌های سیستم از طریق دامنه زیر قابل دسترسی هستند:", + "base_url": "' . $domain . '" +}'; + } + + /** + * دریافت تمام پرامپ‌های پایه + * @return string + */ + public function getAllBasePrompts(): string + { + $prompts = []; + + $prompts[] = $this->getSystemIntroductionPrompt(); + $prompts[] = $this->getErrorHandlingPrompt(); + $prompts[] = $this->getHelpPrompt(); + $prompts[] = $this->getApiBaseUrlPrompt(); + + return implode("\n\n", $prompts); + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php b/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php new file mode 100644 index 00000000..64c34991 --- /dev/null +++ b/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php @@ -0,0 +1,116 @@ +em = $entityManager; + } + + /** + * دریافت تمام ابزارهای بخش کالاها برای function calling + * @return array + */ + public function getTools(): array + { + $tools = []; + + // ابزار getItemInfo + $itemInfoPrompt = $this->getItemInfoPrompt(); + $itemInfoData = json_decode($itemInfoPrompt, true); + + if ($itemInfoData) { + $tools[] = [ + 'type' => 'function', + 'function' => [ + 'name' => $itemInfoData['tool'], + 'description' => $itemInfoData['description'], + 'parameters' => [ + 'type' => 'object', + 'properties' => $itemInfoData['input'], + 'required' => array_keys($itemInfoData['input']) + ] + ] + ]; + } + + return $tools; + } + + /** + * تولید تمام پرامپ‌های بخش کالاها + * @return string + */ + public function getAllInventoryPrompts(): string + { + $prompts = []; + + // اضافه کردن تمام پرامپ‌های موجود + $prompts[] = $this->getItemInfoPrompt(); + + // در آینده پرامپ‌های دیگر اضافه خواهند شد + // $prompts[] = $this->getCreateItemPrompt(); + // $prompts[] = $this->getUpdateItemPrompt(); + // $prompts[] = $this->getSearchItemPrompt(); + // $prompts[] = $this->getItemStockPrompt(); + + // ترکیب تمام پرامپ‌ها + return implode("\n\n", $prompts); + } + + /** + * پرامپ برای دریافت اطلاعات کامل کالا + * @return string + */ + public function getItemInfoPrompt(): string + { + return '{ + "tool": "getItemInfo", + "description": "Get complete item information by code", + "endpoint": "/api/item/info/{code}", + "method": "GET", + "input": { + "code": "string - Item code (e.g., 1001, 1002)" + }, + "output": { + "id": "integer - Item ID", + "code": "string - Item code", + "name": "string - Item name", + "description": "string - Item description", + "category": "string - Item category", + "unit": "string - Unit of measurement", + "price": "float - Item price", + "stock": "float - Current stock quantity", + "minStock": "float - Minimum stock level", + "maxStock": "float - Maximum stock level", + "supplier": "string - Supplier name", + "barcode": "string - Barcode", + "isActive": "boolean - Item active status" + }, + "examples": { + "input": {"code": "1001"}, + "output": { + "id": 45, + "code": "1001", + "name": "لپ‌تاپ اپل", + "description": "لپ‌تاپ اپل مک‌بوک پرو 13 اینچ", + "category": "الکترونیک", + "unit": "عدد", + "price": 45000000, + "stock": 15, + "minStock": 5, + "maxStock": 50, + "supplier": "شرکت اپل", + "barcode": "1234567890123", + "isActive": true + } + } + }'; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php b/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php new file mode 100644 index 00000000..771e261a --- /dev/null +++ b/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php @@ -0,0 +1,185 @@ +em = $entityManager; + } + + /** + * دریافت تمام ابزارهای بخش اشخاص برای function calling + * @return array + */ + public function getTools(): array + { + $tools = []; + + // ابزار getPersonInfo + $personInfoPrompt = $this->getPersonInfoPrompt(); + $personInfoData = json_decode($personInfoPrompt, true); + + if ($personInfoData) { + $tools[] = [ + 'type' => 'function', + 'function' => [ + 'name' => $personInfoData['tool'], + 'description' => $personInfoData['description'], + 'parameters' => [ + 'type' => 'object', + 'properties' => $personInfoData['input'], + 'required' => array_keys($personInfoData['input']) + ] + ] + ]; + } + + return $tools; + } + + /** + * تولید تمام پرامپ‌های بخش اشخاص + * @return string + */ + public function getAllPersonPrompts(): string + { + $prompts = []; + + // اضافه کردن تمام پرامپ‌های موجود + $prompts[] = $this->getPersonInfoPrompt(); + + // در آینده پرامپ‌های دیگر اضافه خواهند شد + // $prompts[] = $this->getCreatePersonPrompt(); + // $prompts[] = $this->getUpdatePersonPrompt(); + // $prompts[] = $this->getSearchPersonPrompt(); + // $prompts[] = $this->getDeletePersonPrompt(); + + // ترکیب تمام پرامپ‌ها + return implode("\n\n", $prompts); + } + + /** + * پرامپ برای دریافت اطلاعات کامل شخص + * @return string + */ + public function getPersonInfoPrompt(): string + { + return '{ + "tool": "getPersonInfo", + "description": "Get complete person information by code", + "endpoint": "/api/person/info/{code}", + "method": "GET", + "input": { + "code": "string - Person code (e.g., 1001, 1002)" + }, + "output": { + "id": "integer - Person ID", + "code": "string - Person code", + "nikename": "string - Person nickname", + "name": "string - Person name", + "tel": "string - Telephone number", + "mobile": "string - Mobile number", + "mobile2": "string|null - Secondary mobile", + "des": "string - Description", + "company": "string - Company name", + "shenasemeli": "string - National ID", + "sabt": "string - Registration number", + "shahr": "string - City", + "keshvar": "string - Country", + "ostan": "string - Province", + "postalcode": "string - Postal code", + "codeeghtesadi": "string - Economic code", + "email": "string - Email address", + "website": "string - Website", + "fax": "string - Fax number", + "birthday": "string|null - Birthday", + "speedAccess": "boolean - Quick access flag", + "address": "string - Address", + "prelabel": "string|null - Pre label", + "accounts": "array - Bank accounts information", + "types": "array - Person types with checked status", + "bs": "float - Credit balance (بستانکار)", + "bd": "float - Debit balance (بدهکار)", + "balance": "float - Net balance (bs - bd)" + }, + "examples": { + "input": {"code": "1001"}, + "output": { + "id": 65, + "code": "1000", + "nikename": "مشتری ۱", + "name": "", + "tel": "", + "mobile": "09183282405", + "mobile2": null, + "des": "", + "company": "", + "shenasemeli": "0025", + "sabt": "", + "shahr": "", + "keshvar": "", + "ostan": "", + "postalcode": "", + "codeeghtesadi": "", + "email": "", + "website": "", + "fax": "", + "birthday": null, + "speedAccess": true, + "address": "", + "prelabel": null, + "accounts": [ + { + "bank": "حساب ۲", + "shabaNum": "شماره شبا", + "cardNum": "شماره کارت", + "accountNum": "شماره حساب" + } + ], + "types": [ + { + "label": "مشتری", + "code": "customer", + "checked": true + }, + { + "label": "بازاریاب", + "code": "marketer", + "checked": true + }, + { + "label": "کارمند", + "code": "emplyee", + "checked": false + }, + { + "label": "تامین‌کننده", + "code": "supplier", + "checked": false + }, + { + "label": "همکار", + "code": "colleague", + "checked": false + }, + { + "label": "فروشنده", + "code": "salesman", + "checked": false + } + ], + "bs": 752593632.55, + "bd": 283007419, + "balance": 469586213.54999995 + } + } +}'; + } + +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/PromptService.php b/hesabixCore/src/Service/AGI/Promps/PromptService.php new file mode 100644 index 00000000..a6117eb1 --- /dev/null +++ b/hesabixCore/src/Service/AGI/Promps/PromptService.php @@ -0,0 +1,167 @@ +em = $entityManager; + $this->personPromptService = $personPromptService; + $this->basePromptService = $basePromptService; + $this->inventoryPromptService = $inventoryPromptService; + $this->bankPromptService = $bankPromptService; + } + + /** + * دریافت تمام ابزارهای موجود برای function calling + * @return array + */ + public function getAllTools(): array + { + $tools = []; + + // ابزارهای بخش اشخاص + $personTools = $this->personPromptService->getTools(); + $tools = array_merge($tools, $personTools); + + // ابزارهای بخش کالاها + $inventoryTools = $this->inventoryPromptService->getTools(); + $tools = array_merge($tools, $inventoryTools); + + // ابزارهای بخش بانک‌ها + $bankTools = $this->bankPromptService->getTools(); + $tools = array_merge($tools, $bankTools); + + // در آینده ابزارهای بخش‌های دیگر اضافه خواهند شد + // $accountingTools = $this->accountingPromptService->getTools(); + // $tools = array_merge($tools, $accountingTools); + + return $tools; + } + + /** + * دریافت تمام پرامپ‌های پایه (سیستم، API، و غیره) + * @return string + */ + public function getAllBasePrompts(): string + { + return $this->basePromptService->getAllBasePrompts(); + } + + /** + * دریافت پرامپ سیستمی بر اساس کلید + * @param string $key کلید پرامپ + * @return string|null + */ + public function getSystemPrompt(string $key): ?string + { + switch ($key) { + case 'person': + return $this->personPromptService->getAllPersonPrompts(); + // در آینده موارد بیشتر اضافه خواهند شد + // case 'accounting': + // return $this->accountingPromptService->getAllAccountingPrompts(); + // case 'inventory': + // return $this->inventoryPromptService->getAllInventoryPrompts(); + // case 'reports': + // return $this->reportsPromptService->getAllReportsPrompts(); + // case 'bank': + // return $this->bankPromptService->getAllBankPrompts(); + default: + return null; + } + } + + /** + * دریافت تمام پرامپ‌های سیستمی + * @return array + */ + public function getAllSystemPrompts(): array + { + $prompts = []; + + // پرامپ‌های پایه (سیستم، API، و غیره) + $prompts['base'] = $this->basePromptService->getAllBasePrompts(); + + // پرامپ‌های بخش اشخاص + $prompts['person'] = $this->personPromptService->getAllPersonPrompts(); + + // پرامپ‌های بخش کالاها + $prompts['inventory'] = $this->inventoryPromptService->getAllInventoryPrompts(); + + // پرامپ‌های بخش بانک‌ها + $prompts['bank'] = $this->bankPromptService->getAllBankPrompts(); + + // در آینده بخش‌های دیگر اضافه خواهند شد + // $prompts['accounting'] = $this->accountingPromptService->getAllAccountingPrompts(); + // $prompts['reports'] = $this->reportsPromptService->getAllReportsPrompts(); + // $prompts['sales'] = $this->salesPromptService->getAllSalesPrompts(); + // $prompts['purchases'] = $this->purchasesPromptService->getAllPurchasesPrompts(); + // $prompts['bank'] = $this->bankPromptService->getAllBankPrompts(); + + return $prompts; + } + + /** + * دریافت تمام پرامپ‌ها به صورت یک رشته واحد + * @return string + */ + public function getAllPromptsAsString(): string + { + $allPrompts = $this->getAllSystemPrompts(); + return implode("\n\n", $allPrompts); + } + + /** + * قوانین خروجی JSON و مثال برای پاسخ هوش مصنوعی + * @return string + */ + public function getOutputFormatPrompt(): string + { + $prompt = "\n\nقوانین مهم: همیشه پاسخ را فقط در قالب یک شیء JSON با ساختار زیر ارسال کن و هیچ توضیح اضافه‌ای ننویس:\n"; + $prompt .= <<$this->request->headers->get('api-key') ]); if(!$token) { return false; } + + // بررسی تاریخ انقضا + $dateExpire = $token->getDateExpire(); + if($dateExpire && $dateExpire != '0') { + $expireTimestamp = (int)$dateExpire; + $currentTimestamp = time(); + if($currentTimestamp > $expireTimestamp) { + return false; // توکن منقضی شده + } + } + $bid = $token->getBid(); } if ($this->request->headers->get('activeYear')) { @@ -96,8 +108,19 @@ class Access 'money'=>$money ]; + // بررسی دسترسی هوش مصنوعی + $aiPermission = $this->em->getRepository(Permission::class)->findOneBy([ + 'bid'=>$bid, + 'user'=>$this->user + ]); + $accessArray['ai'] = false; + if($aiPermission && $aiPermission->isAi()){ + $accessArray['ai'] = true; + } + if($bid->getOwner()->getEmail() === $this->user->getUserIdentifier()){ - //user is owner + //user is owner - همه دسترسی‌ها را دارد + $accessArray['ai'] = true; return $accessArray; } elseif ($this->user && $roll == 'join' && count($this->em->getRepository(Permission::class)->findBy(['user'=>$this->user,'bid'=>$bid]))){ @@ -114,4 +137,36 @@ class Access } return false; } + + /** + * ایجاد توکن API برای هوش مصنوعی + * @param Business $business کسب و کار + * @param User $user کاربر ایجاد کننده + * @param int|null $expireSeconds زمان انقضا به ثانیه (null برای همیشه معتبر) + * @return APIToken + */ + public function createAiToken(Business $business, User $user, ?int $expireSeconds = null): APIToken + { + $token = new APIToken(); + $token->setBid($business); + $token->setSubmitter($user); + $token->setIsForAi(true); + + // ایجاد توکن تصادفی + $randomToken = bin2hex(random_bytes(32)); + $token->setToken($randomToken); + + // تنظیم تاریخ انقضا + if ($expireSeconds !== null) { + $expireTimestamp = time() + $expireSeconds; + $token->setDateExpire((string)$expireTimestamp); + } else { + $token->setDateExpire('0'); // همیشه معتبر + } + + $this->em->persist($token); + $this->em->flush(); + + return $token; + } } \ No newline at end of file diff --git a/hesabixCore/src/Service/Inquiry.php b/hesabixCore/src/Service/Inquiry.php new file mode 100644 index 00000000..6fe3fc4b --- /dev/null +++ b/hesabixCore/src/Service/Inquiry.php @@ -0,0 +1,448 @@ +entityManager->getRepository(PostalCodeInquiry::class)->findByPostalCode($postalCode); + + if ($existingInquiry) { + // اگر در دیتابیس موجود است، از آن استفاده کن + $addressData = $existingInquiry->getAddressData(); + return [ + 'result' => 1, + 'response_body' => [ + 'data' => [ + 'address' => $addressData + ], + 'message' => 'موفق (از کش)', + 'error_code' => null + ] + ]; + } + + // اگر در دیتابیس موجود نیست، از API استفاده کن + $registryMGR = new RegistryMGR($this->entityManager); + $inquiryPanel = $registryMGR->get('system', key: 'inquiryPanel'); + if($inquiryPanel == 'zohal'){ + $inquiryZohalAPIKey = $registryMGR->get('system', key: 'inquiryZohalAPIKey'); + + // بررسی وجود API Key + if (empty($inquiryZohalAPIKey)) { + return [ + 'result' => 0, + 'message' => 'API Key تنظیم نشده است', + 'error_code' => 'API_KEY_MISSING' + ]; + } + + // استفاده از API جدید زحل + $url = "https://service.zohal.io/api/v0/services/inquiry/postal_code_inquiry"; + + // آماده‌سازی داده‌های JSON + $postData = json_encode([ + 'postal_code' => $postalCode + ]); + + // تنظیمات cURL + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($postData), + 'Authorization: Bearer ' . $inquiryZohalAPIKey, + 'X-API-Key: ' . $inquiryZohalAPIKey + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + // بررسی خطای cURL + if ($curlError) { + return [ + 'result' => 0, + 'message' => 'خطا در ارتباط با سرور: ' . $curlError, + 'error_code' => 'CURL_ERROR' + ]; + } + + if ($httpCode === 200) { + $data = json_decode($response, true); + + // بررسی کدهای result + if (isset($data['result'])) { + switch ($data['result']) { + case 1: + // موفق - ذخیره در دیتابیس + if (isset($data['response_body']['data']['address'])) { + $this->saveToDatabase($postalCode, $data['response_body']['data']['address']); + } + return $data; + case 4: + return [ + 'result' => 4, + 'message' => 'توکن غیر فعال شده است', + 'error_code' => 'TOKEN_INACTIVE' + ]; + case 5: + return [ + 'result' => 5, + 'message' => 'سرویس در دسترسی نمی‌باشد', + 'error_code' => 'SERVICE_UNAVAILABLE' + ]; + case 6: + return [ + 'result' => 6, + 'message' => 'فراخوانی وب‌سرویس با پارامترهای ورودی صحیح نمی‌باشد', + 'error_code' => 'INVALID_PARAMETERS' + ]; + default: + return [ + 'result' => $data['result'], + 'message' => $data['message'] ?? 'خطای نامشخص', + 'error_code' => $data['error_code'] ?? 'UNKNOWN_ERROR' + ]; + } + } + + return $data; + } else { + return [ + 'result' => 0, + 'message' => 'خطا در ارتباط با سرویس استعلام کد پستی (کد خطا: ' . $httpCode . ')', + 'error_code' => 'HTTP_ERROR_' . $httpCode + ]; + } + } + + return [ + 'result' => 0, + 'message' => 'سرویس استعلام کد پستی فعال نیست', + 'error_code' => 'SERVICE_NOT_ACTIVE' + ]; + } + + public function cardToSheba($cardNumber) + { + // بررسی دیتابیس برای کش + $existingInquiry = $this->entityManager->getRepository(CardToShebaInquiry::class)->findByCardNumber($cardNumber); + + if ($existingInquiry) { + // اگر در دیتابیس موجود است، از آن استفاده کن + $shebaData = $existingInquiry->getShebaData(); + return [ + 'result' => 1, + 'response_body' => [ + 'data' => $shebaData, + 'message' => 'موفق (از حافظه موقت)', + 'error_code' => null + ] + ]; + } + + // اگر در دیتابیس موجود نیست، از API استفاده کن + $registryMGR = new RegistryMGR($this->entityManager); + $inquiryPanel = $registryMGR->get('system', key: 'inquiryPanel'); + if($inquiryPanel == 'zohal'){ + $inquiryZohalAPIKey = $registryMGR->get('system', key: 'inquiryZohalAPIKey'); + + // بررسی وجود API Key + if (empty($inquiryZohalAPIKey)) { + return [ + 'result' => 0, + 'message' => 'API Key تنظیم نشده است', + 'error_code' => 'API_KEY_MISSING' + ]; + } + + // استفاده از API زحل برای تبدیل کارت به شبا + $url = "https://service.zohal.io/api/v0/services/inquiry/card_to_iban"; + + // آماده‌سازی داده‌های JSON + $postData = json_encode([ + 'card_number' => $cardNumber + ]); + + // تنظیمات cURL + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($postData), + 'Authorization: Bearer ' . $inquiryZohalAPIKey, + 'X-API-Key: ' . $inquiryZohalAPIKey + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + // بررسی خطای cURL + if ($curlError) { + return [ + 'result' => 0, + 'message' => 'خطا در ارتباط با سرور: ' . $curlError, + 'error_code' => 'CURL_ERROR' + ]; + } + + if ($httpCode === 200) { + $data = json_decode($response, true); + + // بررسی کدهای result + if (isset($data['result'])) { + switch ($data['result']) { + case 1: + // موفق - ذخیره در دیتابیس + if (isset($data['response_body']['data'])) { + $this->saveCardToShebaToDatabase($cardNumber, $data['response_body']['data']); + } + return $data; + case 4: + return [ + 'result' => 4, + 'message' => 'توکن غیر فعال شده است', + 'error_code' => 'TOKEN_INACTIVE' + ]; + case 5: + return [ + 'result' => 5, + 'message' => 'سرویس در دسترسی نمی‌باشد', + 'error_code' => 'SERVICE_UNAVAILABLE' + ]; + case 6: + return [ + 'result' => 6, + 'message' => 'فراخوانی وب‌سرویس با پارامترهای ورودی صحیح نمی‌باشد', + 'error_code' => 'INVALID_PARAMETERS' + ]; + default: + return [ + 'result' => $data['result'], + 'message' => $data['message'] ?? 'خطای نامشخص', + 'error_code' => $data['error_code'] ?? 'UNKNOWN_ERROR' + ]; + } + } + + return $data; + } else { + return [ + 'result' => 0, + 'message' => 'خطا در ارتباط با سرویس تبدیل کارت به شبا (کد خطا: ' . $httpCode . ')', + 'error_code' => 'HTTP_ERROR_' . $httpCode + ]; + } + } + + return [ + 'result' => 0, + 'message' => 'سرویس تبدیل کارت به شبا فعال نیست', + 'error_code' => 'SERVICE_NOT_ACTIVE' + ]; + } + + private function saveToDatabase(string $postalCode, array $addressData): void + { + try { + $inquiry = new PostalCodeInquiry(); + $inquiry->setPostalCode($postalCode); + $inquiry->setAddressData($addressData); + $inquiry->setUpdatedAt(new \DateTimeImmutable()); + + $this->entityManager->persist($inquiry); + $this->entityManager->flush(); + + // ثبت لاگ ذخیره موفق در دیتابیس + error_log("کد پستی {$postalCode} با موفقیت در کش ذخیره شد"); + } catch (\Exception $e) { + // در صورت خطا در ذخیره، فقط لاگ کن و ادامه بده + error_log('خطا در ذخیره استعلام کد پستی: ' . $e->getMessage()); + } + } + + public function accountToSheba($bankCode, $accountNumber) + { + // بررسی دیتابیس برای کش + $cacheKey = $bankCode . '_' . $accountNumber; + $existingInquiry = $this->entityManager->getRepository(AccountToShebaInquiry::class)->findByCacheKey($cacheKey); + + if ($existingInquiry) { + // اگر در دیتابیس موجود است، از آن استفاده کن + $shebaData = $existingInquiry->getShebaData(); + return [ + 'result' => 1, + 'response_body' => [ + 'data' => $shebaData, + 'message' => 'موفق (از حافظه موقت)', + 'error_code' => null + ] + ]; + } + + // اگر در دیتابیس موجود نیست، از API استفاده کن + $registryMGR = new RegistryMGR($this->entityManager); + $inquiryPanel = $registryMGR->get('system', key: 'inquiryPanel'); + if($inquiryPanel == 'zohal'){ + $inquiryZohalAPIKey = $registryMGR->get('system', key: 'inquiryZohalAPIKey'); + + // بررسی وجود API Key + if (empty($inquiryZohalAPIKey)) { + return [ + 'result' => 0, + 'message' => 'API Key تنظیم نشده است', + 'error_code' => 'API_KEY_MISSING' + ]; + } + + // استفاده از API زحل برای تبدیل حساب به شبا + $url = "https://service.zohal.io/api/v0/services/inquiry/account_to_iban"; + + // آماده‌سازی داده‌های JSON + $postData = json_encode([ + 'bank_code' => $bankCode, + 'bank_account' => $accountNumber + ]); + + // تنظیمات cURL + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($postData), + 'Authorization: Bearer ' . $inquiryZohalAPIKey, + 'X-API-Key: ' . $inquiryZohalAPIKey + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + // بررسی خطای cURL + if ($curlError) { + return [ + 'result' => 0, + 'message' => 'خطا در ارتباط با سرور: ' . $curlError, + 'error_code' => 'CURL_ERROR' + ]; + } + + if ($httpCode === 200) { + $data = json_decode($response, true); + + // بررسی کدهای result + if (isset($data['result'])) { + switch ($data['result']) { + case 1: + // موفق - ذخیره در دیتابیس + if (isset($data['response_body']['data'])) { + $this->saveAccountToShebaToDatabase($cacheKey, $data['response_body']['data']); + } + return $data; + case 4: + return [ + 'result' => 4, + 'message' => 'توکن غیر فعال شده است', + 'error_code' => 'TOKEN_INACTIVE' + ]; + case 5: + return [ + 'result' => 5, + 'message' => 'سرویس در دسترسی نمی‌باشد', + 'error_code' => 'SERVICE_UNAVAILABLE' + ]; + case 6: + return [ + 'result' => 6, + 'message' => 'فراخوانی وب‌سرویس با پارامترهای ورودی صحیح نمی‌باشد', + 'error_code' => 'INVALID_PARAMETERS' + ]; + default: + return [ + 'result' => $data['result'], + 'message' => $data['message'] ?? 'خطای نامشخص', + 'error_code' => $data['error_code'] ?? 'UNKNOWN_ERROR' + ]; + } + } + + return $data; + } else { + return [ + 'result' => 0, + 'message' => 'خطا در ارتباط با سرویس تبدیل حساب به شبا (کد خطا: ' . $httpCode . ')', + 'error_code' => 'HTTP_ERROR_' . $httpCode + ]; + } + } + + return [ + 'result' => 0, + 'message' => 'سرویس تبدیل حساب به شبا فعال نیست', + 'error_code' => 'SERVICE_NOT_ACTIVE' + ]; + } + + private function saveCardToShebaToDatabase(string $cardNumber, array $shebaData): void + { + try { + $inquiry = new CardToShebaInquiry(); + $inquiry->setCardNumber($cardNumber); + $inquiry->setShebaData($shebaData); + $inquiry->setUpdatedAt(new \DateTimeImmutable()); + + $this->entityManager->persist($inquiry); + $this->entityManager->flush(); + + // ثبت لاگ ذخیره موفق در دیتابیس + error_log("شماره کارت {$cardNumber} با موفقیت در حافظه موقت ذخیره شد"); + } catch (\Exception $e) { + // در صورت خطا در ذخیره، فقط لاگ کن و ادامه بده + error_log('خطا در ذخیره استعلام کارت به شبا: ' . $e->getMessage()); + } + } + + private function saveAccountToShebaToDatabase(string $cacheKey, array $shebaData): void + { + try { + $inquiry = new AccountToShebaInquiry(); + $inquiry->setCacheKey($cacheKey); + $inquiry->setShebaData($shebaData); + $inquiry->setUpdatedAt(new \DateTimeImmutable()); + + $this->entityManager->persist($inquiry); + $this->entityManager->flush(); + + // ثبت لاگ ذخیره موفق در دیتابیس + error_log("حساب با کلید {$cacheKey} با موفقیت در حافظه موقت ذخیره شد"); + } catch (\Exception $e) { + // در صورت خطا در ذخیره، فقط لاگ کن و ادامه بده + error_log('خطا در ذخیره استعلام حساب به شبا: ' . $e->getMessage()); + } + } +} diff --git a/hesabixCore/src/Service/registryMGR.php b/hesabixCore/src/Service/registryMGR.php index 43bc59a4..454927cf 100644 --- a/hesabixCore/src/Service/registryMGR.php +++ b/hesabixCore/src/Service/registryMGR.php @@ -24,6 +24,13 @@ class registryMGR } $item->setRoot($root); $item->setName($key); + + // محدود کردن طول داده برای جلوگیری از خطای value_of_key + $maxLength = 65535; // حداکثر طول برای TEXT در MySQL + if (strlen($v) > $maxLength) { + $v = substr($v, 0, $maxLength - 100) . '... [برش داده شده]'; + } + $item->setValueOfKey($v); $this->em->persist($item); $this->em->flush(); @@ -36,12 +43,7 @@ class registryMGR 'name'=>$key ]); if(! $item){ - $item = new Registry(); - $item->setRoot($root); - $item->setName($key); - $item->setValueOfKey(''); - $this->em->persist($item); - $this->em->flush(); + return null; // برگرداندن null به جای ایجاد آیتم جدید } return $item->getValueOfKey(); } diff --git a/install.sh b/install.sh index b1a08aab..3c604bb6 100644 --- a/install.sh +++ b/install.sh @@ -791,11 +791,11 @@ install_software() { # Check if remote origin exists if ! git remote get-url origin >/dev/null 2>&1; then # Add remote repository if it doesn't exist - git remote add origin https://github.com/morrning/hesabixCore.git || \ + git remote add origin https://source.hesabix.ir/morrning/hesabixCore.git || \ handle_error "Failed to add remote repository" else # Update remote URL if it exists - git remote set-url origin https://github.com/morrning/hesabixCore.git || \ + git remote set-url origin https://source.hesabix.ir/morrning/hesabixCore.git || \ handle_error "Failed to update remote repository" fi diff --git a/webUI/package.json b/webUI/package.json index 191b73a7..d2930fdd 100755 --- a/webUI/package.json +++ b/webUI/package.json @@ -17,6 +17,7 @@ "@tiptap/extension-text-align": "^2.11.7", "@tiptap/starter-kit": "^2.11.7", "@tiptap/vue-3": "^2.11.7", + "@types/dompurify": "^3.0.5", "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", "@vueuse/core": "^13.1.0", @@ -27,12 +28,14 @@ "axios": "^1.8.4", "date-fns": "^4.1.0", "date-fns-jalali": "^3.2.0-0", + "dompurify": "^3.2.6", "downloadjs": "^1.4.7", "file-saver": "^2.0.5", "html5-qrcode": "^2.3.8", "jalali-moment": "^3.3.11", "libphonenumber-js": "^1.12.7", "lodash": "^4.17.21", + "marked": "^16.1.0", "maska": "^3.1.1", "maz-ui": "^3.50.1", "pinia": "^3.0.2", diff --git a/webUI/src/components/PersonInfo.vue b/webUI/src/components/PersonInfo.vue new file mode 100644 index 00000000..ce73dd25 --- /dev/null +++ b/webUI/src/components/PersonInfo.vue @@ -0,0 +1,322 @@ + + + + + \ No newline at end of file diff --git a/webUI/src/components/widgets/AIChart.vue b/webUI/src/components/widgets/AIChart.vue new file mode 100644 index 00000000..12bf66b4 --- /dev/null +++ b/webUI/src/components/widgets/AIChart.vue @@ -0,0 +1,489 @@ + + + + + \ No newline at end of file diff --git a/webUI/src/components/widgets/inquiry/postalcode2address.vue b/webUI/src/components/widgets/inquiry/postalcode2address.vue new file mode 100644 index 00000000..402042ee --- /dev/null +++ b/webUI/src/components/widgets/inquiry/postalcode2address.vue @@ -0,0 +1,188 @@ + + + + diff --git a/webUI/src/i18n/en_lang.ts b/webUI/src/i18n/en_lang.ts index e0613b5a..e087b459 100755 --- a/webUI/src/i18n/en_lang.ts +++ b/webUI/src/i18n/en_lang.ts @@ -86,6 +86,7 @@ const en_lang = { cheque_output: "Cheque Output", presells: "Presells", presell_view: "View Presell", + inquiry: "Inquiries", hrm: 'HR & Payroll', hrm_docs: 'Payroll Document', }, diff --git a/webUI/src/i18n/fa_lang.ts b/webUI/src/i18n/fa_lang.ts index 126e811e..a0c92537 100755 --- a/webUI/src/i18n/fa_lang.ts +++ b/webUI/src/i18n/fa_lang.ts @@ -194,6 +194,7 @@ const fa_lang = { plugins_invoices: "صورت حساب‌ها", repservice: "مدیریت تعمیرگاه", repservice_reqs: "درخواست‌ها", + inquiry: "استعلامات", hrm: 'منابع انسانی', hrm_docs: 'سند حقوق', buysellByPerson: "گزارش خرید و فروش های اشخاص", @@ -202,7 +203,7 @@ const fa_lang = { }, time: { month: "{id} ماه", - }, + }, calendar: { shamsi: "هجری شمسی", gregorian: "میلادی", @@ -922,5 +923,15 @@ const fa_lang = { numberinput: { invalid_number: "فقط عدد انگلیسی مجاز است" }, + chart: { + download: "دانلود نمودار", + refresh: "نوسازی نمودار", + fullscreen: "تمام‌صفحه", + details: "جزئیات نمودار", + type: "نوع نمودار", + id: "شناسه نمودار", + dataPoints: "تعداد نقاط داده", + created: "تاریخ ایجاد" + }, }; export default fa_lang diff --git a/webUI/src/router/index.ts b/webUI/src/router/index.ts index f2750dde..f28339da 100755 --- a/webUI/src/router/index.ts +++ b/webUI/src/router/index.ts @@ -1012,6 +1012,12 @@ const router = createRouter({ component: () => import('../views/acc/plugins/hrm/docs/view.vue'), }, + { + path: 'inquiry/panel', + name: 'inquiry_panel', + component: () => + import('../views/acc/inquiry/panel.vue'), + }, ], }, { diff --git a/webUI/src/stores/navigationStore.ts b/webUI/src/stores/navigationStore.ts new file mode 100644 index 00000000..536a2a03 --- /dev/null +++ b/webUI/src/stores/navigationStore.ts @@ -0,0 +1,33 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useNavigationStore = defineStore('navigation', () => { + const drawer = ref(true) + const previousDrawerState = ref(true) + + const closeDrawer = () => { + previousDrawerState.value = drawer.value + drawer.value = false + } + + const openDrawer = () => { + drawer.value = previousDrawerState.value + } + + const toggleDrawer = () => { + drawer.value = !drawer.value + } + + const setDrawer = (value: boolean) => { + drawer.value = value + } + + return { + drawer, + previousDrawerState, + closeDrawer, + openDrawer, + toggleDrawer, + setDrawer + } +}) \ No newline at end of file diff --git a/webUI/src/views/acc/App.vue b/webUI/src/views/acc/App.vue index 1645a6af..38e47980 100755 --- a/webUI/src/views/acc/App.vue +++ b/webUI/src/views/acc/App.vue @@ -4,6 +4,7 @@ import axios from "axios"; import Swal from "sweetalert2"; import { getApiUrl, getBasePath, getSiteName, getSiteSlogan } from "@/hesabixConfig"; import { ref } from 'vue'; +import { useNavigationStore } from '@/stores/navigationStore'; import Profile_btn from '@/components/application/buttons/profile_btn.vue'; import Notifications_btn from '@/components/application/buttons/notifications_btn.vue'; import Year_cob from '@/components/application/combobox/year_cob.vue'; @@ -15,7 +16,7 @@ import ShortcutsButton from '@/components/application/buttons/ShortcutsButton.vu export default { data() { return { - drawer: ref(null), + navigationStore: useNavigationStore(), plugins: [], business: { id: '', name: '' }, timeNow: '', @@ -35,6 +36,16 @@ export default { canFreeAccounting: true, }; }, + computed: { + drawer: { + get() { + return this.navigationStore.drawer; + }, + set(value) { + this.navigationStore.setDrawer(value); + } + } + }, mounted() { axios.post('/api/plugin/get/actives').then((response) => { this.plugins = response.data; @@ -169,8 +180,10 @@ export default { { path: '/acc/business/extramoneys', key: '-', label: this.$t('drawer.extra_moneys'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('accpro') }, { path: '/acc/business/logs', key: '=', label: this.$t('drawer.history'), ctrl: true, shift: true, permission: () => this.permissions.log }, { path: '/acc/plugin/repservice/order/list', key: '[', label: this.$t('drawer.repservice_reqs'), ctrl: true, shift: true, permission: () => this.permissions.plugRepservice && this.isPluginActive('repservice') }, - { path: '/acc/sms/panel', key: ']', label: this.$t('drawer.sms_panel'), ctrl: true, shift: true, permission: () => this.permissions.owner }, + { path: '/acc/inquiry/panel', key: ']', label: this.$t('drawer.inquiry'), ctrl: true, shift: true, permission: () => true }, + { path: '/acc/wizard/home', key: 'A', label: 'هوش مصنوعی', ctrl: true, shift: true, permission: () => this.permissions.ai }, { path: '/acc/printers/list', key: ';', label: this.$t('drawer.cloud_printers'), ctrl: true, shift: true, permission: () => this.permissions.owner }, + { path: '/acc/sms/panel', key: '`', label: this.$t('drawer.sms_panel'), ctrl: true, shift: true, permission: () => this.permissions.owner }, { path: '/acc/archive/list', key: '\'', label: this.$t('drawer.archive_files'), ctrl: true, shift: true, permission: () => this.permissions.archiveUpload || this.permissions.archiveMod || this.permissions.archiveDelete }, { path: '/acc/archive/order/new', key: ',', label: this.$t('drawer.archive_order'), ctrl: true, shift: true, permission: () => this.permissions.owner }, { path: '/acc/archive/order/list', key: '.', label: this.$t('drawer.archive_log'), ctrl: true, shift: true, permission: () => this.permissions.owner }, @@ -790,6 +803,20 @@ export default { + + + + {{ $t('drawer.inquiry') }} + {{ getShortcutKey('/acc/inquiry/panel') }} + + + + + + هوش مصنوعی + {{ getShortcutKey('/acc/wizard/home') }} + +