Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
16 KiB
JSON
1 line
16 KiB
JSON
{"repo": "aryawiratama26/saveit-website", "files": {"tests/Unit/Services/DeepseekServiceTest.php": {"f9708ef0e90bfd9e437d22c02f2608a14916de27": {"t": 1785745775.9015539, "n": 3094, "h": "d9953e83eb51fad4", "c": "<?php\n\nnamespace Tests\\Unit\\Services;\n\nuse App\\Services\\DeepseekService;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Http;\nuse Tests\\TestCase;\n\nclass DeepseekServiceTest extends TestCase\n{\n public function test_throws_exception_when_api_key_is_missing(): void\n {\n Config::set('services.deepseek.api_key', '');\n\n $service = new DeepseekService();\n\n $this->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": "<?php\n\nnamespace Tests\\Unit\\Services;\n\nuse App\\Services\\DeepseekService;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Http;\nuse Tests\\TestCase;\n\nclass DeepseekServiceTest extends TestCase\n{\n public function test_throws_exception_when_api_key_is_missing(): void\n {\n Config::set('services.deepseek.api_key', '');\n\n $service = new DeepseekService();\n\n $this->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": "<?php\n\nnamespace Tests\\Unit\\Services;\n\nuse App\\Services\\DeepseekService;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Http;\nuse Tests\\TestCase;\n\nclass DeepseekServiceTest extends TestCase\n{\n public function test_throws_exception_when_api_key_is_missing(): void\n {\n Config::set('services.deepseek.api_key', '');\n\n $service = new DeepseekService();\n\n $this->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": "<?php\n\nnamespace App\\Services;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Facades\\Log;\n\nclass DeepseekService\n{\n private string $apiKey;\n private string $apiUrl;\n\n public function __construct()\n {\n $this->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 = <<<PROMPT\nKamu adalah asisten ekstraksi data keuangan. Dari teks OCR yang diberikan, ekstrak informasi transaksi keuangan ke dalam format JSON berikut:\n\n{\n \"type\": \"income\" atau \"expense\",\n \"amount\": angka (tanpa simbol mata uang, tanpa titik pemisah ribuan),\n \"description\": \"deskripsi singkat transaksi\",\n \"date\": \"YYYY-MM-DD\" (jika tidak ditemukan, gunakan tanggal hari ini),\n \"category_suggestion\": \"saran kategori, pilih salah satu: Gaji, Freelance, Investasi, Pemasukan Lainnya, Makanan, Transportasi, Belanja, Tagihan, Hiburan, Kesehatan, Pendidikan, Pengeluaran Lainnya\"\n}\n\n=== ATURAN PALING PENTING (WAJIB DIPATUHI) ===\n\nDETEKSI JENIS DOKUMEN TERLEBIH DAHULU:\n- Jika teks mengandung kata \"SLIP GAJI\", \"PAYSLIP\", \"GAJI POKOK\", \"TUNJANGAN\", \"POTONGAN\", \"GAJI BERSIH\", atau \"TAKE HOME PAY\" → dokumen ini adalah SLIP GAJI.\n- Jika teks mengandung kata \"STRUK\", \"NOTA\", \"KASIR\", \"BELANJA\", \"PEMBELIAN\" → dokumen ini adalah STRUK BELANJA.\n\nJIKA DOKUMEN ADALAH SLIP GAJI:\n- type = \"income\"\n- amount = HARUS angka dari \"GAJI BERSIH YANG DITERIMA\" atau \"TAKE HOME PAY\" atau \"GAJI BERSIH\" atau \"NETT SALARY\".\n- DILARANG KERAS menggunakan angka dari \"TOTAL PENGHASILAN\" atau \"GROSS SALARY\". Itu adalah gaji kotor, BUKAN gaji yang diterima karyawan.\n- Jika tidak ada label \"Gaji Bersih\" secara eksplisit, HITUNG: Total Penghasilan - Total Potongan.\n- category_suggestion = \"Gaji\"\n\nContoh untuk slip gaji:\nJika teks OCR mengandung \"TOTAL PENGHASILAN Rp 10.900.000\" dan \"TOTAL POTONGAN Rp 1.575.000\" dan \"GAJI BERSIH YANG DITERIMA Rp 9.325.000\", maka amount = 9325000 (BUKAN 10900000).\n\nJIKA DOKUMEN ADALAH STRUK BELANJA / NOTA:\n- type = \"expense\"\n- Cari kata kunci \"TOTAL\" / \"SUBTOTAL\" / \"TOTAL BAYAR\" untuk amount.\n- Jangan gunakan angka \"TUNAI\", \"KEMBALI\", atau \"POIN\".\n- Jika ada rincian item tetapi tidak ada angka TOTAL, jumlahkan total harga item-item tersebut.\n\nJIKA DOKUMEN LAINNYA:\n- Jika teks menunjukkan pembayaran masuk/transfer masuk, type = \"income\"\n- Jika teks menunjukkan pembayaran keluar/pembelian, type = \"expense\"\n\n=== ATURAN FORMAT ===\n- Amount harus berupa angka saja tanpa format (contoh: 9325000 bukan 9.325.000)\n- Hanya kembalikan JSON valid, tanpa teks tambahan apapun\nPROMPT;\n\n try {\n $response = Http::timeout(90)\n ->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"}}}} |