successPaginated( $this->service->list($this->scope($request), $request->validated(), $request->user(), $this->tenantId($request)), CustomerResource::class, 'Daftar customer berhasil diambil' ); } public function options(Request $request) { return $this->success( $this->service->options($request->user(), $this->tenantId($request)), 'Pilihan relasi customer berhasil diambil' ); } public function odpPorts(Request $request, int $node) { $data=$request->validate(['customer_id'=>['nullable','integer','exists:customers,id']]); return $this->success($this->service->availableOdpPorts($node,$request->user(),$this->tenantId($request),$data['customer_id'] ?? null),'Port ODP tersedia berhasil diambil'); } public function ocrKtp(Request $request) { $data = $request->validate([ 'image' => ['required', 'image', 'mimes:jpeg,jpg,png,webp', 'max:5120'], 'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'], ]); $tenantId = $request->user()->isMasterAdmin() ? ($data['tenant_id'] ?? $request->attributes->get('tenant_id')) : $request->attributes->get('tenant_id'); abort_unless($tenantId, 422, 'Tenant wajib dipilih sebelum menjalankan OCR.'); if (! $request->user()->isMasterAdmin()) { abort_unless($request->user()->userTenants()->where('tenant_id', $tenantId)->exists(), 403); } $setting = \App\Models\TenantInstallationSetting::where('tenant_id', $tenantId)->first(); abort_unless($setting?->ktp_ocr_enabled, 422, 'OCR KTP belum diaktifkan untuk tenant ini.'); abort_unless($setting->ocr_provider && $setting->ocr_api_key, 422, 'Provider atau API key OCR belum dikonfigurasi.'); $file = $data['image']; $mime = $file->getMimeType(); $base64 = base64_encode(file_get_contents($file->getRealPath())); $prompt = 'Baca KTP Indonesia ini. Kembalikan hanya JSON valid tanpa markdown dengan field: nik, nama, alamat, rt_rw, kelurahan_desa, kecamatan, kabupaten_kota, provinsi. Gunakan null bila tidak terbaca.'; if ($setting->ocr_provider === 'openai') { $response = Http::withToken($setting->ocr_api_key)->timeout(60)->post('https://api.openai.com/v1/chat/completions', [ 'model' => 'gpt-4.1-mini', 'response_format' => ['type' => 'json_object'], 'messages' => [['role' => 'user', 'content' => [ ['type' => 'text', 'text' => $prompt], ['type' => 'image_url', 'image_url' => ['url' => "data:{$mime};base64,{$base64}"]], ]]], ]); $content = $response->successful() ? $response->json('choices.0.message.content') : null; } else { $response = Http::timeout(60)->post('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='.urlencode($setting->ocr_api_key), [ 'contents' => [['parts' => [['text' => $prompt], ['inline_data' => ['mime_type' => $mime, 'data' => $base64]]]]], 'generationConfig' => ['responseMimeType' => 'application/json'], ]); $content = $response->successful() ? $response->json('candidates.0.content.parts.0.text') : null; } if (! $response->successful() || ! $content) { throw ValidationException::withMessages(['image' => 'Provider AI gagal membaca KTP. Periksa API key, kuota, dan kualitas gambar.']); } $result = json_decode(trim(str_replace(['```json', '```'], '', $content)), true); if (! is_array($result)) throw ValidationException::withMessages(['image' => 'Respons OCR tidak dapat diproses.']); return $this->success($result, 'KTP berhasil dibaca'); } public function store(StoreCustomerRequest $request) { return $this->created( new CustomerResource($this->service->create($request->validated(), $request->user(), $this->tenantId($request))), 'Customer berhasil didaftarkan' ); } public function show(Request $request, int $id) { $customer = $this->service->find($id, $this->scope($request) === 'trash'); $this->authorizeTenant($request, $customer->tenant_id); return $this->success(new CustomerResource($customer), 'Detail customer berhasil diambil'); } public function update(UpdateCustomerRequest $request, int $id) { $customer = $this->service->find($id); $this->authorizeTenant($request, $customer->tenant_id); return $this->updated( new CustomerResource($this->service->update($customer, $request->validated(), $request->user())), 'Data customer berhasil diperbarui' ); } public function prepareActivation(UpdateCustomerRequest $request, int $id) { $customer = $this->service->find($id); $this->authorizeTenant($request, $customer->tenant_id); $mode = \App\Models\TenantInstallationSetting::firstOrCreate( ['tenant_id' => $customer->tenant_id], ['activation_mode' => 'list_order'] )->activation_mode; abort_unless($mode === 'list_order', 422, 'Tenant mengatur persiapan aktivasi melalui Ticket.'); $customer = $this->service->update($customer, $request->validated(), $request->user()); $customer = $this->service->ensureActivationCode($customer); return $this->updated( new CustomerResource($customer), 'Persiapan aktivasi berhasil disimpan' ); } public function updateRegistration(UpdateCustomerRequest $request, int $id) { $customer = $this->service->find($id); $this->authorizeTenant($request, $customer->tenant_id); return $this->updated( new CustomerResource($this->service->update($customer, $request->validated(), $request->user())), 'Data registrasi berhasil disimpan' ); } public function activate(ActivateCustomerRequest $request, int $id) { $customer = $this->service->find($id); $this->authorizeTenant($request, $customer->tenant_id); return $this->updated(new CustomerResource($this->service->activate($customer, $request->validated(), $request->user())), 'Customer berhasil diaktifkan'); } public function deactivate(Request $request, int $id) { $customer = $this->service->find($id); $this->authorizeTenant($request, $customer->tenant_id); return $this->updated(new CustomerResource($this->service->deactivate($customer)), 'Customer berhasil dinonaktifkan'); } public function destroy(Request $request, int $id) { $customer = $this->service->find($id); $this->authorizeTenant($request, $customer->tenant_id); $this->service->trash($customer); return $this->deleted('Customer dipindahkan ke Sampah'); } public function cancelOrder(Request $request, int $id) { $customer = $this->service->find($id); $this->authorizeTenant($request, $customer->tenant_id); abort_unless($customer->status === 'order', 422, 'Hanya customer dalam List Order yang dapat dihapus melalui aksi ini.'); $this->service->trash($customer); return $this->deleted('List Order dipindahkan ke Sampah'); } public function restore(Request $request, int $id) { $customer = $this->service->find($id, true); $this->authorizeTenant($request, $customer->tenant_id); return $this->updated(new CustomerResource($this->service->restore($customer)), 'Customer berhasil dipulihkan'); } public function forceDelete(Request $request, int $id) { $customer = $this->service->find($id, true); $this->authorizeTenant($request, $customer->tenant_id); $this->service->forceDelete($customer); return $this->deleted('Customer dihapus permanen'); } private function scope(Request $request): string { return $request->route()->defaults['customer_scope'] ?? 'orders'; } private function tenantId(Request $request): ?int { return $request->attributes->get('tenant_id'); } private function authorizeTenant(Request $request, int $tenantId): void { abort_unless($request->user()->isMasterAdmin() || $tenantId === $this->tenantId($request), 403); } }