From 35007091740fc86bb1b61f6ce4dbbffa5a70c003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 00:47:50 +0100 Subject: [PATCH 01/12] feat: vtiger addon --- .../addons/vtiger/class-vtiger-addon.php | 193 +++++++++ .../vtiger/class-vtiger-form-bridge.php | 393 ++++++++++++++++++ forms-bridge/addons/vtiger/hooks.php | 163 ++++++++ .../addons/vtiger/templates/accounts.php | 343 +++++++++++++++ .../addons/vtiger/templates/contacts.php | 210 ++++++++++ .../addons/vtiger/templates/leads.php | 278 +++++++++++++ .../addons/vtiger/templates/potentials.php | 311 ++++++++++++++ 7 files changed, 1891 insertions(+) create mode 100644 forms-bridge/addons/vtiger/class-vtiger-addon.php create mode 100644 forms-bridge/addons/vtiger/class-vtiger-form-bridge.php create mode 100644 forms-bridge/addons/vtiger/hooks.php create mode 100644 forms-bridge/addons/vtiger/templates/accounts.php create mode 100644 forms-bridge/addons/vtiger/templates/contacts.php create mode 100644 forms-bridge/addons/vtiger/templates/leads.php create mode 100644 forms-bridge/addons/vtiger/templates/potentials.php diff --git a/forms-bridge/addons/vtiger/class-vtiger-addon.php b/forms-bridge/addons/vtiger/class-vtiger-addon.php new file mode 100644 index 0000000..afb3e59 --- /dev/null +++ b/forms-bridge/addons/vtiger/class-vtiger-addon.php @@ -0,0 +1,193 @@ + '__vtiger-' . time(), + 'method' => 'listtypes', + 'endpoint' => '', + 'backend' => $backend, + ) + ); + + $response = $bridge->submit(); + + if ( is_wp_error( $response ) ) { + Logger::log( 'Vtiger backend ping error response', Logger::ERROR ); + Logger::log( $response, Logger::ERROR ); + return false; + } + + return true; + } + + /** + * Performs a query request against the backend module and retrieve the response data. + * + * @param string $endpoint Target module name. + * @param string $backend Target backend name. + * + * @return array|WP_Error + */ + public function fetch( $endpoint, $backend ) { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => '__vtiger-' . time(), + 'method' => 'query', + 'endpoint' => $endpoint, + 'backend' => $backend, + ) + ); + + return $bridge->submit( + array( + 'query' => "SELECT id, label FROM {$endpoint} LIMIT 100;", + ) + ); + } + + /** + * Fetch available modules from the backend. + * + * @param Backend $backend HTTP backend object. + * + * @return array + */ + public function get_endpoints( $backend ) { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => '__vtiger-' . time(), + 'method' => 'listtypes', + 'endpoint' => '', + 'backend' => $backend, + ) + ); + + $response = $bridge->submit(); + + if ( is_wp_error( $response ) ) { + return array(); + } + + if ( ! isset( $response['data']['result']['types'] ) ) { + return array(); + } + + return $response['data']['result']['types']; + } + + /** + * Performs an introspection of the backend module and returns API fields + * and accepted content type. + * + * @param string $module Target module name. + * @param string $backend Target backend name. + * @param string|null $method API method. + * + * @return array List of fields and content type of the module. + */ + public function get_endpoint_schema( $module, $backend, $method = null ) { + if ( 'create' !== $method ) { + return array(); + } + + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => '__vtiger-' . time(), + 'method' => 'describe', + 'endpoint' => $module, + 'backend' => $backend, + ) + ); + + $response = $bridge->submit(); + + if ( is_wp_error( $response ) ) { + return array(); + } + + if ( ! isset( $response['data']['result']['fields'] ) ) { + return array(); + } + + $fields = array(); + foreach ( $response['data']['result']['fields'] as $spec ) { + if ( ! $spec['editable'] ) { + continue; + } + + $type = 'string'; + + if ( in_array( $spec['type']['name'], array( 'integer', 'autogenerated' ), true ) ) { + $type = 'integer'; + } elseif ( in_array( $spec['type']['name'], array( 'double', 'currency' ), true ) ) { + $type = 'number'; + } elseif ( 'boolean' === $spec['type']['name'] ) { + $type = 'boolean'; + } + + $schema = array( + 'type' => $type, + 'required' => $spec['mandatory'], + ); + + $fields[] = array( + 'name' => $spec['name'], + 'schema' => $schema, + ); + } + + return $fields; + } +} + +Vtiger_Addon::setup(); diff --git a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php new file mode 100644 index 0000000..af435bd --- /dev/null +++ b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php @@ -0,0 +1,393 @@ + $res ); + if ( self::$request ) { + $error_data['request'] = self::$request; + } + + $error->add_data( $error_data ); + return $error; + } + + return $data; + } + + /** + * Get challenge token for authentication. + * + * @param string $username Username for challenge. + * @param Backend $backend Bridge backend object. + * + * @return string|WP_Error Challenge token on success. + */ + private static function get_challenge( $username, $backend ) { + $url = self::ENDPOINT . '?' . http_build_query( + array( + 'operation' => 'getchallenge', + 'username' => $username, + ) + ); + + $response = $backend->get( $url ); + $result = self::rest_response( $response ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + if ( empty( $result['result']['token'] ) ) { + return new WP_Error( + 'challenge_failed', + __( 'Vtiger challenge failed: No token returned', 'forms-bridge' ), + $result + ); + } + + return $result['result']['token']; + } + + /** + * Login to Vtiger and get session name. + * + * @param array $credentials Credentials array with client_id (username) and client_secret (access key). + * @param Backend $backend Bridge backend object. + * + * @return string|WP_Error Session name on success. + */ + private static function rest_login( $credentials, $backend ) { + if ( self::$session_name ) { + return self::$session_name; + } + + $username = $credentials[0] ?? ''; + $access_key = $credentials[1] ?? ''; + + // Step 1: Get challenge token. + $token = self::get_challenge( $username, $backend ); + + if ( is_wp_error( $token ) ) { + return $token; + } + + // Step 2: Login with MD5(token + accessKey). + $access_key_hash = md5( $token . $access_key ); + + $payload = array( + 'operation' => 'login', + 'username' => $username, + 'accessKey' => $access_key_hash, + ); + + $response = $backend->post( self::ENDPOINT, $payload, 'application/x-www-form-urlencoded' ); + $result = self::rest_response( $response ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + if ( empty( $result['result']['sessionName'] ) ) { + return new WP_Error( + 'login_failed', + __( 'Vtiger login failed: No session returned', 'forms-bridge' ), + $result + ); + } + + self::$session_name = $result['result']['sessionName']; + self::$user_id = $result['result']['userId'] ?? ''; + + return self::$session_name; + } + + /** + * Bridge constructor with addon name provisioning. + * + * @param array $data Bridge data. + */ + public function __construct( $data ) { + parent::__construct( $data, 'vtiger' ); + } + + /** + * Submits submission to the backend. + * + * @param array $payload Submission data. + * @param array $more_args Additional arguments. + * + * @return array|WP_Error HTTP response. + */ + public function submit( $payload = array(), $more_args = array() ) { + if ( ! $this->is_valid ) { + return new WP_Error( + 'invalid_bridge', + 'Bridge data is invalid', + (array) $this->data + ); + } + + $backend = $this->backend(); + + if ( ! $backend ) { + return new WP_Error( + 'invalid_backend', + 'The bridge does not have a valid backend' + ); + } + + $credential = $backend->credential; + if ( ! $credential ) { + return new WP_Error( + 'invalid_credential', + 'The bridge does not have a valid credential' + ); + } + + add_filter( + 'http_bridge_request', + static function ( $request ) { + self::$request = $request; + return $request; + }, + 10, + 1 + ); + + // Get credentials for login. + $login_credentials = $credential->authorization(); + + // Login to get session. + $session_name = self::rest_login( $login_credentials, $backend ); + + if ( is_wp_error( $session_name ) ) { + return $session_name; + } + + // Build and execute the API request. + return $this->execute_operation( $session_name, $payload, $more_args, $backend ); + } + + /** + * Execute a Vtiger webservice operation. + * + * @param string $session_name Session name. + * @param array $payload Form submission payload. + * @param array $more_args Additional arguments. + * @param Backend $backend Backend object. + * + * @return array|WP_Error HTTP response. + */ + private function execute_operation( $session_name, $payload, $more_args, $backend ) { + $module = $this->endpoint; + + switch ( $this->method ) { + case 'listtypes': + $url = self::ENDPOINT . '?' . http_build_query( + array( + 'operation' => 'listtypes', + 'sessionName' => $session_name, + ) + ); + $response = $backend->get( $url ); + break; + + case 'describe': + $url = self::ENDPOINT . '?' . http_build_query( + array( + 'operation' => 'describe', + 'sessionName' => $session_name, + 'elementType' => $module, + ) + ); + $response = $backend->get( $url ); + break; + + case 'query': + $query = $more_args['query'] ?? $payload['query'] ?? "SELECT * FROM {$module} LIMIT 20;"; + $url = self::ENDPOINT . '?' . http_build_query( + array( + 'operation' => 'query', + 'sessionName' => $session_name, + 'query' => $query, + ) + ); + $response = $backend->get( $url ); + break; + + case 'retrieve': + $url = self::ENDPOINT . '?' . http_build_query( + array( + 'operation' => 'retrieve', + 'sessionName' => $session_name, + 'id' => $payload['id'] ?? '', + ) + ); + $response = $backend->get( $url ); + break; + + case 'create': + // Add module type to element data. + $element = array_merge( + $payload, + array( 'assigned_user_id' => $payload['assigned_user_id'] ?? self::$user_id ) + ); + + $post_data = array( + 'operation' => 'create', + 'sessionName' => $session_name, + 'elementType' => $module, + 'element' => wp_json_encode( $element ), + ); + + $response = $backend->post( self::ENDPOINT, $post_data, 'application/x-www-form-urlencoded' ); + break; + + case 'update': + $post_data = array( + 'operation' => 'update', + 'sessionName' => $session_name, + 'element' => wp_json_encode( $payload ), + ); + + $response = $backend->post( self::ENDPOINT, $post_data, 'application/x-www-form-urlencoded' ); + break; + + case 'delete': + $post_data = array( + 'operation' => 'delete', + 'sessionName' => $session_name, + 'id' => $payload['id'] ?? '', + ); + + $response = $backend->post( self::ENDPOINT, $post_data, 'application/x-www-form-urlencoded' ); + break; + + case 'sync': + $url = self::ENDPOINT . '?' . http_build_query( + array( + 'operation' => 'sync', + 'sessionName' => $session_name, + 'elementType' => $module, + 'modifiedTime' => $more_args['modifiedTime'] ?? 0, + ) + ); + $response = $backend->get( $url ); + break; + + default: + // For custom operations, try as GET first. + $params = array_merge( + array( + 'operation' => $this->method, + 'sessionName' => $session_name, + ), + $payload + ); + + if ( ! empty( $module ) ) { + $params['elementType'] = $module; + } + + $url = self::ENDPOINT . '?' . http_build_query( $params ); + $response = $backend->get( $url ); + break; + } + + $result = self::rest_response( $response ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + // Normalize response. + $response['data'] = $result; + + return $response; + } +} diff --git a/forms-bridge/addons/vtiger/hooks.php b/forms-bridge/addons/vtiger/hooks.php new file mode 100644 index 0000000..22bc2c1 --- /dev/null +++ b/forms-bridge/addons/vtiger/hooks.php @@ -0,0 +1,163 @@ + array( + array( + 'ref' => '#credential', + 'name' => 'name', + 'label' => __( 'Name', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#credential', + 'name' => 'schema', + 'type' => 'text', + 'value' => 'Basic', + ), + array( + 'ref' => '#credential', + 'name' => 'client_id', + 'label' => __( 'Username', 'forms-bridge' ), + 'description' => __( + 'Vtiger user name', + 'forms-bridge' + ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#credential', + 'name' => 'client_secret', + 'description' => __( + 'Access Key from My Preferences in Vtiger', + 'forms-bridge' + ), + 'label' => __( 'Access Key', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#backend', + 'name' => 'base_url', + 'label' => __( 'Vtiger URL', 'forms-bridge' ), + 'description' => __( + 'Base URL of your Vtiger installation (e.g., https://crm.example.com)', + 'forms-bridge' + ), + 'type' => 'url', + 'required' => true, + ), + array( + 'ref' => '#backend', + 'name' => 'name', + 'default' => 'Vtiger', + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'label' => __( 'Module', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#bridge', + 'name' => 'method', + 'label' => __( 'Operation', 'forms-bridge' ), + 'type' => 'text', + 'value' => 'create', + 'required' => true, + ), + ), + 'bridge' => array( + 'name' => '', + 'form_id' => '', + 'backend' => '', + 'endpoint' => '', + 'method' => 'create', + ), + 'backend' => array( + 'name' => 'Vtiger', + 'headers' => array( + array( + 'name' => 'Accept', + 'value' => 'application/json', + ), + ), + ), + 'credential' => array( + 'name' => '', + 'schema' => 'Basic', + 'client_id' => '', + 'client_secret' => '', + ), + ), + $defaults, + $schema + ); + }, + 10, + 3 +); diff --git a/forms-bridge/addons/vtiger/templates/accounts.php b/forms-bridge/addons/vtiger/templates/accounts.php new file mode 100644 index 0000000..b7714bd --- /dev/null +++ b/forms-bridge/addons/vtiger/templates/accounts.php @@ -0,0 +1,343 @@ + __( 'Accounts', 'forms-bridge' ), + 'description' => __( + 'Account form template. The resulting bridge will convert form submissions into Vtiger accounts (organizations).', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Accounts', 'forms-bridge' ), + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => 'Accounts', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'description' => __( + 'User to assign the account to', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'result.[].id', + 'label' => 'result.[].label', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'accounttype', + 'label' => __( 'Account Type', 'forms-bridge' ), + 'description' => __( + 'Type of account', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'Analyst', + 'label' => __( 'Analyst', 'forms-bridge' ), + ), + array( + 'value' => 'Competitor', + 'label' => __( 'Competitor', 'forms-bridge' ), + ), + array( + 'value' => 'Customer', + 'label' => __( 'Customer', 'forms-bridge' ), + ), + array( + 'value' => 'Integrator', + 'label' => __( 'Integrator', 'forms-bridge' ), + ), + array( + 'value' => 'Investor', + 'label' => __( 'Investor', 'forms-bridge' ), + ), + array( + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), + ), + array( + 'value' => 'Press', + 'label' => __( 'Press', 'forms-bridge' ), + ), + array( + 'value' => 'Prospect', + 'label' => __( 'Prospect', 'forms-bridge' ), + ), + array( + 'value' => 'Reseller', + 'label' => __( 'Reseller', 'forms-bridge' ), + ), + array( + 'value' => 'Other', + 'label' => __( 'Other', 'forms-bridge' ), + ), + ), + 'default' => 'Prospect', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'industry', + 'label' => __( 'Industry', 'forms-bridge' ), + 'description' => __( + 'Industry sector', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'Apparel', + 'label' => __( 'Apparel', 'forms-bridge' ), + ), + array( + 'value' => 'Banking', + 'label' => __( 'Banking', 'forms-bridge' ), + ), + array( + 'value' => 'Biotechnology', + 'label' => __( 'Biotechnology', 'forms-bridge' ), + ), + array( + 'value' => 'Chemicals', + 'label' => __( 'Chemicals', 'forms-bridge' ), + ), + array( + 'value' => 'Communications', + 'label' => __( 'Communications', 'forms-bridge' ), + ), + array( + 'value' => 'Construction', + 'label' => __( 'Construction', 'forms-bridge' ), + ), + array( + 'value' => 'Consulting', + 'label' => __( 'Consulting', 'forms-bridge' ), + ), + array( + 'value' => 'Education', + 'label' => __( 'Education', 'forms-bridge' ), + ), + array( + 'value' => 'Electronics', + 'label' => __( 'Electronics', 'forms-bridge' ), + ), + array( + 'value' => 'Energy', + 'label' => __( 'Energy', 'forms-bridge' ), + ), + array( + 'value' => 'Engineering', + 'label' => __( 'Engineering', 'forms-bridge' ), + ), + array( + 'value' => 'Entertainment', + 'label' => __( 'Entertainment', 'forms-bridge' ), + ), + array( + 'value' => 'Finance', + 'label' => __( 'Finance', 'forms-bridge' ), + ), + array( + 'value' => 'Government', + 'label' => __( 'Government', 'forms-bridge' ), + ), + array( + 'value' => 'Healthcare', + 'label' => __( 'Healthcare', 'forms-bridge' ), + ), + array( + 'value' => 'Hospitality', + 'label' => __( 'Hospitality', 'forms-bridge' ), + ), + array( + 'value' => 'Insurance', + 'label' => __( 'Insurance', 'forms-bridge' ), + ), + array( + 'value' => 'Manufacturing', + 'label' => __( 'Manufacturing', 'forms-bridge' ), + ), + array( + 'value' => 'Media', + 'label' => __( 'Media', 'forms-bridge' ), + ), + array( + 'value' => 'Retail', + 'label' => __( 'Retail', 'forms-bridge' ), + ), + array( + 'value' => 'Technology', + 'label' => __( 'Technology', 'forms-bridge' ), + ), + array( + 'value' => 'Transportation', + 'label' => __( 'Transportation', 'forms-bridge' ), + ), + array( + 'value' => 'Other', + 'label' => __( 'Other', 'forms-bridge' ), + ), + ), + ), + ), + 'bridge' => array( + 'endpoint' => 'Accounts', + 'method' => 'create', + 'custom_fields' => array( + array( + 'name' => 'accounttype', + 'value' => 'Prospect', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'accountname', + 'to' => 'accountname', + 'cast' => 'string', + ), + array( + 'from' => '?email', + 'to' => 'email1', + 'cast' => 'string', + ), + array( + 'from' => '?phone', + 'to' => 'phone', + 'cast' => 'string', + ), + array( + 'from' => '?fax', + 'to' => 'fax', + 'cast' => 'string', + ), + array( + 'from' => '?website', + 'to' => 'website', + 'cast' => 'string', + ), + array( + 'from' => '?description', + 'to' => 'description', + 'cast' => 'string', + ), + array( + 'from' => '?employees', + 'to' => 'employees', + 'cast' => 'integer', + ), + array( + 'from' => '?annual_revenue', + 'to' => 'annual_revenue', + 'cast' => 'number', + ), + array( + 'from' => '?address', + 'to' => 'bill_street', + 'cast' => 'string', + ), + array( + 'from' => '?city', + 'to' => 'bill_city', + 'cast' => 'string', + ), + array( + 'from' => '?state', + 'to' => 'bill_state', + 'cast' => 'string', + ), + array( + 'from' => '?postal_code', + 'to' => 'bill_code', + 'cast' => 'string', + ), + array( + 'from' => '?country', + 'to' => 'bill_country', + 'cast' => 'string', + ), + array( + 'from' => '?accounttype', + 'to' => 'accounttype', + 'cast' => 'string', + ), + array( + 'from' => '?industry', + 'to' => 'industry', + 'cast' => 'string', + ), + array( + 'from' => '?assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + ), + ), + ), + 'form' => array( + 'fields' => array( + array( + 'label' => __( 'Account Name', 'forms-bridge' ), + 'name' => 'accountname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Email', 'forms-bridge' ), + 'name' => 'email', + 'type' => 'email', + ), + array( + 'label' => __( 'Phone', 'forms-bridge' ), + 'name' => 'phone', + 'type' => 'tel', + ), + array( + 'label' => __( 'Website', 'forms-bridge' ), + 'name' => 'website', + 'type' => 'url', + ), + array( + 'label' => __( 'Address', 'forms-bridge' ), + 'name' => 'address', + 'type' => 'text', + ), + array( + 'label' => __( 'City', 'forms-bridge' ), + 'name' => 'city', + 'type' => 'text', + ), + array( + 'label' => __( 'Country', 'forms-bridge' ), + 'name' => 'country', + 'type' => 'text', + ), + array( + 'label' => __( 'Description', 'forms-bridge' ), + 'name' => 'description', + 'type' => 'textarea', + ), + ), + ), +); diff --git a/forms-bridge/addons/vtiger/templates/contacts.php b/forms-bridge/addons/vtiger/templates/contacts.php new file mode 100644 index 0000000..ea1ebe3 --- /dev/null +++ b/forms-bridge/addons/vtiger/templates/contacts.php @@ -0,0 +1,210 @@ + __( 'Contacts', 'forms-bridge' ), + 'description' => __( + 'Contact form template. The resulting bridge will convert form submissions into Vtiger contacts.', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Contacts', 'forms-bridge' ), + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => 'Contacts', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'description' => __( + 'User to assign the contact to', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'result.[].id', + 'label' => 'result.[].label', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'leadsource', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'description' => __( + 'Source of the contact', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'Web Site', + 'label' => __( 'Web Site', 'forms-bridge' ), + ), + array( + 'value' => 'Cold Call', + 'label' => __( 'Cold Call', 'forms-bridge' ), + ), + array( + 'value' => 'Email', + 'label' => __( 'Email', 'forms-bridge' ), + ), + array( + 'value' => 'Word of mouth', + 'label' => __( 'Word of Mouth', 'forms-bridge' ), + ), + array( + 'value' => 'Campaign', + 'label' => __( 'Campaign', 'forms-bridge' ), + ), + array( + 'value' => 'Other', + 'label' => __( 'Other', 'forms-bridge' ), + ), + ), + 'default' => 'Web Site', + ), + ), + 'bridge' => array( + 'endpoint' => 'Contacts', + 'method' => 'create', + 'custom_fields' => array( + array( + 'name' => 'leadsource', + 'value' => 'Web Site', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'firstname', + 'to' => 'firstname', + 'cast' => 'string', + ), + array( + 'from' => 'lastname', + 'to' => 'lastname', + 'cast' => 'string', + ), + array( + 'from' => 'email', + 'to' => 'email', + 'cast' => 'string', + ), + array( + 'from' => '?phone', + 'to' => 'phone', + 'cast' => 'string', + ), + array( + 'from' => '?mobile', + 'to' => 'mobile', + 'cast' => 'string', + ), + array( + 'from' => '?title', + 'to' => 'title', + 'cast' => 'string', + ), + array( + 'from' => '?department', + 'to' => 'department', + 'cast' => 'string', + ), + array( + 'from' => '?description', + 'to' => 'description', + 'cast' => 'string', + ), + array( + 'from' => '?address', + 'to' => 'mailingstreet', + 'cast' => 'string', + ), + array( + 'from' => '?city', + 'to' => 'mailingcity', + 'cast' => 'string', + ), + array( + 'from' => '?state', + 'to' => 'mailingstate', + 'cast' => 'string', + ), + array( + 'from' => '?postal_code', + 'to' => 'mailingzip', + 'cast' => 'string', + ), + array( + 'from' => '?country', + 'to' => 'mailingcountry', + 'cast' => 'string', + ), + array( + 'from' => '?leadsource', + 'to' => 'leadsource', + 'cast' => 'string', + ), + array( + 'from' => '?assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + ), + ), + ), + 'form' => array( + 'fields' => array( + array( + 'label' => __( 'First Name', 'forms-bridge' ), + 'name' => 'firstname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Last Name', 'forms-bridge' ), + 'name' => 'lastname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Email', 'forms-bridge' ), + 'name' => 'email', + 'type' => 'email', + 'required' => true, + ), + array( + 'label' => __( 'Phone', 'forms-bridge' ), + 'name' => 'phone', + 'type' => 'tel', + ), + array( + 'label' => __( 'Title', 'forms-bridge' ), + 'name' => 'title', + 'type' => 'text', + ), + array( + 'label' => __( 'Description', 'forms-bridge' ), + 'name' => 'description', + 'type' => 'textarea', + ), + ), + ), +); diff --git a/forms-bridge/addons/vtiger/templates/leads.php b/forms-bridge/addons/vtiger/templates/leads.php new file mode 100644 index 0000000..71ba711 --- /dev/null +++ b/forms-bridge/addons/vtiger/templates/leads.php @@ -0,0 +1,278 @@ + __( 'Leads', 'forms-bridge' ), + 'description' => __( + 'Lead capture form template. The resulting bridge will convert form submissions into Vtiger leads.', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Leads', 'forms-bridge' ), + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => 'Leads', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'description' => __( + 'User to assign the lead to', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'result.[].id', + 'label' => 'result.[].label', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'leadstatus', + 'label' => __( 'Lead Status', 'forms-bridge' ), + 'description' => __( + 'Initial status of the lead', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'New', + 'label' => __( 'New', 'forms-bridge' ), + ), + array( + 'value' => 'Assigned', + 'label' => __( 'Assigned', 'forms-bridge' ), + ), + array( + 'value' => 'In Process', + 'label' => __( 'In Process', 'forms-bridge' ), + ), + array( + 'value' => 'Converted', + 'label' => __( 'Converted', 'forms-bridge' ), + ), + array( + 'value' => 'Recycled', + 'label' => __( 'Recycled', 'forms-bridge' ), + ), + array( + 'value' => 'Dead', + 'label' => __( 'Dead', 'forms-bridge' ), + ), + ), + 'default' => 'New', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'leadsource', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'description' => __( + 'Source of the lead', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'Web Site', + 'label' => __( 'Web Site', 'forms-bridge' ), + ), + array( + 'value' => 'Cold Call', + 'label' => __( 'Cold Call', 'forms-bridge' ), + ), + array( + 'value' => 'Email', + 'label' => __( 'Email', 'forms-bridge' ), + ), + array( + 'value' => 'Word of mouth', + 'label' => __( 'Word of Mouth', 'forms-bridge' ), + ), + array( + 'value' => 'Campaign', + 'label' => __( 'Campaign', 'forms-bridge' ), + ), + array( + 'value' => 'Conference', + 'label' => __( 'Conference', 'forms-bridge' ), + ), + array( + 'value' => 'Trade Show', + 'label' => __( 'Trade Show', 'forms-bridge' ), + ), + array( + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), + ), + array( + 'value' => 'Other', + 'label' => __( 'Other', 'forms-bridge' ), + ), + ), + 'default' => 'Web Site', + ), + ), + 'bridge' => array( + 'endpoint' => 'Leads', + 'method' => 'create', + 'custom_fields' => array( + array( + 'name' => 'leadstatus', + 'value' => 'New', + ), + array( + 'name' => 'leadsource', + 'value' => 'Web Site', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'firstname', + 'to' => 'firstname', + 'cast' => 'string', + ), + array( + 'from' => 'lastname', + 'to' => 'lastname', + 'cast' => 'string', + ), + array( + 'from' => 'email', + 'to' => 'email', + 'cast' => 'string', + ), + array( + 'from' => '?phone', + 'to' => 'phone', + 'cast' => 'string', + ), + array( + 'from' => '?mobile', + 'to' => 'mobile', + 'cast' => 'string', + ), + array( + 'from' => '?company', + 'to' => 'company', + 'cast' => 'string', + ), + array( + 'from' => '?designation', + 'to' => 'designation', + 'cast' => 'string', + ), + array( + 'from' => '?description', + 'to' => 'description', + 'cast' => 'string', + ), + array( + 'from' => '?website', + 'to' => 'website', + 'cast' => 'string', + ), + array( + 'from' => '?address', + 'to' => 'lane', + 'cast' => 'string', + ), + array( + 'from' => '?city', + 'to' => 'city', + 'cast' => 'string', + ), + array( + 'from' => '?state', + 'to' => 'state', + 'cast' => 'string', + ), + array( + 'from' => '?postal_code', + 'to' => 'code', + 'cast' => 'string', + ), + array( + 'from' => '?country', + 'to' => 'country', + 'cast' => 'string', + ), + array( + 'from' => '?leadstatus', + 'to' => 'leadstatus', + 'cast' => 'string', + ), + array( + 'from' => '?leadsource', + 'to' => 'leadsource', + 'cast' => 'string', + ), + array( + 'from' => '?assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + ), + ), + ), + 'form' => array( + 'fields' => array( + array( + 'label' => __( 'First Name', 'forms-bridge' ), + 'name' => 'firstname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Last Name', 'forms-bridge' ), + 'name' => 'lastname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Email', 'forms-bridge' ), + 'name' => 'email', + 'type' => 'email', + 'required' => true, + ), + array( + 'label' => __( 'Phone', 'forms-bridge' ), + 'name' => 'phone', + 'type' => 'tel', + ), + array( + 'label' => __( 'Company', 'forms-bridge' ), + 'name' => 'company', + 'type' => 'text', + ), + array( + 'label' => __( 'Website', 'forms-bridge' ), + 'name' => 'website', + 'type' => 'url', + ), + array( + 'label' => __( 'Message', 'forms-bridge' ), + 'name' => 'description', + 'type' => 'textarea', + ), + ), + ), +); diff --git a/forms-bridge/addons/vtiger/templates/potentials.php b/forms-bridge/addons/vtiger/templates/potentials.php new file mode 100644 index 0000000..f417c8d --- /dev/null +++ b/forms-bridge/addons/vtiger/templates/potentials.php @@ -0,0 +1,311 @@ + __( 'Potentials', 'forms-bridge' ), + 'description' => __( + 'Potential (Opportunity) form template. The resulting bridge will convert form submissions into Vtiger potentials (sales opportunities).', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Potentials', 'forms-bridge' ), + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => 'Potentials', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'description' => __( + 'User to assign the potential to', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'result.[].id', + 'label' => 'result.[].label', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'related_to', + 'label' => __( 'Related Account', 'forms-bridge' ), + 'description' => __( + 'Related account for this potential', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Accounts', + 'finger' => array( + 'value' => 'result.[].id', + 'label' => 'result.[].label', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'sales_stage', + 'label' => __( 'Sales Stage', 'forms-bridge' ), + 'description' => __( + 'Current stage in the sales process', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'Prospecting', + 'label' => __( 'Prospecting', 'forms-bridge' ), + ), + array( + 'value' => 'Qualification', + 'label' => __( 'Qualification', 'forms-bridge' ), + ), + array( + 'value' => 'Needs Analysis', + 'label' => __( 'Needs Analysis', 'forms-bridge' ), + ), + array( + 'value' => 'Value Proposition', + 'label' => __( 'Value Proposition', 'forms-bridge' ), + ), + array( + 'value' => 'Id. Decision Makers', + 'label' => __( 'Identifying Decision Makers', 'forms-bridge' ), + ), + array( + 'value' => 'Perception Analysis', + 'label' => __( 'Perception Analysis', 'forms-bridge' ), + ), + array( + 'value' => 'Proposal/Price Quote', + 'label' => __( 'Proposal/Price Quote', 'forms-bridge' ), + ), + array( + 'value' => 'Negotiation/Review', + 'label' => __( 'Negotiation/Review', 'forms-bridge' ), + ), + array( + 'value' => 'Closed Won', + 'label' => __( 'Closed Won', 'forms-bridge' ), + ), + array( + 'value' => 'Closed Lost', + 'label' => __( 'Closed Lost', 'forms-bridge' ), + ), + ), + 'default' => 'Prospecting', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'leadsource', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'description' => __( + 'Source of the potential', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'Web Site', + 'label' => __( 'Web Site', 'forms-bridge' ), + ), + array( + 'value' => 'Cold Call', + 'label' => __( 'Cold Call', 'forms-bridge' ), + ), + array( + 'value' => 'Email', + 'label' => __( 'Email', 'forms-bridge' ), + ), + array( + 'value' => 'Existing Customer', + 'label' => __( 'Existing Customer', 'forms-bridge' ), + ), + array( + 'value' => 'Word of mouth', + 'label' => __( 'Word of Mouth', 'forms-bridge' ), + ), + array( + 'value' => 'Campaign', + 'label' => __( 'Campaign', 'forms-bridge' ), + ), + array( + 'value' => 'Conference', + 'label' => __( 'Conference', 'forms-bridge' ), + ), + array( + 'value' => 'Trade Show', + 'label' => __( 'Trade Show', 'forms-bridge' ), + ), + array( + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), + ), + array( + 'value' => 'Self Generated', + 'label' => __( 'Self Generated', 'forms-bridge' ), + ), + array( + 'value' => 'Other', + 'label' => __( 'Other', 'forms-bridge' ), + ), + ), + 'default' => 'Web Site', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'opportunity_type', + 'label' => __( 'Opportunity Type', 'forms-bridge' ), + 'description' => __( + 'Type of business opportunity', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'Existing Business', + 'label' => __( 'Existing Business', 'forms-bridge' ), + ), + array( + 'value' => 'New Business', + 'label' => __( 'New Business', 'forms-bridge' ), + ), + ), + 'default' => 'New Business', + ), + ), + 'bridge' => array( + 'endpoint' => 'Potentials', + 'method' => 'create', + 'custom_fields' => array( + array( + 'name' => 'sales_stage', + 'value' => 'Prospecting', + ), + array( + 'name' => 'leadsource', + 'value' => 'Web Site', + ), + array( + 'name' => 'opportunity_type', + 'value' => 'New Business', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'potentialname', + 'to' => 'potentialname', + 'cast' => 'string', + ), + array( + 'from' => '?amount', + 'to' => 'amount', + 'cast' => 'number', + ), + array( + 'from' => '?closingdate', + 'to' => 'closingdate', + 'cast' => 'string', + ), + array( + 'from' => '?probability', + 'to' => 'probability', + 'cast' => 'number', + ), + array( + 'from' => '?nextstep', + 'to' => 'nextstep', + 'cast' => 'string', + ), + array( + 'from' => '?description', + 'to' => 'description', + 'cast' => 'string', + ), + array( + 'from' => '?sales_stage', + 'to' => 'sales_stage', + 'cast' => 'string', + ), + array( + 'from' => '?leadsource', + 'to' => 'leadsource', + 'cast' => 'string', + ), + array( + 'from' => '?opportunity_type', + 'to' => 'opportunity_type', + 'cast' => 'string', + ), + array( + 'from' => '?related_to', + 'to' => 'related_to', + 'cast' => 'string', + ), + array( + 'from' => '?assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + ), + ), + ), + 'form' => array( + 'fields' => array( + array( + 'label' => __( 'Potential Name', 'forms-bridge' ), + 'name' => 'potentialname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Amount', 'forms-bridge' ), + 'name' => 'amount', + 'type' => 'number', + 'required' => true, + ), + array( + 'label' => __( 'Expected Close Date', 'forms-bridge' ), + 'name' => 'closingdate', + 'type' => 'date', + 'required' => true, + 'description' => __( 'Format: YYYY-MM-DD', 'forms-bridge' ), + ), + array( + 'label' => __( 'Probability (%)', 'forms-bridge' ), + 'name' => 'probability', + 'type' => 'number', + 'description' => __( 'Likelihood of closing (0-100)', 'forms-bridge' ), + ), + array( + 'label' => __( 'Next Step', 'forms-bridge' ), + 'name' => 'nextstep', + 'type' => 'text', + ), + array( + 'label' => __( 'Description', 'forms-bridge' ), + 'name' => 'description', + 'type' => 'textarea', + ), + ), + ), +); From 47a0040409863a94bafb9dbf40549928d00c81ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 03:36:41 +0100 Subject: [PATCH 02/12] feat: vtiger addon logo --- forms-bridge/addons/vtiger/assets/logo.png | Bin 0 -> 14617 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 forms-bridge/addons/vtiger/assets/logo.png diff --git a/forms-bridge/addons/vtiger/assets/logo.png b/forms-bridge/addons/vtiger/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..dd2d81ea711e40f233382dda3b0878441aa2f9df GIT binary patch literal 14617 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 AiU0rr literal 0 HcmV?d00001 From 47cbf9163b277cb11947655b3a9010c21eb74851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 03:38:22 +0100 Subject: [PATCH 03/12] feat: format code --- forms-bridge/addons/vtiger/class-vtiger-form-bridge.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php index af435bd..b61c7ed 100644 --- a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php +++ b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php @@ -351,9 +351,9 @@ private function execute_operation( $session_name, $payload, $more_args, $backen case 'sync': $url = self::ENDPOINT . '?' . http_build_query( array( - 'operation' => 'sync', - 'sessionName' => $session_name, - 'elementType' => $module, + 'operation' => 'sync', + 'sessionName' => $session_name, + 'elementType' => $module, 'modifiedTime' => $more_args['modifiedTime'] ?? 0, ) ); From 5182a65ed5108e0499006ab9716693c9750cd53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Tue, 25 Nov 2025 10:26:13 +0100 Subject: [PATCH 04/12] feat: improve github actions cache strategy --- .github/workflows/release.yml | 6 ++---- .github/workflows/tests.yml | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 984602a..a0dda66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,11 +30,9 @@ jobs: uses: actions/cache@v4 with: path: node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }} + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + ${{ runner.os }}-node- - name: Install dependencies run: npm ci diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 855f6ad..6745c2f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,14 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - name: Composer install run: composer -q install @@ -35,17 +43,21 @@ jobs: with: submodules: true - - name: Cache composer + - name: Cache composer dependencies uses: actions/cache@v4 with: path: vendor - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('composer.lock') }} + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- - - name: Cache plugins + - name: Cache test plugins uses: actions/cache@v4 with: path: /tmp/*.zip - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('bin/download-test-deps.sh') }} + key: ${{ runner.os }}-test-plugins-${{ hashFiles('bin/download-test-deps.sh') }} + restore-keys: | + ${{ runner.os }}-test-plugins- - name: Composer install run: composer -q install From e108d0912b7bee8db6b4b2df65cb2fbb567bd85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Tue, 25 Nov 2025 14:01:18 +0100 Subject: [PATCH 05/12] feat: force x-www-form-urlencoded for vtiger templates backend --- forms-bridge/addons/vtiger/hooks.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/forms-bridge/addons/vtiger/hooks.php b/forms-bridge/addons/vtiger/hooks.php index 22bc2c1..a11aee7 100644 --- a/forms-bridge/addons/vtiger/hooks.php +++ b/forms-bridge/addons/vtiger/hooks.php @@ -115,6 +115,11 @@ function ( $defaults, $addon, $schema ) { 'name' => 'name', 'default' => 'Vtiger', ), + array( + 'ref' => '#backend/headers[]', + 'name' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), array( 'ref' => '#bridge', 'name' => 'endpoint', @@ -141,6 +146,10 @@ function ( $defaults, $addon, $schema ) { 'backend' => array( 'name' => 'Vtiger', 'headers' => array( + array( + 'name' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), array( 'name' => 'Accept', 'value' => 'application/json', From 5c91369e539115ded696aed14a8fb0aa2ef98e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Tue, 25 Nov 2025 14:01:45 +0100 Subject: [PATCH 06/12] fix: vtiger form bridge login credential argument --- .../suitecrm/class-suitecrm-form-bridge.php | 1 + .../vtiger/class-vtiger-form-bridge.php | 35 +++++++------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php index 73f5c36..195cc7c 100644 --- a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php +++ b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php @@ -198,6 +198,7 @@ public function submit( $payload = array(), $attachments = array() ) { add_filter( 'http_bridge_request', static function ( $request ) { + unset( $request['args']['headers']['authentication'] ); self::$request = $request; return $request; }, diff --git a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php index b61c7ed..c1b6f0c 100644 --- a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php +++ b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php @@ -60,16 +60,9 @@ public static function rest_response( $res ) { } if ( empty( $res['data'] ) ) { - $content_type = - Http_Client::get_content_type( $res['headers'] ) ?? 'undefined'; - return new WP_Error( - 'unknown_content_type', - sprintf( - /* translators: %s: Content-Type header value */ - esc_html( __( 'Unknown HTTP response content type %s', 'forms-bridge' ) ), - sanitize_text_field( $content_type ) - ), + 'bad_request', + 'Vtiger null response body', $res ); } @@ -136,18 +129,18 @@ private static function get_challenge( $username, $backend ) { /** * Login to Vtiger and get session name. * - * @param array $credentials Credentials array with client_id (username) and client_secret (access key). - * @param Backend $backend Bridge backend object. + * @param Credential $credential Bridge credential object. + * @param Backend $backend Bridge backend object. * * @return string|WP_Error Session name on success. */ - private static function rest_login( $credentials, $backend ) { + private static function rest_login( $credential, $backend ) { if ( self::$session_name ) { return self::$session_name; } - $username = $credentials[0] ?? ''; - $access_key = $credentials[1] ?? ''; + $username = $credential->client_id ?? ''; + $access_key = $credential->client_secret ?? ''; // Step 1: Get challenge token. $token = self::get_challenge( $username, $backend ); @@ -165,7 +158,7 @@ private static function rest_login( $credentials, $backend ) { 'accessKey' => $access_key_hash, ); - $response = $backend->post( self::ENDPOINT, $payload, 'application/x-www-form-urlencoded' ); + $response = $backend->post( self::ENDPOINT, $payload ); $result = self::rest_response( $response ); if ( is_wp_error( $result ) ) { @@ -232,6 +225,7 @@ public function submit( $payload = array(), $more_args = array() ) { add_filter( 'http_bridge_request', static function ( $request ) { + unset( $request['args']['headers']['authentication'] ); self::$request = $request; return $request; }, @@ -239,11 +233,8 @@ static function ( $request ) { 1 ); - // Get credentials for login. - $login_credentials = $credential->authorization(); - // Login to get session. - $session_name = self::rest_login( $login_credentials, $backend ); + $session_name = self::rest_login( $credential, $backend ); if ( is_wp_error( $session_name ) ) { return $session_name; @@ -325,7 +316,7 @@ private function execute_operation( $session_name, $payload, $more_args, $backen 'element' => wp_json_encode( $element ), ); - $response = $backend->post( self::ENDPOINT, $post_data, 'application/x-www-form-urlencoded' ); + $response = $backend->post( self::ENDPOINT, $post_data ); break; case 'update': @@ -335,7 +326,7 @@ private function execute_operation( $session_name, $payload, $more_args, $backen 'element' => wp_json_encode( $payload ), ); - $response = $backend->post( self::ENDPOINT, $post_data, 'application/x-www-form-urlencoded' ); + $response = $backend->post( self::ENDPOINT, $post_data ); break; case 'delete': @@ -345,7 +336,7 @@ private function execute_operation( $session_name, $payload, $more_args, $backen 'id' => $payload['id'] ?? '', ); - $response = $backend->post( self::ENDPOINT, $post_data, 'application/x-www-form-urlencoded' ); + $response = $backend->post( self::ENDPOINT, $post_data ); break; case 'sync': From c36abb0edb3c810405481cea6f3813aaa8e706b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 02:20:20 +0100 Subject: [PATCH 07/12] feat: vtiger templates --- .../addons/vtiger/class-vtiger-addon.php | 2 +- .../vtiger/class-vtiger-form-bridge.php | 8 +- forms-bridge/addons/vtiger/jobs/account.php | 261 ++++++++++++ forms-bridge/addons/vtiger/jobs/contact.php | 305 ++++++++++++++ .../addons/vtiger/jobs/event-date-time.php | 79 ++++ .../addons/vtiger/jobs/skip-contact.php | 75 ++++ .../addons/vtiger/templates/accounts.php | 205 +++++---- .../addons/vtiger/templates/contacts.php | 170 +++----- .../addons/vtiger/templates/leads.php | 224 ++++------ .../addons/vtiger/templates/meetings.php | 389 ++++++++++++++++++ .../addons/vtiger/templates/potentials.php | 311 -------------- 11 files changed, 1335 insertions(+), 694 deletions(-) create mode 100644 forms-bridge/addons/vtiger/jobs/account.php create mode 100644 forms-bridge/addons/vtiger/jobs/contact.php create mode 100644 forms-bridge/addons/vtiger/jobs/event-date-time.php create mode 100644 forms-bridge/addons/vtiger/jobs/skip-contact.php create mode 100644 forms-bridge/addons/vtiger/templates/meetings.php delete mode 100644 forms-bridge/addons/vtiger/templates/potentials.php diff --git a/forms-bridge/addons/vtiger/class-vtiger-addon.php b/forms-bridge/addons/vtiger/class-vtiger-addon.php index afb3e59..a0f213f 100644 --- a/forms-bridge/addons/vtiger/class-vtiger-addon.php +++ b/forms-bridge/addons/vtiger/class-vtiger-addon.php @@ -90,7 +90,7 @@ public function fetch( $endpoint, $backend ) { return $bridge->submit( array( - 'query' => "SELECT id, label FROM {$endpoint} LIMIT 100;", + 'query' => "SELECT * FROM {$endpoint};", ) ); } diff --git a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php index c1b6f0c..ce1ba4e 100644 --- a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php +++ b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php @@ -210,7 +210,8 @@ public function submit( $payload = array(), $more_args = array() ) { if ( ! $backend ) { return new WP_Error( 'invalid_backend', - 'The bridge does not have a valid backend' + 'The bridge does not have a valid backend', + $this->data, ); } @@ -218,7 +219,8 @@ public function submit( $payload = array(), $more_args = array() ) { if ( ! $credential ) { return new WP_Error( 'invalid_credential', - 'The bridge does not have a valid credential' + 'The bridge does not have a valid credential', + $backend->data(), ); } @@ -280,7 +282,7 @@ private function execute_operation( $session_name, $payload, $more_args, $backen break; case 'query': - $query = $more_args['query'] ?? $payload['query'] ?? "SELECT * FROM {$module} LIMIT 20;"; + $query = $more_args['query'] ?? $payload['query'] ?? "SELECT * FROM {$module};"; $url = self::ENDPOINT . '?' . http_build_query( array( 'operation' => 'query', diff --git a/forms-bridge/addons/vtiger/jobs/account.php b/forms-bridge/addons/vtiger/jobs/account.php new file mode 100644 index 0000000..dbb68b8 --- /dev/null +++ b/forms-bridge/addons/vtiger/jobs/account.php @@ -0,0 +1,261 @@ + __( 'Account', 'forms-bridge' ), + 'description' => __( 'Creates an account (organization) in Vtiger', 'forms-bridge' ), + 'method' => 'forms_bridge_vtiger_create_account', + 'input' => array( + array( + 'name' => 'accountname', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'phone', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'otherphone', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'website', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'fax', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'tickersymbol', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email1', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email2', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'ownership', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'employees', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'account_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'rating', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'industry', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'siccode', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'accounttype', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'annual_revenue', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'emailoptout', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'notify_owner', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'assigned_user_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'bill_street', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'ship_street', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'bill_city', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'ship_city', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'bill_state', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'ship_state', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'bill_code', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'ship_code', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'bill_country', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'ship_country', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'bill_pobox', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'ship_pobox', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'description', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'starred', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'tags', + 'schema' => array( 'type' => 'string' ), + ), + ), + 'output' => array( + array( + 'name' => 'account_id', + 'schema' => array( 'type' => 'string' ), + ), + ), +); + +/** + * Creates a new account and add its ID to the payload. + * + * @param array $payload Bridge payload. + * @param Form_Bridge $bridge Bridge object. + * + * @return array + */ +function forms_bridge_vtiger_create_account( $payload, $bridge ) { + $account = array( + 'accountname' => $payload['accountname'], + ); + + $account_fields = array( + 'phone', + 'website', + 'fax', + 'tickersymbol', + 'otherphone', + 'account_id', + 'email1', + 'email2', + 'employees', + 'ownership', + 'rating', + 'industry', + 'siccode', + 'accounttype', + 'annual_revenue', + 'emailoptout', + 'notify_owner', + 'assigned_user_id', + 'bill_street', + 'ship_street', + 'bill_city', + 'ship_city', + 'bill_code', + 'ship_code', + 'bill_state', + 'ship_state', + 'bill_country', + 'ship_country', + 'bill_pobox', + 'ship_pobox', + 'description', + 'starred', + 'tags', + ); + + foreach ( $account_fields as $field ) { + if ( isset( $payload[ $field ] ) ) { + $account[ $field ] = $payload[ $field ]; + } + } + + $query = "SELECT id FROM Accounts WHERE accountname = '{$account['accountname']}';"; + + $response = $bridge->patch( + array( + 'method' => 'query', + 'endpoint' => 'Accounts', + ) + )->submit( array( 'query' => $query ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $account_id = $response['data']['result'][0]['id']; + if ( ! empty( $account_id ) ) { + $response = $bridge->patch( + array( + 'method' => 'update', + 'endpoint' => 'Accounts', + ) + )->submit( array_merge( array( 'id' => $account_id ), $account ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $payload['account_id'] = $account_id; + return $payload; + } + + $response = $bridge->patch( + array( + 'method' => 'create', + 'endpoint' => 'Accounts', + ) + )->submit( $account ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $payload['account_id'] = $response['data']['result']['id']; + return $payload; +} diff --git a/forms-bridge/addons/vtiger/jobs/contact.php b/forms-bridge/addons/vtiger/jobs/contact.php new file mode 100644 index 0000000..1f0f063 --- /dev/null +++ b/forms-bridge/addons/vtiger/jobs/contact.php @@ -0,0 +1,305 @@ + __( 'Contact', 'forms-bridge' ), + 'description' => __( 'Creates a contact in Vtiger', 'forms-bridge' ), + 'method' => 'forms_bridge_vtiger_create_contact', + 'input' => array( + array( + 'name' => 'lastname', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'firstname', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'salutationtype', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'leadsource', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'birthday', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'assigned_user_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'phone', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'mobile', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'homephone', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'otherphone', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'fax', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'secondaryemail', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'contact_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'account_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'title', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'department', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'assistant', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'assistantphone', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'donotcall', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'emailoptout', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'reference', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'notify_owner', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'portal', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'mailingstreet', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'otherstreet', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'mailingcity', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'othercity', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'mailingstate', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'otherstate', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'mailingzip', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'otherzip', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'mailingcountry', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'othercountry', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'mailingpobox', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'otherpobox', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'imagename', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'description', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'starred', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'tags', + 'schema' => array( 'type' => 'string' ), + ), + ), + 'output' => array( + array( + 'name' => 'contact_id', + 'schema' => array( 'type' => 'string' ), + ), + ), +); + +/** + * Creates a new contact and add its ID to the payload. + * + * @param array $payload Bridge payload. + * @param Vtiger_Form_Bridge $bridge Bridge object. + * + * @return array + */ +function forms_bridge_vtiger_create_contact( $payload, $bridge ) { + $contact = array( + 'lastname' => $payload['lastname'], + ); + + $contact_fields = array( + 'firstname', + 'salutationtype', + 'assigned_user_id', + 'phone', + 'mobile', + 'account_id', + 'homephone', + 'leadsource', + 'otherphone', + 'title', + 'fax', + 'department', + 'birthday', + 'email', + 'contact_id', + 'assistant', + 'secondaryemail', + 'assistantphone', + 'donotcall', + 'emailoptout', + 'reference', + 'notify_owner', + 'portal', + 'mailingstreet', + 'otherstreet', + 'mailingcity', + 'othercity', + 'mailingzip', + 'otherzip', + 'mailingstate', + 'otherstate', + 'mailingcountry', + 'othercountry', + 'mailingpobox', + 'otherpobox', + 'imagename', + 'description', + 'starred', + 'tags', + ); + + foreach ( $contact_fields as $field ) { + if ( isset( $payload[ $field ] ) ) { + $contact[ $field ] = $payload[ $field ]; + } + } + + if ( isset( $contact['firstname'] ) ) { + $query = "SELECT id FROM Contacts WHERE firstname = '{$contact['firstname']}' AND lastname = '{$contact['lastname']}'"; + + if ( isset( $contact['email'] ) ) { + $query .= " OR email = '{$contact['email']}';"; + } else { + $query .= ';'; + } + } elseif ( isset( $contact['email'] ) ) { + $query = "SELECT id FROM Contacts WHERE email = '{$contact['email']}';"; + } + + if ( isset( $query ) ) { + $response = $bridge->patch( + array( + 'method' => 'query', + 'endpoint' => 'Contacts', + ) + )->submit( array( 'query' => $query ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $contact_id = $response['data']['result'][0]['id'] ?? null; + if ( ! empty( $contact_id ) ) { + $response = $bridge->patch( + array( + 'name' => '__vtiger-' . time(), + 'method' => 'update', + 'endpoint' => 'Contacts', + ) + )->submit( array_merge( array( 'id' => $contact_id ), $contact ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $payload['contact_id'] = $contact_id; + return $payload; + } + } + + $response = $bridge->patch( + array( + 'name' => '__vtiger-' . time(), + 'method' => 'create', + 'endpoint' => 'Contacts', + ) + )->submit( $contact ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $payload['contact_id'] = $response['data']['result']['id']; + return $payload; +} diff --git a/forms-bridge/addons/vtiger/jobs/event-date-time.php b/forms-bridge/addons/vtiger/jobs/event-date-time.php new file mode 100644 index 0000000..375846b --- /dev/null +++ b/forms-bridge/addons/vtiger/jobs/event-date-time.php @@ -0,0 +1,79 @@ + __( 'Event date and time', 'forms-bridge' ), + 'description' => __( 'Given a datetime and a duration, sets up the vtiger event dates', 'forms-bridge' ), + 'method' => 'forms_bridge_vtiger_event_date_and_time', + 'input' => array( + array( + 'name' => 'datetime', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'duration_hours', + 'schema' => array( 'type' => 'integer' ), + 'required' => true, + ), + array( + 'name' => 'duration_minutes', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + ), + 'output' => array( + array( + 'name' => 'date_start', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'time_start', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'due_date', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'time_end', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'duration_hours', + 'schema' => array( 'type' => 'integer' ), + ), + array( + 'name' => 'duration_minutes', + 'schema' => array( 'type' => 'string' ), + ), + ), +); + +/** + * Given a payload with a datetime and duration, sets up the vtiger event dates. + * + * @param array $payload Bridge payload. + * + * @return array + */ +function forms_bridge_vtiger_event_date_and_time( $payload ) { + $datetime = $payload['datetime']; + $time = strtotime( $datetime ); + $endtime = $time + $payload['duration_hours'] * 3600 + $payload['duration_minutes'] * 60; + + $payload['date_start'] = date( 'Y-m-d', $time ); + $payload['time_start'] = date( 'H:i:s', $time ); + $payload['due_date'] = date( 'Y-m-d', $endtime ); + $payload['time_end'] = date( 'H:i:s', $endtime ); + + return $payload; +} diff --git a/forms-bridge/addons/vtiger/jobs/skip-contact.php b/forms-bridge/addons/vtiger/jobs/skip-contact.php new file mode 100644 index 0000000..9a8b532 --- /dev/null +++ b/forms-bridge/addons/vtiger/jobs/skip-contact.php @@ -0,0 +1,75 @@ + __( 'Skip contact', 'forms-bridge' ), + 'description' => __( 'Searches for existing contacts by name and skip duplications', 'forms-bridge' ), + 'method' => 'forms_bridge_vtiger_skip_contact', + 'input' => array( + array( + 'name' => 'lastname', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'firstname', + 'schema' => array( 'type' => 'string' ), + ), + ), + 'output' => array( + array( + 'name' => 'lastname', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'firstname', + 'schema' => array( 'type' => 'string' ), + ), + ), +); + +/** + * Look up existing contacts by name and skips bridge submission if found. + * + * @param array $payload Bridge payload. + * @param Vtiger_Form_Bridge $bridge Bridge object. + */ +function forms_bridge_vtiger_skip_contact( $payload, $bridge ) { + $query = "SELECT id FROM Contacts WHERE email = '{$payload['email']}'"; + + if ( isset( $payload['firstname'], $payload['lastname'] ) ) { + $query .= " OR firstname = '{$payload['firstname']}' AND lastname = '{$payload['lastname']}';"; + } + + $response = $bridge->patch( + array( + 'method' => 'query', + 'endpoint' => 'Contacts', + ) + )->submit( array( 'query' => $query ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $contact_id = $response['data']['result'][0]['id'] ?? null; + if ( ! empty( $contact_id ) ) { + $result = forms_bridge_vtiger_create_contact( $payload, $bridge ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + return; + } + + return $payload; +} diff --git a/forms-bridge/addons/vtiger/templates/accounts.php b/forms-bridge/addons/vtiger/templates/accounts.php index b7714bd..6d0c39c 100644 --- a/forms-bridge/addons/vtiger/templates/accounts.php +++ b/forms-bridge/addons/vtiger/templates/accounts.php @@ -24,35 +24,27 @@ array( 'ref' => '#bridge', 'name' => 'endpoint', - 'value' => 'Accounts', + 'value' => 'Contacts', ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'assigned_user_id', - 'label' => __( 'Assigned User', 'forms-bridge' ), - 'description' => __( - 'User to assign the account to', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( 'endpoint' => 'Users', 'finger' => array( - 'value' => 'result.[].id', - 'label' => 'result.[].label', + 'value' => 'result[].id', + 'label' => 'result[].user_name', ), ), ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'accounttype', - 'label' => __( 'Account Type', 'forms-bridge' ), - 'description' => __( - 'Type of account', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'accounttype', + 'label' => __( 'Account Type', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( 'value' => 'Analyst', 'label' => __( 'Analyst', 'forms-bridge' ), @@ -94,7 +86,7 @@ 'label' => __( 'Other', 'forms-bridge' ), ), ), - 'default' => 'Prospect', + 'default' => 'Prospect', ), array( 'ref' => '#bridge/custom_fields[]', @@ -154,10 +146,18 @@ 'value' => 'Entertainment', 'label' => __( 'Entertainment', 'forms-bridge' ), ), + array( + 'value' => 'Environmental', + 'label' => __( 'Environmental', 'forms-bridge' ), + ), array( 'value' => 'Finance', 'label' => __( 'Finance', 'forms-bridge' ), ), + array( + 'value' => 'Food & Beverage', + 'label' => __( 'Food & Beverage', 'forms-bridge' ), + ), array( 'value' => 'Government', 'label' => __( 'Government', 'forms-bridge' ), @@ -174,6 +174,10 @@ 'value' => 'Insurance', 'label' => __( 'Insurance', 'forms-bridge' ), ), + array( + 'value' => 'Machinery', + 'label' => __( 'Machinery', 'forms-bridge' ), + ), array( 'value' => 'Manufacturing', 'label' => __( 'Manufacturing', 'forms-bridge' ), @@ -182,18 +186,38 @@ 'value' => 'Media', 'label' => __( 'Media', 'forms-bridge' ), ), + array( + 'value' => 'Not For Profit', + 'label' => __( 'Not For Profit', 'forms-bridge' ), + ), + array( + 'value' => 'Recreation', + 'label' => __( 'Recreation', 'forms-bridge' ), + ), array( 'value' => 'Retail', 'label' => __( 'Retail', 'forms-bridge' ), ), + array( + 'value' => 'Shipping', + 'label' => __( 'Shipping', 'forms-bridge' ), + ), array( 'value' => 'Technology', 'label' => __( 'Technology', 'forms-bridge' ), ), + array( + 'value' => 'Telecomunications', + 'label' => __( 'Telecomunications', 'forms-bridge' ), + ), array( 'value' => 'Transportation', 'label' => __( 'Transportation', 'forms-bridge' ), ), + array( + 'value' => 'Utilities', + 'label' => __( 'Utilities', 'forms-bridge' ), + ), array( 'value' => 'Other', 'label' => __( 'Other', 'forms-bridge' ), @@ -202,94 +226,21 @@ ), ), 'bridge' => array( - 'endpoint' => 'Accounts', - 'method' => 'create', - 'custom_fields' => array( - array( - 'name' => 'accounttype', - 'value' => 'Prospect', - ), - ), - 'mutations' => array( + 'endpoint' => 'Contacts', + 'method' => 'create', + 'workflow' => array( 'account', 'skip-contact' ), + 'mutations' => array( array( array( - 'from' => 'accountname', - 'to' => 'accountname', - 'cast' => 'string', - ), - array( - 'from' => '?email', - 'to' => 'email1', - 'cast' => 'string', - ), - array( - 'from' => '?phone', - 'to' => 'phone', - 'cast' => 'string', - ), - array( - 'from' => '?fax', - 'to' => 'fax', - 'cast' => 'string', - ), - array( - 'from' => '?website', - 'to' => 'website', - 'cast' => 'string', - ), - array( - 'from' => '?description', - 'to' => 'description', - 'cast' => 'string', - ), - array( - 'from' => '?employees', - 'to' => 'employees', - 'cast' => 'integer', - ), - array( - 'from' => '?annual_revenue', - 'to' => 'annual_revenue', - 'cast' => 'number', - ), - array( - 'from' => '?address', - 'to' => 'bill_street', - 'cast' => 'string', - ), - array( - 'from' => '?city', - 'to' => 'bill_city', - 'cast' => 'string', - ), - array( - 'from' => '?state', - 'to' => 'bill_state', - 'cast' => 'string', - ), - array( - 'from' => '?postal_code', - 'to' => 'bill_code', - 'cast' => 'string', - ), - array( - 'from' => '?country', - 'to' => 'bill_country', - 'cast' => 'string', - ), - array( - 'from' => '?accounttype', - 'to' => 'accounttype', - 'cast' => 'string', - ), - array( - 'from' => '?industry', - 'to' => 'industry', - 'cast' => 'string', + 'from' => 'email1', + 'to' => 'user_email', + 'cast' => 'copy', ), + ), + array( array( - 'from' => '?assigned_user_id', - 'to' => 'assigned_user_id', + 'from' => 'user_email', + 'to' => 'email', 'cast' => 'string', ), ), @@ -298,15 +249,33 @@ 'form' => array( 'fields' => array( array( - 'label' => __( 'Account Name', 'forms-bridge' ), + 'label' => __( 'First Name', 'forms-bridge' ), + 'name' => 'firstname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Last Name', 'forms-bridge' ), + 'name' => 'lastname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Company Name', 'forms-bridge' ), 'name' => 'accountname', 'type' => 'text', 'required' => true, ), array( - 'label' => __( 'Email', 'forms-bridge' ), - 'name' => 'email', - 'type' => 'email', + 'label' => __( 'Email', 'forms-bridge' ), + 'name' => 'email1', + 'type' => 'email', + 'required' => true, + ), + array( + 'label' => __( 'Title', 'forms-bridge' ), + 'name' => 'title', + 'type' => 'text', ), array( 'label' => __( 'Phone', 'forms-bridge' ), @@ -320,17 +289,27 @@ ), array( 'label' => __( 'Address', 'forms-bridge' ), - 'name' => 'address', + 'name' => 'bill_street', 'type' => 'text', ), array( 'label' => __( 'City', 'forms-bridge' ), - 'name' => 'city', + 'name' => 'bill_city', + 'type' => 'text', + ), + array( + 'label' => __( 'Postal Code', 'forms-bridge' ), + 'name' => 'bill_code', + 'type' => 'text', + ), + array( + 'label' => __( 'State', 'forms-bridge' ), + 'name' => 'bill_state', 'type' => 'text', ), array( 'label' => __( 'Country', 'forms-bridge' ), - 'name' => 'country', + 'name' => 'bill_country', 'type' => 'text', ), array( diff --git a/forms-bridge/addons/vtiger/templates/contacts.php b/forms-bridge/addons/vtiger/templates/contacts.php index ea1ebe3..275bed7 100644 --- a/forms-bridge/addons/vtiger/templates/contacts.php +++ b/forms-bridge/addons/vtiger/templates/contacts.php @@ -12,7 +12,7 @@ return array( 'title' => __( 'Contacts', 'forms-bridge' ), 'description' => __( - 'Contact form template. The resulting bridge will convert form submissions into Vtiger contacts.', + 'Contact form bridge template. The resulting bridge will convert form submissions into Vtiger contacts.', 'forms-bridge' ), 'fields' => array( @@ -27,32 +27,24 @@ 'value' => 'Contacts', ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'assigned_user_id', - 'label' => __( 'Assigned User', 'forms-bridge' ), - 'description' => __( - 'User to assign the contact to', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( 'endpoint' => 'Users', 'finger' => array( - 'value' => 'result.[].id', - 'label' => 'result.[].label', + 'value' => 'result[].id', + 'label' => 'result[].user_name', ), ), ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'leadsource', - 'label' => __( 'Lead Source', 'forms-bridge' ), - 'description' => __( - 'Source of the contact', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'leadsource', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( 'value' => 'Web Site', 'label' => __( 'Web Site', 'forms-bridge' ), @@ -62,114 +54,46 @@ 'label' => __( 'Cold Call', 'forms-bridge' ), ), array( - 'value' => 'Email', - 'label' => __( 'Email', 'forms-bridge' ), - ), - array( - 'value' => 'Word of mouth', - 'label' => __( 'Word of Mouth', 'forms-bridge' ), - ), - array( - 'value' => 'Campaign', - 'label' => __( 'Campaign', 'forms-bridge' ), - ), - array( - 'value' => 'Other', - 'label' => __( 'Other', 'forms-bridge' ), - ), - ), - 'default' => 'Web Site', - ), - ), - 'bridge' => array( - 'endpoint' => 'Contacts', - 'method' => 'create', - 'custom_fields' => array( - array( - 'name' => 'leadsource', - 'value' => 'Web Site', - ), - ), - 'mutations' => array( - array( - array( - 'from' => 'firstname', - 'to' => 'firstname', - 'cast' => 'string', - ), - array( - 'from' => 'lastname', - 'to' => 'lastname', - 'cast' => 'string', - ), - array( - 'from' => 'email', - 'to' => 'email', - 'cast' => 'string', - ), - array( - 'from' => '?phone', - 'to' => 'phone', - 'cast' => 'string', - ), - array( - 'from' => '?mobile', - 'to' => 'mobile', - 'cast' => 'string', - ), - array( - 'from' => '?title', - 'to' => 'title', - 'cast' => 'string', - ), - array( - 'from' => '?department', - 'to' => 'department', - 'cast' => 'string', + 'value' => 'Direct Mail', + 'label' => __( 'Direct Mail', 'forms-bridge' ), ), array( - 'from' => '?description', - 'to' => 'description', - 'cast' => 'string', + 'value' => 'Existing Customer', + 'label' => __( 'Existing Customer', 'forms-bridge' ), ), array( - 'from' => '?address', - 'to' => 'mailingstreet', - 'cast' => 'string', + 'value' => 'Employee', + 'label' => __( 'Employee', 'forms-bridge' ), ), array( - 'from' => '?city', - 'to' => 'mailingcity', - 'cast' => 'string', + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), ), array( - 'from' => '?state', - 'to' => 'mailingstate', - 'cast' => 'string', + 'value' => 'Public Relations', + 'label' => __( 'Public Relations', 'forms-bridge' ), ), array( - 'from' => '?postal_code', - 'to' => 'mailingzip', - 'cast' => 'string', - ), - array( - 'from' => '?country', - 'to' => 'mailingcountry', - 'cast' => 'string', + 'value' => 'Word of Mouth', + 'label' => __( 'Word of Mouth', 'forms-bridge' ), ), array( - 'from' => '?leadsource', - 'to' => 'leadsource', - 'cast' => 'string', + 'value' => 'Conference', + 'label' => __( 'Conference', 'forms-bridge' ), ), array( - 'from' => '?assigned_user_id', - 'to' => 'assigned_user_id', - 'cast' => 'string', + 'value' => 'Other', + 'label' => __( 'Other', 'forms-bridge' ), ), ), + 'default' => 'Web Site', ), ), + 'bridge' => array( + 'endpoint' => 'Contacts', + 'method' => 'create', + 'workflow' => array( 'skip-contact' ), + ), 'form' => array( 'fields' => array( array( @@ -196,8 +120,28 @@ 'type' => 'tel', ), array( - 'label' => __( 'Title', 'forms-bridge' ), - 'name' => 'title', + 'label' => __( 'Address', 'forms-bridge' ), + 'name' => 'mailingstreet', + 'type' => 'text', + ), + array( + 'label' => __( 'City', 'forms-bridge' ), + 'name' => 'mailingcity', + 'type' => 'text', + ), + array( + 'label' => __( 'Postal Code', 'forms-bridge' ), + 'name' => 'mailingzip', + 'type' => 'text', + ), + array( + 'label' => __( 'State', 'forms-bridge' ), + 'name' => 'mailingstate', + 'type' => 'text', + ), + array( + 'label' => __( 'Country', 'forms-bridge' ), + 'name' => 'mailingcountry', 'type' => 'text', ), array( diff --git a/forms-bridge/addons/vtiger/templates/leads.php b/forms-bridge/addons/vtiger/templates/leads.php index 71ba711..0789408 100644 --- a/forms-bridge/addons/vtiger/templates/leads.php +++ b/forms-bridge/addons/vtiger/templates/leads.php @@ -27,212 +27,125 @@ 'value' => 'Leads', ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'assigned_user_id', - 'label' => __( 'Assigned User', 'forms-bridge' ), - 'description' => __( - 'User to assign the lead to', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( 'endpoint' => 'Users', 'finger' => array( - 'value' => 'result.[].id', - 'label' => 'result.[].label', + 'value' => 'result[].id', + 'label' => 'result[].user_name', ), ), ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'leadstatus', - 'label' => __( 'Lead Status', 'forms-bridge' ), - 'description' => __( - 'Initial status of the lead', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - array( - 'value' => 'New', - 'label' => __( 'New', 'forms-bridge' ), - ), - array( - 'value' => 'Assigned', - 'label' => __( 'Assigned', 'forms-bridge' ), - ), - array( - 'value' => 'In Process', - 'label' => __( 'In Process', 'forms-bridge' ), - ), + 'ref' => '#bridge/custom_fields[]', + 'name' => 'leadstatus', + 'label' => __( 'Lead Status', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( - 'value' => 'Converted', - 'label' => __( 'Converted', 'forms-bridge' ), + 'value' => 'Not Contacted', + 'label' => __( 'Not Contacted', 'forms-bridge' ), ), array( - 'value' => 'Recycled', - 'label' => __( 'Recycled', 'forms-bridge' ), + 'value' => 'Contacted', + 'label' => __( 'Contacted', 'forms-bridge' ), ), array( - 'value' => 'Dead', - 'label' => __( 'Dead', 'forms-bridge' ), - ), - ), - 'default' => 'New', - ), - array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'leadsource', - 'label' => __( 'Lead Source', 'forms-bridge' ), - 'description' => __( - 'Source of the lead', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - array( - 'value' => 'Web Site', - 'label' => __( 'Web Site', 'forms-bridge' ), + 'value' => 'Attempted to Contact', + 'label' => __( 'Attempted to Contact', 'forms-bridge' ), ), array( - 'value' => 'Cold Call', - 'label' => __( 'Cold Call', 'forms-bridge' ), + 'value' => 'Contact in Future', + 'label' => __( 'Contact in Future', 'forms-bridge' ), ), array( - 'value' => 'Email', - 'label' => __( 'Email', 'forms-bridge' ), + 'value' => 'Cold', + 'label' => __( 'Cold', 'forms-bridge' ), ), array( - 'value' => 'Word of mouth', - 'label' => __( 'Word of Mouth', 'forms-bridge' ), + 'value' => 'Warm', + 'label' => __( 'Warm', 'forms-bridge' ), ), array( - 'value' => 'Campaign', - 'label' => __( 'Campaign', 'forms-bridge' ), + 'value' => 'Hot', + 'label' => __( 'Hot', 'forms-bridge' ), ), array( - 'value' => 'Conference', - 'label' => __( 'Conference', 'forms-bridge' ), + 'value' => 'Lost Lead', + 'label' => __( 'Lost Lead', 'forms-bridge' ), ), array( - 'value' => 'Trade Show', - 'label' => __( 'Trade Show', 'forms-bridge' ), + 'value' => 'Pre Qualified', + 'label' => __( 'Pre Qualified', 'forms-bridge' ), ), array( - 'value' => 'Partner', - 'label' => __( 'Partner', 'forms-bridge' ), + 'value' => 'Qualified', + 'label' => __( 'Junk Lead', 'forms-bridge' ), ), array( - 'value' => 'Other', - 'label' => __( 'Other', 'forms-bridge' ), + 'value' => 'Junk Lead', + 'label' => __( 'Junk Lead', 'forms-bridge' ), ), ), - 'default' => 'Web Site', - ), - ), - 'bridge' => array( - 'endpoint' => 'Leads', - 'method' => 'create', - 'custom_fields' => array( - array( - 'name' => 'leadstatus', - 'value' => 'New', - ), - array( - 'name' => 'leadsource', - 'value' => 'Web Site', - ), + 'default' => 'Not Contacted', ), - 'mutations' => array( - array( - array( - 'from' => 'firstname', - 'to' => 'firstname', - 'cast' => 'string', - ), - array( - 'from' => 'lastname', - 'to' => 'lastname', - 'cast' => 'string', - ), - array( - 'from' => 'email', - 'to' => 'email', - 'cast' => 'string', - ), - array( - 'from' => '?phone', - 'to' => 'phone', - 'cast' => 'string', - ), - array( - 'from' => '?mobile', - 'to' => 'mobile', - 'cast' => 'string', - ), - array( - 'from' => '?company', - 'to' => 'company', - 'cast' => 'string', - ), - array( - 'from' => '?designation', - 'to' => 'designation', - 'cast' => 'string', - ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'leadsource', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( - 'from' => '?description', - 'to' => 'description', - 'cast' => 'string', + 'value' => 'Web Site', + 'label' => __( 'Web Site', 'forms-bridge' ), ), array( - 'from' => '?website', - 'to' => 'website', - 'cast' => 'string', + 'value' => 'Cold Call', + 'label' => __( 'Cold Call', 'forms-bridge' ), ), array( - 'from' => '?address', - 'to' => 'lane', - 'cast' => 'string', + 'value' => 'Direct Mail', + 'label' => __( 'Direct Mail', 'forms-bridge' ), ), array( - 'from' => '?city', - 'to' => 'city', - 'cast' => 'string', + 'value' => 'Existing Customer', + 'label' => __( 'Existing Customer', 'forms-bridge' ), ), array( - 'from' => '?state', - 'to' => 'state', - 'cast' => 'string', + 'value' => 'Employee', + 'label' => __( 'Employee', 'forms-bridge' ), ), array( - 'from' => '?postal_code', - 'to' => 'code', - 'cast' => 'string', + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), ), array( - 'from' => '?country', - 'to' => 'country', - 'cast' => 'string', + 'value' => 'Public Relations', + 'label' => __( 'Public Relations', 'forms-bridge' ), ), array( - 'from' => '?leadstatus', - 'to' => 'leadstatus', - 'cast' => 'string', + 'value' => 'Word of Mouth', + 'label' => __( 'Word of Mouth', 'forms-bridge' ), ), array( - 'from' => '?leadsource', - 'to' => 'leadsource', - 'cast' => 'string', + 'value' => 'Conference', + 'label' => __( 'Conference', 'forms-bridge' ), ), array( - 'from' => '?assigned_user_id', - 'to' => 'assigned_user_id', - 'cast' => 'string', + 'value' => 'Other', + 'label' => __( 'Other', 'forms-bridge' ), ), ), + 'default' => 'Web Site', ), ), + 'bridge' => array( + 'endpoint' => 'Leads', + 'method' => 'create', + ), 'form' => array( 'fields' => array( array( @@ -263,6 +176,11 @@ 'name' => 'company', 'type' => 'text', ), + array( + 'label' => __( 'Designation', 'forms-bridge' ), + 'name' => 'designation', + 'type' => 'text', + ), array( 'label' => __( 'Website', 'forms-bridge' ), 'name' => 'website', diff --git a/forms-bridge/addons/vtiger/templates/meetings.php b/forms-bridge/addons/vtiger/templates/meetings.php new file mode 100644 index 0000000..f0e3e99 --- /dev/null +++ b/forms-bridge/addons/vtiger/templates/meetings.php @@ -0,0 +1,389 @@ + __( 'Meetings', 'forms-bridge' ), + 'description' => __( + 'Meetings bridge template. The resulting bridge will convert form submissions into Vtiger meetings.', + 'forms-bridge', + ), + 'fields' => array( + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Meetings', 'forms-bridge' ), + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => 'Events', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'duration_hours', + 'label' => __( 'Duration (Hours)', 'forms-bridge' ), + 'type' => 'number', + 'default' => 1, + 'required' => true, + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'duration_minutes', + 'label' => __( 'Duration (Minutes)', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( + array( + 'value' => '0', + 'label' => '.00', + ), + array( + 'value' => '15', + 'label' => '.15', + ), + array( + 'value' => '30', + 'label' => '.30', + ), + array( + 'value' => '45', + 'label' => '.45', + ), + ), + 'default' => '00', + 'required' => true, + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'result[].id', + 'label' => 'result[].user_name', + ), + ), + 'required' => true, + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'leadsource', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'Web Site', + 'label' => __( 'Web Site', 'forms-bridge' ), + ), + array( + 'value' => 'Cold Call', + 'label' => __( 'Cold Call', 'forms-bridge' ), + ), + array( + 'value' => 'Direct Mail', + 'label' => __( 'Direct Mail', 'forms-bridge' ), + ), + array( + 'value' => 'Existing Customer', + 'label' => __( 'Existing Customer', 'forms-bridge' ), + ), + array( + 'value' => 'Employee', + 'label' => __( 'Employee', 'forms-bridge' ), + ), + array( + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), + ), + array( + 'value' => 'Public Relations', + 'label' => __( 'Public Relations', 'forms-bridge' ), + ), + array( + 'value' => 'Word of Mouth', + 'label' => __( 'Word of Mouth', 'forms-bridge' ), + ), + array( + 'value' => 'Conference', + 'label' => __( 'Conference', 'forms-bridge' ), + ), + array( + 'value' => 'Other', + 'label' => __( 'Other', 'forms-bridge' ), + ), + ), + 'default' => 'Web Site', + ), + ), + 'bridge' => array( + 'endpoint' => 'Events', + 'method' => 'create', + 'custom_fields' => array( + array( + 'name' => 'eventstatus', + 'value' => 'Planned', + ), + array( + 'name' => 'activitytype', + 'value' => 'Meeting', + ), + ), + 'workflow' => array( 'date-fields-to-date', 'event-date-time', 'contact' ), + 'mutations' => array( + array(), + array(), + array( + array( + 'from' => 'assigned_user_id', + 'to' => 'meeting_assigned_user_id', + 'cast' => 'copy', + ), + array( + 'from' => 'firstname', + 'to' => 'subject[0]', + 'cast' => 'copy', + ), + array( + 'from' => 'lastname', + 'to' => 'subject[1]', + 'cast' => 'copy', + ), + array( + 'from' => 'subject', + 'to' => 'subject', + 'cast' => 'concat', + ), + ), + array( + array( + 'from' => 'meeting_assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + array( + 'from' => 'event_description', + 'to' => 'description', + 'cast' => 'string', + ), + array( + 'from' => 'duration_hours', + 'to' => 'duration_hours', + 'cast' => 'integer', + ), + array( + 'from' => 'duration_minutes', + 'to' => 'duration_minutes', + 'cast' => 'integer', + ), + ), + ), + ), + 'form' => array( + 'title' => __( 'Meetings', 'forms-bridge' ), + 'fields' => array( + array( + 'label' => __( 'First Name', 'forms-bridge' ), + 'name' => 'firstname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Last Name', 'forms-bridge' ), + 'name' => 'lastname', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Email', 'forms-bridge' ), + 'name' => 'email', + 'type' => 'email', + 'required' => true, + ), + array( + 'label' => __( 'Phone', 'forms-bridge' ), + 'name' => 'phone', + 'type' => 'tel', + ), + array( + 'name' => 'date', + 'label' => __( 'Date', 'forms-bridge' ), + 'type' => 'date', + 'required' => true, + ), + array( + 'name' => 'hour', + 'label' => __( 'Hour', 'forms-bridge' ), + 'type' => 'select', + 'required' => true, + 'options' => array( + array( + 'label' => __( '1 AM', 'forms-bridge' ), + 'value' => '01', + ), + array( + 'label' => __( '2 AM', 'forms-bridge' ), + 'value' => '02', + ), + array( + 'label' => __( '3 AM', 'forms-bridge' ), + 'value' => '03', + ), + array( + 'label' => __( '4 AM', 'forms-bridge' ), + 'value' => '04', + ), + array( + 'label' => __( '5 AM', 'forms-bridge' ), + 'value' => '05', + ), + array( + 'label' => __( '6 AM', 'forms-bridge' ), + 'value' => '06', + ), + array( + 'label' => __( '7 AM', 'forms-bridge' ), + 'value' => '07', + ), + array( + 'label' => __( '8 AM', 'forms-bridge' ), + 'value' => '08', + ), + array( + 'label' => __( '9 AM', 'forms-bridge' ), + 'value' => '09', + ), + array( + 'label' => __( '10 AM', 'forms-bridge' ), + 'value' => '10', + ), + array( + 'label' => __( '11 AM', 'forms-bridge' ), + 'value' => '11', + ), + array( + 'label' => __( '12 AM', 'forms-bridge' ), + 'value' => '12', + ), + array( + 'label' => __( '1 PM', 'forms-bridge' ), + 'value' => '13', + ), + array( + 'label' => __( '2 PM', 'forms-bridge' ), + 'value' => '14', + ), + array( + 'label' => __( '3 PM', 'forms-bridge' ), + 'value' => '15', + ), + array( + 'label' => __( '4 PM', 'forms-bridge' ), + 'value' => '16', + ), + array( + 'label' => __( '5 PM', 'forms-bridge' ), + 'value' => '17', + ), + array( + 'label' => __( '6 PM', 'forms-bridge' ), + 'value' => '18', + ), + array( + 'label' => __( '7 PM', 'forms-bridge' ), + 'value' => '19', + ), + array( + 'label' => __( '8 PM', 'forms-bridge' ), + 'value' => '20', + ), + array( + 'label' => __( '9 PM', 'forms-bridge' ), + 'value' => '21', + ), + array( + 'label' => __( '10 PM', 'forms-bridge' ), + 'value' => '22', + ), + array( + 'label' => __( '11 PM', 'forms-bridge' ), + 'value' => '23', + ), + array( + 'label' => __( '12 PM', 'forms-bridge' ), + 'value' => '24', + ), + ), + ), + array( + 'name' => 'minute', + 'label' => __( 'Minute', 'forms-bridge' ), + 'type' => 'select', + 'required' => true, + 'options' => array( + array( + 'label' => '00', + 'value' => '00.0', + ), + array( + 'label' => '05', + 'value' => '05', + ), + array( + 'label' => '10', + 'value' => '10', + ), + array( + 'label' => '15', + 'value' => '15', + ), + array( + 'label' => '20', + 'value' => '20', + ), + array( + 'label' => '25', + 'value' => '25', + ), + array( + 'label' => '30', + 'value' => '30', + ), + array( + 'label' => '35', + 'value' => '35', + ), + array( + 'label' => '40', + 'value' => '40', + ), + array( + 'label' => '45', + 'value' => '45', + ), + array( + 'label' => '50', + 'value' => '50', + ), + array( + 'label' => '55', + 'value' => '55', + ), + ), + ), + array( + 'name' => 'event_description', + 'type' => 'textarea', + 'label' => __( 'Comments', 'forms-bridge' ), + ), + ), + ), +); diff --git a/forms-bridge/addons/vtiger/templates/potentials.php b/forms-bridge/addons/vtiger/templates/potentials.php deleted file mode 100644 index f417c8d..0000000 --- a/forms-bridge/addons/vtiger/templates/potentials.php +++ /dev/null @@ -1,311 +0,0 @@ - __( 'Potentials', 'forms-bridge' ), - 'description' => __( - 'Potential (Opportunity) form template. The resulting bridge will convert form submissions into Vtiger potentials (sales opportunities).', - 'forms-bridge' - ), - 'fields' => array( - array( - 'ref' => '#form', - 'name' => 'title', - 'default' => __( 'Potentials', 'forms-bridge' ), - ), - array( - 'ref' => '#bridge', - 'name' => 'endpoint', - 'value' => 'Potentials', - ), - array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'assigned_user_id', - 'label' => __( 'Assigned User', 'forms-bridge' ), - 'description' => __( - 'User to assign the potential to', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - 'endpoint' => 'Users', - 'finger' => array( - 'value' => 'result.[].id', - 'label' => 'result.[].label', - ), - ), - ), - array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'related_to', - 'label' => __( 'Related Account', 'forms-bridge' ), - 'description' => __( - 'Related account for this potential', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - 'endpoint' => 'Accounts', - 'finger' => array( - 'value' => 'result.[].id', - 'label' => 'result.[].label', - ), - ), - ), - array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'sales_stage', - 'label' => __( 'Sales Stage', 'forms-bridge' ), - 'description' => __( - 'Current stage in the sales process', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - array( - 'value' => 'Prospecting', - 'label' => __( 'Prospecting', 'forms-bridge' ), - ), - array( - 'value' => 'Qualification', - 'label' => __( 'Qualification', 'forms-bridge' ), - ), - array( - 'value' => 'Needs Analysis', - 'label' => __( 'Needs Analysis', 'forms-bridge' ), - ), - array( - 'value' => 'Value Proposition', - 'label' => __( 'Value Proposition', 'forms-bridge' ), - ), - array( - 'value' => 'Id. Decision Makers', - 'label' => __( 'Identifying Decision Makers', 'forms-bridge' ), - ), - array( - 'value' => 'Perception Analysis', - 'label' => __( 'Perception Analysis', 'forms-bridge' ), - ), - array( - 'value' => 'Proposal/Price Quote', - 'label' => __( 'Proposal/Price Quote', 'forms-bridge' ), - ), - array( - 'value' => 'Negotiation/Review', - 'label' => __( 'Negotiation/Review', 'forms-bridge' ), - ), - array( - 'value' => 'Closed Won', - 'label' => __( 'Closed Won', 'forms-bridge' ), - ), - array( - 'value' => 'Closed Lost', - 'label' => __( 'Closed Lost', 'forms-bridge' ), - ), - ), - 'default' => 'Prospecting', - ), - array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'leadsource', - 'label' => __( 'Lead Source', 'forms-bridge' ), - 'description' => __( - 'Source of the potential', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - array( - 'value' => 'Web Site', - 'label' => __( 'Web Site', 'forms-bridge' ), - ), - array( - 'value' => 'Cold Call', - 'label' => __( 'Cold Call', 'forms-bridge' ), - ), - array( - 'value' => 'Email', - 'label' => __( 'Email', 'forms-bridge' ), - ), - array( - 'value' => 'Existing Customer', - 'label' => __( 'Existing Customer', 'forms-bridge' ), - ), - array( - 'value' => 'Word of mouth', - 'label' => __( 'Word of Mouth', 'forms-bridge' ), - ), - array( - 'value' => 'Campaign', - 'label' => __( 'Campaign', 'forms-bridge' ), - ), - array( - 'value' => 'Conference', - 'label' => __( 'Conference', 'forms-bridge' ), - ), - array( - 'value' => 'Trade Show', - 'label' => __( 'Trade Show', 'forms-bridge' ), - ), - array( - 'value' => 'Partner', - 'label' => __( 'Partner', 'forms-bridge' ), - ), - array( - 'value' => 'Self Generated', - 'label' => __( 'Self Generated', 'forms-bridge' ), - ), - array( - 'value' => 'Other', - 'label' => __( 'Other', 'forms-bridge' ), - ), - ), - 'default' => 'Web Site', - ), - array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'opportunity_type', - 'label' => __( 'Opportunity Type', 'forms-bridge' ), - 'description' => __( - 'Type of business opportunity', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - array( - 'value' => 'Existing Business', - 'label' => __( 'Existing Business', 'forms-bridge' ), - ), - array( - 'value' => 'New Business', - 'label' => __( 'New Business', 'forms-bridge' ), - ), - ), - 'default' => 'New Business', - ), - ), - 'bridge' => array( - 'endpoint' => 'Potentials', - 'method' => 'create', - 'custom_fields' => array( - array( - 'name' => 'sales_stage', - 'value' => 'Prospecting', - ), - array( - 'name' => 'leadsource', - 'value' => 'Web Site', - ), - array( - 'name' => 'opportunity_type', - 'value' => 'New Business', - ), - ), - 'mutations' => array( - array( - array( - 'from' => 'potentialname', - 'to' => 'potentialname', - 'cast' => 'string', - ), - array( - 'from' => '?amount', - 'to' => 'amount', - 'cast' => 'number', - ), - array( - 'from' => '?closingdate', - 'to' => 'closingdate', - 'cast' => 'string', - ), - array( - 'from' => '?probability', - 'to' => 'probability', - 'cast' => 'number', - ), - array( - 'from' => '?nextstep', - 'to' => 'nextstep', - 'cast' => 'string', - ), - array( - 'from' => '?description', - 'to' => 'description', - 'cast' => 'string', - ), - array( - 'from' => '?sales_stage', - 'to' => 'sales_stage', - 'cast' => 'string', - ), - array( - 'from' => '?leadsource', - 'to' => 'leadsource', - 'cast' => 'string', - ), - array( - 'from' => '?opportunity_type', - 'to' => 'opportunity_type', - 'cast' => 'string', - ), - array( - 'from' => '?related_to', - 'to' => 'related_to', - 'cast' => 'string', - ), - array( - 'from' => '?assigned_user_id', - 'to' => 'assigned_user_id', - 'cast' => 'string', - ), - ), - ), - ), - 'form' => array( - 'fields' => array( - array( - 'label' => __( 'Potential Name', 'forms-bridge' ), - 'name' => 'potentialname', - 'type' => 'text', - 'required' => true, - ), - array( - 'label' => __( 'Amount', 'forms-bridge' ), - 'name' => 'amount', - 'type' => 'number', - 'required' => true, - ), - array( - 'label' => __( 'Expected Close Date', 'forms-bridge' ), - 'name' => 'closingdate', - 'type' => 'date', - 'required' => true, - 'description' => __( 'Format: YYYY-MM-DD', 'forms-bridge' ), - ), - array( - 'label' => __( 'Probability (%)', 'forms-bridge' ), - 'name' => 'probability', - 'type' => 'number', - 'description' => __( 'Likelihood of closing (0-100)', 'forms-bridge' ), - ), - array( - 'label' => __( 'Next Step', 'forms-bridge' ), - 'name' => 'nextstep', - 'type' => 'text', - ), - array( - 'label' => __( 'Description', 'forms-bridge' ), - 'name' => 'description', - 'type' => 'textarea', - ), - ), - ), -); From 8629868a5950b005b5a6d760776b20128c85de3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 02:21:14 +0100 Subject: [PATCH 08/12] fix: form step validation when title is a form field --- src/components/Templates/Wizard/Steps/FormStep.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Templates/Wizard/Steps/FormStep.jsx b/src/components/Templates/Wizard/Steps/FormStep.jsx index ef1d5a3..8eece2b 100644 --- a/src/components/Templates/Wizard/Steps/FormStep.jsx +++ b/src/components/Templates/Wizard/Steps/FormStep.jsx @@ -91,8 +91,7 @@ export default function FormStep({ fields, data, setData, integration }) { useEffect(() => { if (!form) return; - const data = { id: form.id }; - schema.fields.forEach((schema) => { + const data = schema.fields.reduce((data, schema) => { switch (schema.type) { case "object": data[schema.name] = []; @@ -103,8 +102,12 @@ export default function FormStep({ fields, data, setData, integration }) { default: data[schema.name] = ""; } - }); + return data; + }, {}); + + data.id = form.id; + data.title = form.title; setData(data); }, [form]); From e1b0690ba6ddd526170c07136c5b210d46398df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 03:56:53 +0100 Subject: [PATCH 09/12] feat: update suitecrm templates --- .../addons/suitecrm/class-suitecrm-addon.php | 7 +- .../suitecrm/class-suitecrm-form-bridge.php | 2 +- forms-bridge/addons/suitecrm/jobs/contact.php | 4 +- .../addons/suitecrm/templates/accounts.php | 106 +++++++++++++++--- .../addons/suitecrm/templates/contacts.php | 35 +++++- .../addons/suitecrm/templates/leads.php | 54 ++++----- .../addons/suitecrm/templates/meetings.php | 63 +++++++---- .../vtiger/class-vtiger-form-bridge.php | 2 +- .../addons/vtiger/templates/accounts.php | 2 +- .../addons/vtiger/templates/meetings.php | 2 +- 10 files changed, 188 insertions(+), 89 deletions(-) diff --git a/forms-bridge/addons/suitecrm/class-suitecrm-addon.php b/forms-bridge/addons/suitecrm/class-suitecrm-addon.php index ea5d0c5..f261def 100644 --- a/forms-bridge/addons/suitecrm/class-suitecrm-addon.php +++ b/forms-bridge/addons/suitecrm/class-suitecrm-addon.php @@ -88,12 +88,7 @@ public function fetch( $endpoint, $backend ) { ) ); - return $bridge->submit( - array( - 'select_fields' => array( 'id', 'name' ), - 'max_results' => 100, - ) - ); + return $bridge->submit( array( 'max_results' => 100 ) ); } /** diff --git a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php index 195cc7c..7aab83f 100644 --- a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php +++ b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php @@ -198,7 +198,7 @@ public function submit( $payload = array(), $attachments = array() ) { add_filter( 'http_bridge_request', static function ( $request ) { - unset( $request['args']['headers']['authentication'] ); + unset( $request['args']['headers']['Authorization'] ); self::$request = $request; return $request; }, diff --git a/forms-bridge/addons/suitecrm/jobs/contact.php b/forms-bridge/addons/suitecrm/jobs/contact.php index b7a4f4b..d628500 100644 --- a/forms-bridge/addons/suitecrm/jobs/contact.php +++ b/forms-bridge/addons/suitecrm/jobs/contact.php @@ -203,8 +203,8 @@ /** * Creates a new contact and add its ID to the payload. * - * @param array $payload Bridge payload. - * @param Form_Bridge $bridge Bridge object. + * @param array $payload Bridge payload. + * @param SuiteCRM_Form_Bridge $bridge Bridge object. * * @return array */ diff --git a/forms-bridge/addons/suitecrm/templates/accounts.php b/forms-bridge/addons/suitecrm/templates/accounts.php index ab6746f..826accc 100644 --- a/forms-bridge/addons/suitecrm/templates/accounts.php +++ b/forms-bridge/addons/suitecrm/templates/accounts.php @@ -12,7 +12,7 @@ return array( 'title' => __( 'Accounts', 'forms-bridge' ), 'description' => __( - 'Account bridge template. The resulting bridge will convert form submissions into SuiteCRM accounts (companies/organizations).', + 'Account form bridge template. The resulting bridge will convert form submissions into SuiteCRM accounts (companies/organizations).', 'forms-bridge' ), 'fields' => array( @@ -27,15 +27,11 @@ 'value' => 'Contacts', ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'assigned_user_id', - 'label' => __( 'Assigned User', 'forms-bridge' ), - 'description' => __( - 'User to assign the account to', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( 'endpoint' => 'Users', 'finger' => array( 'value' => 'entry_list[].id', @@ -146,6 +142,10 @@ 'value' => 'Entertainment', 'label' => __( 'Entertainment', 'forms-bridge' ), ), + array( + 'value' => 'Environmental', + 'label' => __( 'Environmental', 'forms-bridge' ), + ), array( 'value' => 'Finance', 'label' => __( 'Finance', 'forms-bridge' ), @@ -166,6 +166,10 @@ 'value' => 'Insurance', 'label' => __( 'Insurance', 'forms-bridge' ), ), + array( + 'value' => 'Machinery', + 'label' => __( 'Machinery', 'forms-bridge' ), + ), array( 'value' => 'Manufacturing', 'label' => __( 'Manufacturing', 'forms-bridge' ), @@ -174,18 +178,38 @@ 'value' => 'Media', 'label' => __( 'Media', 'forms-bridge' ), ), + array( + 'value' => 'Not For Profit', + 'label' => __( 'Not For Profit', 'forms-bridge' ), + ), + array( + 'value' => 'Recreation', + 'label' => __( 'Recreation', 'forms-bridge' ), + ), array( 'value' => 'Retail', 'label' => __( 'Retail', 'forms-bridge' ), ), + array( + 'value' => 'Shipping', + 'label' => __( 'Shipping', 'forms-bridge' ), + ), array( 'value' => 'Technology', 'label' => __( 'Technology', 'forms-bridge' ), ), + array( + 'value' => 'Telecomunications', + 'label' => __( 'Telecomunications', 'forms-bridge' ), + ), array( 'value' => 'Transportation', 'label' => __( 'Transportation', 'forms-bridge' ), ), + array( + 'value' => 'Utilities', + 'label' => __( 'Utilities', 'forms-bridge' ), + ), array( 'value' => 'Other', 'label' => __( 'Other', 'forms-bridge' ), @@ -206,10 +230,30 @@ 'value' => 'Cold Call', 'label' => __( 'Cold Call', 'forms-bridge' ), ), + array( + 'value' => 'Existing Customer', + 'label' => __( 'Existing Customer', 'forms-bridge' ), + ), + array( + 'value' => 'Employee', + 'label' => __( 'Employee', 'forms-bridge' ), + ), + array( + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), + ), + array( + 'value' => 'Public Relations', + 'label' => __( 'Public Relations', 'forms-bridge' ), + ), array( 'value' => 'Email', 'label' => __( 'Email', 'forms-bridge' ), ), + array( + 'value' => 'Direct Mail', + 'label' => __( 'Direct Mail', 'forms-bridge' ), + ), array( 'value' => 'Word of mouth', 'label' => __( 'Word of Mouth', 'forms-bridge' ), @@ -218,6 +262,14 @@ 'value' => 'Campaign', 'label' => __( 'Campaign', 'forms-bridge' ), ), + array( + 'value' => 'Conference', + 'label' => __( 'Conference', 'forms-bridge' ), + ), + array( + 'value' => 'Trade Show', + 'label' => __( 'Trade Show', 'forms-bridge' ), + ), array( 'value' => 'Other', 'label' => __( 'Other', 'forms-bridge' ), @@ -227,9 +279,25 @@ ), ), 'bridge' => array( - 'endpoint' => 'Contacts', - 'method' => 'set_entry', - 'workflow' => array( 'account', 'skip-contact' ), + 'endpoint' => 'Contacts', + 'method' => 'set_entry', + 'workflow' => array( 'account', 'skip-contact' ), + 'mutations' => array( + array( + array( + 'from' => 'email1', + 'to' => 'user_email', + 'cast' => 'copy', + ), + ), + array( + array( + 'from' => 'user_email', + 'to' => 'email1', + 'cast' => 'string', + ), + ), + ), ), 'form' => array( 'fields' => array( @@ -252,9 +320,15 @@ 'required' => true, ), array( - 'label' => __( 'Email', 'forms-bridge' ), - 'name' => 'email1', - 'type' => 'email', + 'label' => __( 'Email', 'forms-bridge' ), + 'name' => 'email1', + 'type' => 'email', + 'required' => true, + ), + array( + 'label' => __( 'Title', 'forms-bridge' ), + 'name' => 'title', + 'type' => 'text', ), array( 'label' => __( 'Phone', 'forms-bridge' ), diff --git a/forms-bridge/addons/suitecrm/templates/contacts.php b/forms-bridge/addons/suitecrm/templates/contacts.php index 415e51e..bf73503 100644 --- a/forms-bridge/addons/suitecrm/templates/contacts.php +++ b/forms-bridge/addons/suitecrm/templates/contacts.php @@ -12,7 +12,7 @@ return array( 'title' => __( 'Contacts', 'forms-bridge' ), 'description' => __( - 'Contact form template. The resulting bridge will convert form submissions into SuiteCRM contacts.', + 'Contact form bridge template. The resulting bridge will convert form submissions into SuiteCRM contacts.', 'forms-bridge' ), 'fields' => array( @@ -53,10 +53,30 @@ 'value' => 'Cold Call', 'label' => __( 'Cold Call', 'forms-bridge' ), ), + array( + 'value' => 'Existing Customer', + 'label' => __( 'Existing Customer', 'forms-bridge' ), + ), + array( + 'value' => 'Employee', + 'label' => __( 'Employee', 'forms-bridge' ), + ), + array( + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), + ), + array( + 'value' => 'Public Relations', + 'label' => __( 'Public Relations', 'forms-bridge' ), + ), array( 'value' => 'Email', 'label' => __( 'Email', 'forms-bridge' ), ), + array( + 'value' => 'Direct Mail', + 'label' => __( 'Direct Mail', 'forms-bridge' ), + ), array( 'value' => 'Word of mouth', 'label' => __( 'Word of Mouth', 'forms-bridge' ), @@ -65,6 +85,14 @@ 'value' => 'Campaign', 'label' => __( 'Campaign', 'forms-bridge' ), ), + array( + 'value' => 'Conference', + 'label' => __( 'Conference', 'forms-bridge' ), + ), + array( + 'value' => 'Trade Show', + 'label' => __( 'Trade Show', 'forms-bridge' ), + ), array( 'value' => 'Other', 'label' => __( 'Other', 'forms-bridge' ), @@ -103,11 +131,6 @@ 'name' => 'phone_work', 'type' => 'tel', ), - array( - 'label' => __( 'Mobile', 'forms-bridge' ), - 'name' => 'phone_mobile', - 'type' => 'tel', - ), array( 'label' => __( 'Address', 'forms-bridge' ), 'name' => 'primary_address_street', diff --git a/forms-bridge/addons/suitecrm/templates/leads.php b/forms-bridge/addons/suitecrm/templates/leads.php index fde7796..d2ba716 100644 --- a/forms-bridge/addons/suitecrm/templates/leads.php +++ b/forms-bridge/addons/suitecrm/templates/leads.php @@ -86,10 +86,30 @@ 'value' => 'Cold Call', 'label' => __( 'Cold Call', 'forms-bridge' ), ), + array( + 'value' => 'Existing Customer', + 'label' => __( 'Existing Customer', 'forms-bridge' ), + ), + array( + 'value' => 'Employee', + 'label' => __( 'Employee', 'forms-bridge' ), + ), + array( + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), + ), + array( + 'value' => 'Public Relations', + 'label' => __( 'Public Relations', 'forms-bridge' ), + ), array( 'value' => 'Email', 'label' => __( 'Email', 'forms-bridge' ), ), + array( + 'value' => 'Direct Mail', + 'label' => __( 'Direct Mail', 'forms-bridge' ), + ), array( 'value' => 'Word of mouth', 'label' => __( 'Word of Mouth', 'forms-bridge' ), @@ -106,10 +126,6 @@ 'value' => 'Trade Show', 'label' => __( 'Trade Show', 'forms-bridge' ), ), - array( - 'value' => 'Partner', - 'label' => __( 'Partner', 'forms-bridge' ), - ), array( 'value' => 'Other', 'label' => __( 'Other', 'forms-bridge' ), @@ -147,11 +163,6 @@ 'name' => 'phone_work', 'type' => 'tel', ), - array( - 'label' => __( 'Mobile', 'forms-bridge' ), - 'name' => 'phone_mobile', - 'type' => 'tel', - ), array( 'label' => __( 'Company', 'forms-bridge' ), 'name' => 'account_name', @@ -167,31 +178,6 @@ 'name' => 'website', 'type' => 'url', ), - array( - 'label' => __( 'Address', 'forms-bridge' ), - 'name' => 'primary_address_street', - 'type' => 'text', - ), - array( - 'label' => __( 'City', 'forms-bridge' ), - 'name' => 'primary_address_city', - 'type' => 'text', - ), - array( - 'label' => __( 'Postal Code', 'forms-bridge' ), - 'name' => 'primary_address_postalcode', - 'type' => 'text', - ), - array( - 'label' => __( 'State', 'forms-bridge' ), - 'name' => 'primary_address_state', - 'type' => 'text', - ), - array( - 'label' => __( 'Country', 'forms-bridge' ), - 'name' => 'primary_address_country', - 'type' => 'text', - ), array( 'label' => __( 'Message', 'forms-bridge' ), 'name' => 'description', diff --git a/forms-bridge/addons/suitecrm/templates/meetings.php b/forms-bridge/addons/suitecrm/templates/meetings.php index 26bdf6d..2239ba5 100644 --- a/forms-bridge/addons/suitecrm/templates/meetings.php +++ b/forms-bridge/addons/suitecrm/templates/meetings.php @@ -12,7 +12,7 @@ return array( 'title' => __( 'Meetings', 'forms-bridge' ), 'description' => __( - 'Meetings bridge template. The resulting bridge will convert form submissions into SuiteCRM meetings.', + 'Meetings form bridge template. The resulting bridge will convert form submissions into SuiteCRM meetings.', 'forms-bridge', ), 'fields' => array( @@ -61,22 +61,18 @@ 'required' => true, ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'meeting_assigned_user_id', - 'label' => __( 'Assigned User', 'forms-bridge' ), - 'description' => __( - 'User to assign the account to', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( 'endpoint' => 'Users', 'finger' => array( 'value' => 'entry_list[].id', 'label' => 'entry_list[].name_value_list.name.value', ), ), - 'required' => true, + 'required' => true, ), array( 'ref' => '#bridge/custom_fields[]', @@ -92,10 +88,30 @@ 'value' => 'Cold Call', 'label' => __( 'Cold Call', 'forms-bridge' ), ), + array( + 'value' => 'Existing Customer', + 'label' => __( 'Existing Customer', 'forms-bridge' ), + ), + array( + 'value' => 'Employee', + 'label' => __( 'Employee', 'forms-bridge' ), + ), + array( + 'value' => 'Partner', + 'label' => __( 'Partner', 'forms-bridge' ), + ), + array( + 'value' => 'Public Relations', + 'label' => __( 'Public Relations', 'forms-bridge' ), + ), array( 'value' => 'Email', 'label' => __( 'Email', 'forms-bridge' ), ), + array( + 'value' => 'Direct Mail', + 'label' => __( 'Direct Mail', 'forms-bridge' ), + ), array( 'value' => 'Word of mouth', 'label' => __( 'Word of Mouth', 'forms-bridge' ), @@ -104,6 +120,14 @@ 'value' => 'Campaign', 'label' => __( 'Campaign', 'forms-bridge' ), ), + array( + 'value' => 'Conference', + 'label' => __( 'Conference', 'forms-bridge' ), + ), + array( + 'value' => 'Trade Show', + 'label' => __( 'Trade Show', 'forms-bridge' ), + ), array( 'value' => 'Other', 'label' => __( 'Other', 'forms-bridge' ), @@ -120,10 +144,6 @@ 'name' => 'meeting_status', 'value' => 'Planned', ), - array( - 'name' => 'meeting_type', - 'value' => 'Sugar', - ), array( 'name' => 'parent_type', 'value' => 'Contacts', @@ -131,7 +151,13 @@ ), 'workflow' => array( 'date-fields-to-date', 'contact', 'meeting-invitees' ), 'mutations' => array( - array(), + array( + array( + 'from' => 'assigned_user_id', + 'to' => 'meeting_assigned_user_id', + 'cast' => 'copy', + ), + ), array( array( 'from' => 'first_name', @@ -224,11 +250,6 @@ 'name' => 'phone_work', 'type' => 'tel', ), - array( - 'label' => __( 'Mobile', 'forms-bridge' ), - 'name' => 'phone_mobile', - 'type' => 'tel', - ), array( 'name' => 'date', 'label' => __( 'Date', 'forms-bridge' ), diff --git a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php index ce1ba4e..026a282 100644 --- a/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php +++ b/forms-bridge/addons/vtiger/class-vtiger-form-bridge.php @@ -227,7 +227,7 @@ public function submit( $payload = array(), $more_args = array() ) { add_filter( 'http_bridge_request', static function ( $request ) { - unset( $request['args']['headers']['authentication'] ); + unset( $request['args']['headers']['Authorization'] ); self::$request = $request; return $request; }, diff --git a/forms-bridge/addons/vtiger/templates/accounts.php b/forms-bridge/addons/vtiger/templates/accounts.php index 6d0c39c..af8dc96 100644 --- a/forms-bridge/addons/vtiger/templates/accounts.php +++ b/forms-bridge/addons/vtiger/templates/accounts.php @@ -12,7 +12,7 @@ return array( 'title' => __( 'Accounts', 'forms-bridge' ), 'description' => __( - 'Account form template. The resulting bridge will convert form submissions into Vtiger accounts (organizations).', + 'Account form bridge template. The resulting bridge will convert form submissions into Vtiger accounts (organizations).', 'forms-bridge' ), 'fields' => array( diff --git a/forms-bridge/addons/vtiger/templates/meetings.php b/forms-bridge/addons/vtiger/templates/meetings.php index f0e3e99..0005ced 100644 --- a/forms-bridge/addons/vtiger/templates/meetings.php +++ b/forms-bridge/addons/vtiger/templates/meetings.php @@ -12,7 +12,7 @@ return array( 'title' => __( 'Meetings', 'forms-bridge' ), 'description' => __( - 'Meetings bridge template. The resulting bridge will convert form submissions into Vtiger meetings.', + 'Meetings form bridge template. The resulting bridge will convert form submissions into Vtiger meetings.', 'forms-bridge', ), 'fields' => array( From ebf4c3b4a9613ecf2b07017b53e14eb50907b6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 04:26:52 +0100 Subject: [PATCH 10/12] feat: vtiger addon test case --- tests/addons/test-odoo.php | 24 +- tests/addons/test-suitecrm.php | 50 -- tests/addons/test-vtiger.php | 829 +++++++++++++++++++++++++++++++++ 3 files changed, 844 insertions(+), 59 deletions(-) create mode 100644 tests/addons/test-vtiger.php diff --git a/tests/addons/test-odoo.php b/tests/addons/test-odoo.php index 2b4c816..d433dd3 100644 --- a/tests/addons/test-odoo.php +++ b/tests/addons/test-odoo.php @@ -250,19 +250,25 @@ private static function get_mock_response_data( $method, $params ) { array( 'result' => array( 'id' => array( - 'name' => 'id', - 'string' => 'ID', - 'type' => 'integer', + 'name' => 'id', + 'string' => 'ID', + 'type' => 'integer', + 'required' => false, + 'readonly' => false, ), 'name' => array( - 'name' => 'name', - 'string' => 'Name', - 'type' => 'char', + 'name' => 'name', + 'string' => 'Name', + 'type' => 'char', + 'required' => true, + 'readonly' => false, ), 'email' => array( - 'name' => 'email', - 'string' => 'Email', - 'type' => 'char', + 'name' => 'email', + 'string' => 'Email', + 'type' => 'char', + 'required' => false, + 'readonly' => false, ), ), ), diff --git a/tests/addons/test-suitecrm.php b/tests/addons/test-suitecrm.php index dd84106..1cadbb5 100644 --- a/tests/addons/test-suitecrm.php +++ b/tests/addons/test-suitecrm.php @@ -565,56 +565,6 @@ public function test_addon_get_endpoint_schema() { $this->assertContains( 'last_name', $field_names ); } - /** - * Test that templates exist and are valid. - */ - public function test_templates_exist() { - $templates_dir = dirname( dirname( __DIR__ ) ) . '/forms-bridge/addons/suitecrm/templates/'; - - $this->assertFileExists( $templates_dir . 'contacts.php' ); - $this->assertFileExists( $templates_dir . 'leads.php' ); - $this->assertFileExists( $templates_dir . 'accounts.php' ); - } - - /** - * Test contacts template structure. - */ - public function test_contacts_template_structure() { - $template = include dirname( dirname( __DIR__ ) ) . '/forms-bridge/addons/suitecrm/templates/contacts.php'; - - $this->assertIsArray( $template ); - $this->assertArrayHasKey( 'title', $template ); - $this->assertArrayHasKey( 'description', $template ); - $this->assertArrayHasKey( 'fields', $template ); - $this->assertArrayHasKey( 'bridge', $template ); - $this->assertArrayHasKey( 'form', $template ); - - $this->assertEquals( 'Contacts', $template['bridge']['endpoint'] ); - $this->assertEquals( 'set_entry', $template['bridge']['method'] ); - } - - /** - * Test leads template structure. - */ - public function test_leads_template_structure() { - $template = include dirname( dirname( __DIR__ ) ) . '/forms-bridge/addons/suitecrm/templates/leads.php'; - - $this->assertIsArray( $template ); - $this->assertEquals( 'Leads', $template['bridge']['endpoint'] ); - $this->assertEquals( 'set_entry', $template['bridge']['method'] ); - } - - /** - * Test accounts template structure. - */ - public function test_accounts_template_structure() { - $template = include dirname( dirname( __DIR__ ) ) . '/forms-bridge/addons/suitecrm/templates/accounts.php'; - - $this->assertIsArray( $template ); - $this->assertEquals( 'Contacts', $template['bridge']['endpoint'] ); - $this->assertEquals( 'set_entry', $template['bridge']['method'] ); - } - /** * Test bridge schema hook is applied. */ diff --git a/tests/addons/test-vtiger.php b/tests/addons/test-vtiger.php new file mode 100644 index 0000000..4ab3a6e --- /dev/null +++ b/tests/addons/test-vtiger.php @@ -0,0 +1,829 @@ + 'vtiger-test-credential', + 'schema' => 'Basic', + 'client_id' => 'admin', + 'client_secret' => 'accessKey123', + ) + ), + ); + } + + /** + * Test backend provider. + * + * @return Backend[] + */ + public static function backends_provider() { + return array( + new Backend( + array( + 'name' => 'vtiger-test-backend', + 'base_url' => 'https://vtiger.example.coop', + 'credential' => 'vtiger-test-credential', + 'headers' => array( + array( + 'name' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), + array( + 'name' => 'Accept', + 'value' => 'application/json', + ), + ), + ) + ), + ); + } + + /** + * HTTP requests interceptor. + * + * @param mixed $pre Initial pre hook value. + * @param array $args Request arguments. + * @param string $url Request URL. + * + * @return array + */ + public static function pre_http_request( $pre, $args, $url ) { + self::$request = array( + 'args' => $args, + 'url' => $url, + ); + + // Parse URL to determine the operation being called. + $parsed_url = wp_parse_url( $url ); + $query = array(); + if ( ! empty( $parsed_url['query'] ) ) { + parse_str( $parsed_url['query'], $query ); + } + + $operation = $query['operation'] ?? ''; + + // Check if this is a POST request (for create/update/delete operations). + $body = array(); + if ( ! empty( $args['body'] ) ) { + if ( is_string( $args['body'] ) ) { + parse_str( $args['body'], $body ); + } else { + $body = $args['body']; + } + $operation = $body['operation'] ?? $operation; + } + + // Return appropriate mock response based on operation. + if ( self::$mock_response ) { + $response_body = self::$mock_response; + } else { + $response_body = self::get_mock_response( $operation, $query, $body ); + } + + return array( + 'response' => array( + 'code' => 200, + 'message' => 'Success', + ), + 'headers' => array( 'Content-Type' => 'application/json' ), + 'cookies' => array(), + 'body' => wp_json_encode( $response_body ), + 'http_response' => null, + ); + } + + /** + * Get mock response based on API operation. + * + * @param string $operation API operation name. + * @param array $query Query parameters. + * @param array $body Request body. + * + * @return array Mock response. + */ + private static function get_mock_response( $operation, $query, $body ) { + switch ( $operation ) { + case 'getchallenge': + return array( + 'success' => true, + 'result' => array( + 'token' => 'test-challenge-token-12345', + 'serverTime' => time(), + 'expireTime' => time() + 300, + ), + ); + + case 'login': + return array( + 'success' => true, + 'result' => array( + 'sessionName' => 'test-session-id-67890', + 'userId' => '19x1', + 'version' => '7.4.0', + 'vtigerVersion' => '7.4.0', + ), + ); + + case 'listtypes': + return array( + 'success' => true, + 'result' => array( + 'types' => array( + 'Contacts', + 'Leads', + 'Accounts', + 'Potentials', + 'Calendar', + ), + 'information' => array( + 'Contacts' => array( + 'isEntity' => true, + 'label' => 'Contacts', + 'singular' => 'Contact', + ), + 'Leads' => array( + 'isEntity' => true, + 'label' => 'Leads', + 'singular' => 'Lead', + ), + 'Accounts' => array( + 'isEntity' => true, + 'label' => 'Accounts', + 'singular' => 'Account', + ), + 'Potentials' => array( + 'isEntity' => true, + 'label' => 'Potentials', + 'singular' => 'Potential', + ), + 'Calendar' => array( + 'isEntity' => true, + 'label' => 'Calendar', + 'singular' => 'Event', + ), + ), + ), + ); + + case 'describe': + return array( + 'success' => true, + 'result' => array( + 'label' => 'Contacts', + 'name' => 'Contacts', + 'createable' => true, + 'updateable' => true, + 'deleteable' => true, + 'retrieveable' => true, + 'fields' => array( + array( + 'name' => 'firstname', + 'label' => 'First Name', + 'mandatory' => false, + 'type' => array( + 'name' => 'string', + ), + 'nullable' => true, + 'editable' => true, + ), + array( + 'name' => 'lastname', + 'label' => 'Last Name', + 'mandatory' => true, + 'type' => array( + 'name' => 'string', + ), + 'nullable' => false, + 'editable' => true, + ), + array( + 'name' => 'email', + 'label' => 'Email', + 'mandatory' => false, + 'type' => array( + 'name' => 'email', + ), + 'nullable' => true, + 'editable' => true, + ), + array( + 'name' => 'phone', + 'label' => 'Office Phone', + 'mandatory' => false, + 'type' => array( + 'name' => 'phone', + ), + 'nullable' => true, + 'editable' => true, + ), + ), + ), + ); + + case 'query': + return array( + 'success' => true, + 'result' => array( + array( + 'id' => '4x11', + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'john.doe@example.com', + ), + array( + 'id' => '4x12', + 'firstname' => 'Jane', + 'lastname' => 'Smith', + 'email' => 'jane.smith@example.com', + ), + ), + ); + + case 'retrieve': + return array( + 'success' => true, + 'result' => array( + 'id' => '4x11', + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'john.doe@example.com', + 'phone' => '555-1234', + ), + ); + + case 'create': + $element = array(); + if ( ! empty( $body['element'] ) ) { + $element = json_decode( $body['element'], true ); + } + return array( + 'success' => true, + 'result' => array_merge( + array( + 'id' => '4x123', + 'assigned_user_id' => '19x1', + ), + $element + ), + ); + + case 'update': + $element = array(); + if ( ! empty( $body['element'] ) ) { + $element = json_decode( $body['element'], true ); + } + return array( + 'success' => true, + 'result' => $element, + ); + + case 'delete': + return array( + 'success' => true, + 'result' => array( + 'status' => 'successful', + ), + ); + + case 'sync': + return array( + 'success' => true, + 'result' => array( + 'updated' => array(), + 'deleted' => array(), + ), + ); + + default: + return array( + 'success' => true, + 'result' => array(), + ); + } + } + + /** + * Set up test fixtures. + */ + public function set_up() { + parent::set_up(); + + self::$request = null; + self::$mock_response = null; + + tests_add_filter( 'http_bridge_credentials', array( self::class, 'credentials_provider' ), 10, 0 ); + tests_add_filter( 'http_bridge_backends', array( self::class, 'backends_provider' ), 10, 0 ); + tests_add_filter( 'pre_http_request', array( self::class, 'pre_http_request' ), 10, 3 ); + } + + /** + * Tear down test filters. + */ + public function tear_down() { + remove_filter( 'http_bridge_credentials', array( self::class, 'credentials_provider' ), 10, 0 ); + remove_filter( 'http_bridge_backends', array( self::class, 'backends_provider' ), 10, 0 ); + remove_filter( 'pre_http_request', array( self::class, 'pre_http_request' ), 10, 3 ); + + parent::tear_down(); + } + + /** + * Test that the addon class exists and has correct constants. + */ + public function test_addon_class_exists() { + $this->assertTrue( class_exists( 'FORMS_BRIDGE\Vtiger_Addon' ) ); + $this->assertEquals( 'Vtiger', Vtiger_Addon::TITLE ); + $this->assertEquals( 'vtiger', Vtiger_Addon::NAME ); + $this->assertEquals( '\FORMS_BRIDGE\Vtiger_Form_Bridge', Vtiger_Addon::BRIDGE ); + } + + /** + * Test that the form bridge class exists. + */ + public function test_form_bridge_class_exists() { + $this->assertTrue( class_exists( 'FORMS_BRIDGE\Vtiger_Form_Bridge' ) ); + } + + /** + * Test bridge validation with valid data. + */ + public function test_bridge_validation() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-vtiger-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'create', + ) + ); + + $this->assertTrue( $bridge->is_valid ); + } + + /** + * Test bridge validation with invalid data. + */ + public function test_bridge_validation_invalid() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'invalid-bridge', + // Missing required fields. + ) + ); + + $this->assertFalse( $bridge->is_valid ); + } + + /** + * Test successful challenge-response authentication flow. + */ + public function test_authentication_flow() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-auth-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'query', + ) + ); + + $response = $bridge->submit(); + + $this->assertFalse( is_wp_error( $response ) ); + + // Verify the request URL contains the Vtiger webservice endpoint. + $this->assertStringContainsString( '/webservice.php', self::$request['url'] ); + } + + /** + * Test listtypes operation. + */ + public function test_listtypes() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-listtypes-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => '', + 'method' => 'listtypes', + ) + ); + + $response = $bridge->submit(); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertArrayHasKey( 'result', $response['data'] ); + $this->assertArrayHasKey( 'types', $response['data']['result'] ); + $this->assertContains( 'Contacts', $response['data']['result']['types'] ); + $this->assertContains( 'Leads', $response['data']['result']['types'] ); + $this->assertContains( 'Accounts', $response['data']['result']['types'] ); + } + + /** + * Test describe operation. + */ + public function test_describe() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-describe-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'describe', + ) + ); + + $response = $bridge->submit(); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertArrayHasKey( 'result', $response['data'] ); + $this->assertArrayHasKey( 'fields', $response['data']['result'] ); + $this->assertNotEmpty( $response['data']['result']['fields'] ); + } + + /** + * Test query operation. + */ + public function test_query() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-query-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'query', + ) + ); + + $response = $bridge->submit( + array( + 'query' => 'SELECT * FROM Contacts;', + ) + ); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertArrayHasKey( 'result', $response['data'] ); + $this->assertIsArray( $response['data']['result'] ); + $this->assertCount( 2, $response['data']['result'] ); + } + + /** + * Test retrieve operation. + */ + public function test_retrieve() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-retrieve-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'retrieve', + ) + ); + + $response = $bridge->submit( + array( + 'id' => '4x11', + ) + ); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertArrayHasKey( 'result', $response['data'] ); + $this->assertEquals( '4x11', $response['data']['result']['id'] ); + $this->assertEquals( 'John', $response['data']['result']['firstname'] ); + } + + /** + * Test create operation. + */ + public function test_create() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-create-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'create', + ) + ); + + $payload = array( + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'john.doe@example.com', + ); + + $response = $bridge->submit( $payload ); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertArrayHasKey( 'result', $response['data'] ); + $this->assertArrayHasKey( 'id', $response['data']['result'] ); + $this->assertEquals( '4x123', $response['data']['result']['id'] ); + } + + /** + * Test update operation. + */ + public function test_update() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-update-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'update', + ) + ); + + $payload = array( + 'id' => '4x11', + 'firstname' => 'Jane', + 'lastname' => 'Doe', + ); + + $response = $bridge->submit( $payload ); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertArrayHasKey( 'result', $response['data'] ); + } + + /** + * Test delete operation. + */ + public function test_delete() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-delete-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'delete', + ) + ); + + $payload = array( + 'id' => '4x11', + ); + + $response = $bridge->submit( $payload ); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertTrue( $response['data']['success'] ); + } + + /** + * Test sync operation. + */ + public function test_sync() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-sync-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'sync', + ) + ); + + $response = $bridge->submit(); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertTrue( $response['data']['success'] ); + } + + /** + * Test error response handling. + */ + public function test_error_response_handling() { + self::$mock_response = array( + 'success' => false, + 'error' => array( + 'code' => 'INVALID_SESSIONID', + 'message' => 'Given sessionid is invalid', + ), + ); + + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-error-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'create', + ) + ); + + $response = $bridge->submit( array( 'firstname' => 'Test' ) ); + + $this->assertTrue( is_wp_error( $response ) ); + $this->assertStringContainsString( 'vtiger_', $response->get_error_code() ); + } + + /** + * Test addon ping method. + */ + public function test_addon_ping() { + $addon = Addon::addon( 'vtiger' ); + $response = $addon->ping( 'vtiger-test-backend' ); + + $this->assertTrue( $response ); + } + + /** + * Test addon get_endpoints method. + */ + public function test_addon_get_endpoints() { + Backend::temp_registration( + array( + 'name' => 'vtiger-test-backend', + 'base_url' => 'https://vtiger.example.coop', + 'credential' => 'vtiger-test-credential', + 'headers' => array(), + ) + ); + + $addon = Addon::addon( 'vtiger' ); + $endpoints = $addon->get_endpoints( 'vtiger-test-backend' ); + + $this->assertIsArray( $endpoints ); + $this->assertContains( 'Contacts', $endpoints ); + $this->assertContains( 'Leads', $endpoints ); + $this->assertContains( 'Accounts', $endpoints ); + $this->assertContains( 'Potentials', $endpoints ); + } + + /** + * Test addon get_endpoint_schema method. + */ + public function test_addon_get_endpoint_schema() { + Backend::temp_registration( + array( + 'name' => 'vtiger-test-backend', + 'base_url' => 'https://vtiger.example.coop', + 'credential' => 'vtiger-test-credential', + 'headers' => array(), + ) + ); + + $addon = Addon::addon( 'vtiger' ); + $schema = $addon->get_endpoint_schema( + 'Contacts', + 'vtiger-test-backend', + 'create' + ); + + $this->assertIsArray( $schema ); + $this->assertNotEmpty( $schema ); + + $field_names = array_column( $schema, 'name' ); + $this->assertContains( 'firstname', $field_names ); + $this->assertContains( 'lastname', $field_names ); + } + + /** + * Test bridge schema hook is applied. + */ + public function test_bridge_schema_hook() { + $schema = \FORMS_BRIDGE\Form_Bridge::schema( 'vtiger' ); + + $this->assertArrayHasKey( 'properties', $schema ); + $this->assertArrayHasKey( 'method', $schema['properties'] ); + + // Verify the method enum contains Vtiger-specific methods. + $this->assertContains( 'create', $schema['properties']['method']['enum'] ); + $this->assertContains( 'query', $schema['properties']['method']['enum'] ); + $this->assertContains( 'retrieve', $schema['properties']['method']['enum'] ); + $this->assertContains( 'update', $schema['properties']['method']['enum'] ); + $this->assertContains( 'delete', $schema['properties']['method']['enum'] ); + } + + /** + * Test MD5 access key hashing in login. + */ + public function test_access_key_md5_hashing() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-md5-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'query', + ) + ); + + $response = $bridge->submit(); + + // Check that a request was made. + $this->assertNotNull( self::$request ); + + // Verify the flow completed successfully. + $this->assertFalse( is_wp_error( $response ) ); + } + + /** + * Test invalid backend handling. + */ + public function test_invalid_backend() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-invalid-backend-bridge', + 'backend' => 'non-existent-backend', + 'endpoint' => 'Contacts', + 'method' => 'create', + ) + ); + + $response = $bridge->submit( array( 'firstname' => 'Test' ) ); + + $this->assertTrue( is_wp_error( $response ) ); + $this->assertEquals( 'invalid_backend', $response->get_error_code() ); + } + + /** + * Test invalid credential handling. + */ + public function test_invalid_credential() { + Backend::temp_registration( + array( + 'name' => 'vtiger-no-cred-backend', + 'base_url' => 'https://vtiger.example.coop', + 'credential' => 'non-existent-credential', + 'headers' => array(), + ) + ); + + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-invalid-credential-bridge', + 'backend' => 'vtiger-no-cred-backend', + 'endpoint' => 'Contacts', + 'method' => 'create', + ) + ); + + $response = $bridge->submit( array( 'firstname' => 'Test' ) ); + + $this->assertTrue( is_wp_error( $response ) ); + $this->assertEquals( 'invalid_credential', $response->get_error_code() ); + } + + /** + * Test that assigned_user_id is automatically set on create. + */ + public function test_auto_assigned_user_id() { + $bridge = new Vtiger_Form_Bridge( + array( + 'name' => 'test-auto-user-bridge', + 'backend' => 'vtiger-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'create', + ) + ); + + $payload = array( + 'firstname' => 'John', + 'lastname' => 'Doe', + ); + + $response = $bridge->submit( $payload ); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'assigned_user_id', $response['data']['result'] ); + $this->assertEquals( '19x1', $response['data']['result']['assigned_user_id'] ); + } +} From 9af0789d61544e64702a8a4e9b9f2f4e51fbbf36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 04:27:05 +0100 Subject: [PATCH 11/12] fix: wc warnings on tests --- tests/bootstrap.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 050ae5d..e331e70 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -51,6 +51,14 @@ function ( $trigger, $function_name ) { require ABSPATH . 'wp-content/mu-plugins/wpforms/wpforms.php'; require ABSPATH . 'wp-content/mu-plugins/woocommerce/woocommerce.php'; + add_filter( + 'woocommerce_load_webhooks_limit', + function () { + return -1; + }, + 90, + ); + /* Plugin tests */ require dirname( __DIR__ ) . '/forms-bridge/deps/plugin/tests/bootstrap.php'; } From f322bffa305a4ebd5aa248cf1088ff741891a52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Wed, 26 Nov 2025 04:28:40 +0100 Subject: [PATCH 12/12] feat: add vtiger to readmes --- README.md | 1 + forms-bridge/readme.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index f83c189..152ee36 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Forms Bridge has the following addons: - [Rocket.Chat](https://formsbridge.codeccoop.org/documentation/rocket-chat/) - [Slack](https://formsbridge.codeccoop.org/documentation/slack/) - [SuiteCRM](https://formsbridge.codeccoop.org/documentation/suitecrm/) +- [Vtiger](https://formsbridge.codeccoop.org/documentation/vtiger/) - [Zoho CRM](https://formsbridge.codeccoop.org/documentation/zoho-crm/) - [Zulip](https://formsbridge.codeccoop.org/documentation/zulip/) diff --git a/forms-bridge/readme.txt b/forms-bridge/readme.txt index 2125bb0..03bd41c 100644 --- a/forms-bridge/readme.txt +++ b/forms-bridge/readme.txt @@ -45,6 +45,7 @@ Forms Bridge has the following addons: * [Rocket.Chat](https://formsbridge.codeccoop.org/documentation/rocket-chat/) * [Slack](https://formsbridge.codeccoop.org/documentation/slack/) * [SuiteCRM](https://formsbridge.codeccoop.org/documentation/suitecrm/) +* [Vtiger](https://formsbridge.codeccoop.org/documentation/vtiger/) * [Zoho CRM](https://formsbridge.codeccoop.org/documentation/zoho-crm/) * [Zulip](https://formsbridge.codeccoop.org/documentation/zulip/)