{"repo": "aryawiratama26/saveit-website", "files": {"tests/Unit/Services/DeepseekServiceTest.php": {"f9708ef0e90bfd9e437d22c02f2608a14916de27": {"t": 1785745775.9015539, "n": 3094, "h": "d9953e83eb51fad4", "c": "expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('DEEPSEEK_API_KEY belum dikonfigurasi');\n\n $service->parseFinancialDocument('test text');\n }\n\n public function test_parses_valid_response(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response([\n 'choices' => [[\n 'message' => [\n 'content' => json_encode([\n 'type' => 'expense',\n 'amount' => 150000,\n 'description' => 'Belanja bulanan',\n 'date' => '2026-07-21',\n 'category_suggestion' => 'Belanja',\n ]),\n ],\n ]],\n ], 200),\n ]);\n\n $service = new DeepseekService();\n $result = $service->parseFinancialDocument('Sample OCR text');\n\n $this->assertEquals('expense', $result['type']);\n $this->assertEquals(150000, $result['amount']);\n $this->assertEquals('Belanja bulanan', $result['description']);\n }\n\n public function test_throws_exception_on_api_error(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response(['error' => 'Unauthorized'], 401),\n ]);\n\n $service = new DeepseekService();\n\n $this->expectException(\\RuntimeException::class);\n\n $service->parseFinancialDocument('Sample text');\n }\n\n public function test_normalizes_negative_amount(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response([\n 'choices' => [[\n 'message' => [\n 'content' => json_encode([\n 'type' => 'expense',\n 'amount' => -50000,\n 'description' => 'Test',\n 'date' => '2026-07-21',\n ]),\n ],\n ]],\n ], 200),\n ]);\n\n $service = new DeepseekService();\n $result = $service->parseFinancialDocument('test');\n\n $this->assertEquals(50000, $result['amount']);\n }\n}\n"}, "e0daec2ba82231596c05511ccc19331794cf40e9": {"t": 1785756407.8582106, "n": 3094, "h": "d9953e83eb51fad4", "c": "expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('DEEPSEEK_API_KEY belum dikonfigurasi');\n\n $service->parseFinancialDocument('test text');\n }\n\n public function test_parses_valid_response(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response([\n 'choices' => [[\n 'message' => [\n 'content' => json_encode([\n 'type' => 'expense',\n 'amount' => 150000,\n 'description' => 'Belanja bulanan',\n 'date' => '2026-07-21',\n 'category_suggestion' => 'Belanja',\n ]),\n ],\n ]],\n ], 200),\n ]);\n\n $service = new DeepseekService();\n $result = $service->parseFinancialDocument('Sample OCR text');\n\n $this->assertEquals('expense', $result['type']);\n $this->assertEquals(150000, $result['amount']);\n $this->assertEquals('Belanja bulanan', $result['description']);\n }\n\n public function test_throws_exception_on_api_error(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response(['error' => 'Unauthorized'], 401),\n ]);\n\n $service = new DeepseekService();\n\n $this->expectException(\\RuntimeException::class);\n\n $service->parseFinancialDocument('Sample text');\n }\n\n public function test_normalizes_negative_amount(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response([\n 'choices' => [[\n 'message' => [\n 'content' => json_encode([\n 'type' => 'expense',\n 'amount' => -50000,\n 'description' => 'Test',\n 'date' => '2026-07-21',\n ]),\n ],\n ]],\n ], 200),\n ]);\n\n $service = new DeepseekService();\n $result = $service->parseFinancialDocument('test');\n\n $this->assertEquals(50000, $result['amount']);\n }\n}\n"}, "0afe634ca6bbadce5906e749a3c160ed095b4f5e": {"t": 1786068315.4859898, "n": 3094, "h": "d9953e83eb51fad4", "c": "expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('DEEPSEEK_API_KEY belum dikonfigurasi');\n\n $service->parseFinancialDocument('test text');\n }\n\n public function test_parses_valid_response(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response([\n 'choices' => [[\n 'message' => [\n 'content' => json_encode([\n 'type' => 'expense',\n 'amount' => 150000,\n 'description' => 'Belanja bulanan',\n 'date' => '2026-07-21',\n 'category_suggestion' => 'Belanja',\n ]),\n ],\n ]],\n ], 200),\n ]);\n\n $service = new DeepseekService();\n $result = $service->parseFinancialDocument('Sample OCR text');\n\n $this->assertEquals('expense', $result['type']);\n $this->assertEquals(150000, $result['amount']);\n $this->assertEquals('Belanja bulanan', $result['description']);\n }\n\n public function test_throws_exception_on_api_error(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response(['error' => 'Unauthorized'], 401),\n ]);\n\n $service = new DeepseekService();\n\n $this->expectException(\\RuntimeException::class);\n\n $service->parseFinancialDocument('Sample text');\n }\n\n public function test_normalizes_negative_amount(): void\n {\n Config::set('services.deepseek.api_key', 'test-key');\n Config::set('services.deepseek.api_url', 'https://api.deepseek.com/chat/completions');\n\n Http::fake([\n 'api.deepseek.com/*' => Http::response([\n 'choices' => [[\n 'message' => [\n 'content' => json_encode([\n 'type' => 'expense',\n 'amount' => -50000,\n 'description' => 'Test',\n 'date' => '2026-07-21',\n ]),\n ],\n ]],\n ], 200),\n ]);\n\n $service = new DeepseekService();\n $result = $service->parseFinancialDocument('test');\n\n $this->assertEquals(50000, $result['amount']);\n }\n}\n"}}, "app/Services/DeepseekService.php": {"0afe634ca6bbadce5906e749a3c160ed095b4f5e": {"t": 1786068314.212665, "n": 6023, "h": "9262c7df59632826", "c": "apiKey = config('services.deepseek.api_key');\n $this->apiUrl = config('services.deepseek.api_url');\n }\n\n /**\n * Parse raw OCR text into structured financial data via Deepseek.\n *\n * @param string $ocrText Raw text from OCR.\n * @return array Structured data: type, amount, description, date, category_suggestion.\n * @throws \\Exception If API call fails or response is invalid.\n */\n public function parseFinancialDocument(string $ocrText): array\n {\n if (empty($this->apiKey)) {\n throw new \\RuntimeException('DEEPSEEK_API_KEY belum dikonfigurasi.');\n }\n\n $systemPrompt = <<withHeaders([\n 'Authorization' => \"Bearer {$this->apiKey}\",\n 'Content-Type' => 'application/json',\n ])\n ->post($this->apiUrl, [\n 'model' => 'deepseek-v4-flash',\n 'messages' => [\n ['role' => 'system', 'content' => $systemPrompt],\n ['role' => 'user', 'content' => \"Berikut teks OCR dari dokumen keuangan:\\n\\n{$ocrText}\"],\n ],\n 'response_format' => ['type' => 'json_object'],\n 'max_tokens' => 3000,\n 'temperature' => 0.1,\n ]);\n\n if ($response->failed()) {\n $errorBody = $response->body();\n Log::error('Deepseek API error', [\n 'status' => $response->status(),\n 'body' => $errorBody,\n ]);\n throw new \\RuntimeException(\"Deepseek API error: HTTP {$response->status()}\");\n }\n\n $content = $response->json('choices.0.message.content');\n\n if (empty($content)) {\n Log::error('Deepseek mengembalikan respons kosong', [\n 'finish_reason' => $response->json('choices.0.finish_reason'),\n 'reasoning_length' => strlen((string) $response->json('choices.0.message.reasoning_content')),\n 'usage' => $response->json('usage'),\n ]);\n throw new \\RuntimeException('Deepseek mengembalikan respons kosong.');\n }\n\n $parsed = json_decode($content, true);\n\n if (json_last_error() !== JSON_ERROR_NONE) {\n throw new \\RuntimeException('Respons Deepseek bukan JSON valid: ' . json_last_error_msg());\n }\n\n // Validate required fields\n $required = ['type', 'amount', 'description'];\n foreach ($required as $field) {\n if (!isset($parsed[$field])) {\n throw new \\RuntimeException(\"Field '{$field}' tidak ditemukan dalam respons.\");\n }\n }\n\n // Normalize values\n $parsed['type'] = in_array($parsed['type'], ['income', 'expense']) ? $parsed['type'] : 'expense';\n $parsed['amount'] = abs(floatval($parsed['amount']));\n $parsed['date'] = $parsed['date'] ?? now()->format('Y-m-d');\n\n Log::info('Deepseek parsing berhasil', [\n 'type' => $parsed['type'],\n 'amount' => $parsed['amount'],\n ]);\n\n return $parsed;\n } catch (\\Illuminate\\Http\\Client\\ConnectionException $e) {\n Log::error('Deepseek connection error', ['error' => $e->getMessage()]);\n throw new \\RuntimeException('Tidak dapat terhubung ke Deepseek API.');\n }\n }\n}\n"}}}}