{ "type": "file", "name": "class-billcom-api.php", "path": "includes/class-billcom-api.php", "size": 9938, "sha": "9e2edbd64131533494cc64214d96212542f722ab", "content": "dev_key = get_option('aos_hub_billcom_dev_key', '');\n $this->username = get_option('aos_hub_billcom_username', '');\n $this->password = get_option('aos_hub_billcom_password', '');\n $this->org_id = get_option('aos_hub_billcom_org_id', '');\n $this->environment = get_option('aos_hub_billcom_environment', 'sandbox');\n }\n\n /**\n * Check if API is configured.\n */\n public function is_configured(): bool {\n return !empty($this->dev_key) && !empty($this->username) && !empty($this->password) && !empty($this->org_id);\n }\n\n /**\n * Get the base URL for the configured environment.\n */\n private function get_base_url(): string {\n return $this->environment === 'production' ? self::PRODUCTION_URL : self::SANDBOX_URL;\n }\n\n /**\n * Login and get session ID. Caches in transient for 30 minutes.\n * V2 API: POST to /Login.json with form-encoded data.\n */\n public function login(bool $force = false): array {\n if (!$force) {\n $cached = get_transient('aos_hub_billcom_session');\n if ($cached) {\n $this->session_id = $cached;\n return array('success' => true, 'sessionId' => $cached);\n }\n }\n\n $form_data = array(\n 'devKey' => $this->dev_key,\n 'userName' => $this->username,\n 'password' => $this->password,\n 'orgId' => $this->org_id,\n );\n\n // Include MFA remember-me ID if available\n $mfa_id = get_option('aos_hub_billcom_mfa_remember_id', '');\n $device_id = get_option('aos_hub_billcom_mfa_device_id', '');\n if (!empty($mfa_id) && !empty($device_id)) {\n $form_data['mfaId'] = $mfa_id;\n $form_data['deviceId'] = $device_id;\n }\n\n $response = wp_remote_post($this->get_base_url() . '/Login.json', array(\n 'headers' => array(\n 'Content-Type' => 'application/x-www-form-urlencoded',\n 'Accept' => 'application/json',\n ),\n 'body' => $form_data,\n 'timeout' => 30,\n ));\n\n if (is_wp_error($response)) {\n return array('success' => false, 'error' => $response->get_error_message());\n }\n\n $code = wp_remote_retrieve_response_code($response);\n $data = json_decode(wp_remote_retrieve_body($response), true);\n\n // V2 response format: response_status 0 = success, 1 = error\n if (isset($data['response_status']) && $data['response_status'] === 0 && !empty($data['response_data']['sessionId'])) {\n $this->session_id = $data['response_data']['sessionId'];\n set_transient('aos_hub_billcom_session', $data['response_data']['sessionId'], 30 * MINUTE_IN_SECONDS);\n return array(\n 'success' => true,\n 'sessionId' => $data['response_data']['sessionId'],\n 'userId' => $data['response_data']['usersId'] ?? '',\n );\n }\n\n // Check for MFA challenge\n $error_msg = $data['response_data']['error_message'] ?? 'Login failed';\n $error_code = $data['response_data']['error_code'] ?? '';\n\n if (strpos($error_code, 'MFA') !== false || strpos($error_msg, 'MFA') !== false) {\n return array('success' => false, 'mfa_required' => true, 'error' => 'MFA challenge required. Please complete MFA setup in Bill.com Settings.');\n }\n\n return array('success' => false, 'error' => $error_msg, 'http_code' => $code);\n }\n\n /**\n * Make an authenticated V2 API request.\n * V2 API: All requests are POST with form-encoded data (devKey, sessionId, data={json}).\n */\n private function request(string $endpoint, array $data = array()): array {\n // Ensure we have a session\n if (empty($this->session_id)) {\n $login = $this->login();\n if (!$login['success']) {\n return $login;\n }\n }\n\n $url = $this->get_base_url() . $endpoint;\n $form_data = array(\n 'devKey' => $this->dev_key,\n 'sessionId' => $this->session_id,\n );\n\n if (!empty($data)) {\n $form_data['data'] = wp_json_encode($data);\n }\n\n $response = wp_remote_post($url, array(\n 'headers' => array(\n 'Content-Type' => 'application/x-www-form-urlencoded',\n 'Accept' => 'application/json',\n ),\n 'body' => $form_data,\n 'timeout' => 30,\n ));\n\n if (is_wp_error($response)) {\n return array('success' => false, 'error' => $response->get_error_message());\n }\n\n $code = wp_remote_retrieve_response_code($response);\n $result = json_decode(wp_remote_retrieve_body($response), true);\n\n // Session expired — retry once\n if (isset($result['response_status']) && $result['response_status'] === 1) {\n $error_code = $result['response_data']['error_code'] ?? '';\n if ($error_code === 'BDC_1109' || strpos($error_code, 'session') !== false) {\n delete_transient('aos_hub_billcom_session');\n $this->session_id = null;\n $login = $this->login(true);\n if (!$login['success']) {\n return $login;\n }\n $form_data['sessionId'] = $this->session_id;\n $response = wp_remote_post($url, array(\n 'headers' => array(\n 'Content-Type' => 'application/x-www-form-urlencoded',\n 'Accept' => 'application/json',\n ),\n 'body' => $form_data,\n 'timeout' => 30,\n ));\n if (is_wp_error($response)) {\n return array('success' => false, 'error' => $response->get_error_message());\n }\n $result = json_decode(wp_remote_retrieve_body($response), true);\n }\n }\n\n // V2 success\n if (isset($result['response_status']) && $result['response_status'] === 0) {\n return array('success' => true, 'data' => $result['response_data']);\n }\n\n $error_msg = $result['response_data']['error_message'] ?? \"HTTP $code\";\n return array('success' => false, 'error' => $error_msg);\n }\n\n /**\n * List all vendors (paginated).\n * V2 API: POST to /List/Vendor.json\n */\n public function list_vendors(): array {\n $all_vendors = array();\n $start = 0;\n $max = 999;\n $max_pages = 20; // Safety limit\n\n for ($i = 0; $i < $max_pages; $i++) {\n $data = array(\n 'start' => $start,\n 'max' => $max,\n 'filters' => array(\n array(\n 'field' => 'isActive',\n 'op' => '=',\n 'value' => '1',\n ),\n ),\n );\n\n $result = $this->request('/List/Vendor.json', $data);\n if (!$result['success']) {\n return $result;\n }\n\n if (!empty($result['data'])) {\n foreach ($result['data'] as $vendor) {\n if (!is_array($vendor) || empty($vendor['id'])) {\n continue;\n }\n $all_vendors[] = array(\n 'id' => $vendor['id'],\n 'name' => $vendor['name'] ?? '',\n 'shortName' => $vendor['shortName'] ?? '',\n 'email' => $vendor['email'] ?? '',\n );\n }\n }\n\n // If we got fewer than max, we've reached the end\n if (empty($result['data']) || count($result['data']) < $max) {\n break;\n }\n $start += $max;\n }\n\n return array('success' => true, 'vendors' => $all_vendors);\n }\n\n /**\n * Create a bill.\n * V2 API: POST to /Crud/Create/Bill.json\n *\n * @param string $vendor_id Bill.com vendor ID\n * @param string $due_date YYYY-MM-DD format\n * @param array $line_items Array of ['description' => string, 'amount' => float]\n * @param string $invoice_number Unique invoice reference\n * @param string $invoice_date YYYY-MM-DD format\n * @param string $description Bill description\n */\n public function create_bill(string $vendor_id, string $due_date, array $line_items, string $invoice_number, string $invoice_date, string $description = ''): array {\n $bill_line_items = array();\n foreach ($line_items as $item) {\n $bill_line_items[] = array(\n 'entity' => 'BillLineItem',\n 'amount' => round($item['amount'], 2),\n 'description' => $item['description'] ?? '',\n );\n }\n\n $data = array(\n 'obj' => array(\n 'entity' => 'Bill',\n 'isActive' => '1',\n 'vendorId' => $vendor_id,\n 'invoiceNumber' => $invoice_number,\n 'invoiceDate' => $invoice_date,\n 'dueDate' => $due_date,\n 'description' => $description,\n 'billLineItems' => $bill_line_items,\n ),\n );\n\n return $this->request('/Crud/Create/Bill.json', $data);\n }\n}\n", "encoding": "base64", "downloadUrl": "https://raw.githubusercontent.com/nate-jameson/aos-admin-hub/fix/billcom-prod-url/includes/class-billcom-api.php?token=BRRMIFABB6GZIOMBUNQZ2V3JYGGBS" }