From d6535364f9801ca5e47ab85b677dcc6a6548b843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 00:47:17 +0100 Subject: [PATCH 01/12] feat: suitecrm addon --- .../addons/suitecrm/class-suitecrm-addon.php | 195 ++++++++++ .../suitecrm/class-suitecrm-form-bridge.php | 344 ++++++++++++++++++ forms-bridge/addons/suitecrm/hooks.php | 159 ++++++++ .../addons/suitecrm/templates/accounts.php | 343 +++++++++++++++++ .../addons/suitecrm/templates/contacts.php | 210 +++++++++++ .../addons/suitecrm/templates/leads.php | 283 ++++++++++++++ .../suitecrm/templates/opportunities.php | 316 ++++++++++++++++ 7 files changed, 1850 insertions(+) create mode 100644 forms-bridge/addons/suitecrm/class-suitecrm-addon.php create mode 100644 forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php create mode 100644 forms-bridge/addons/suitecrm/hooks.php create mode 100644 forms-bridge/addons/suitecrm/templates/accounts.php create mode 100644 forms-bridge/addons/suitecrm/templates/contacts.php create mode 100644 forms-bridge/addons/suitecrm/templates/leads.php create mode 100644 forms-bridge/addons/suitecrm/templates/opportunities.php diff --git a/forms-bridge/addons/suitecrm/class-suitecrm-addon.php b/forms-bridge/addons/suitecrm/class-suitecrm-addon.php new file mode 100644 index 0000000..6c8f946 --- /dev/null +++ b/forms-bridge/addons/suitecrm/class-suitecrm-addon.php @@ -0,0 +1,195 @@ + '__suitecrm-' . time(), + 'method' => 'get_server_info', + 'endpoint' => '', + 'backend' => $backend, + ) + ); + + $response = $bridge->submit(); + + if ( is_wp_error( $response ) ) { + Logger::log( 'SuiteCRM backend ping error response', Logger::ERROR ); + Logger::log( $response, Logger::ERROR ); + return false; + } + + return true; + } + + /** + * Performs a GET 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 SuiteCRM_Form_Bridge( + array( + 'name' => '__suitecrm-' . time(), + 'method' => 'get_entry_list', + 'endpoint' => $endpoint, + 'backend' => $backend, + ) + ); + + return $bridge->submit( + array( + 'select_fields' => array( 'id', 'name' ), + 'max_results' => 100, + ) + ); + } + + /** + * Fetch available modules from the backend. + * + * @param Backend $backend HTTP backend object. + * + * @return array + */ + public function get_endpoints( $backend ) { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => '__suitecrm-' . time(), + 'method' => 'get_available_modules', + 'endpoint' => '', + 'backend' => $backend, + ) + ); + + $response = $bridge->submit(); + + if ( is_wp_error( $response ) ) { + return array(); + } + + if ( ! isset( $response['data']['modules'] ) ) { + return array(); + } + + return array_map( + function ( $module ) { + return $module['module_key']; + }, + $response['data']['modules'] + ); + } + + /** + * 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 ( 'set_entry' !== $method ) { + return array(); + } + + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => '__suitecrm-' . time(), + 'method' => 'get_module_fields', + 'endpoint' => $module, + 'backend' => $backend, + ) + ); + + $response = $bridge->submit(); + + if ( is_wp_error( $response ) ) { + return array(); + } + + if ( ! isset( $response['data']['module_fields'] ) ) { + return array(); + } + + $fields = array(); + foreach ( $response['data']['module_fields'] as $name => $spec ) { + $type = 'string'; + + if ( in_array( $spec['type'], array( 'int', 'integer' ), true ) ) { + $type = 'integer'; + } elseif ( in_array( $spec['type'], array( 'decimal', 'float', 'currency' ), true ) ) { + $type = 'number'; + } elseif ( 'bool' === $spec['type'] ) { + $type = 'boolean'; + } + + $schema = array( + 'type' => $type, + 'required' => ! empty( $spec['required'] ), + ); + + $fields[] = array( + 'name' => $name, + 'schema' => $schema, + ); + } + + return $fields; + } +} + +SuiteCRM_Addon::setup(); diff --git a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php new file mode 100644 index 0000000..fc3d889 --- /dev/null +++ b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php @@ -0,0 +1,344 @@ + $method, + 'input_type' => 'JSON', + 'response_type' => 'JSON', + 'rest_data' => wp_json_encode( $args ), + ); + } + + /** + * Handle REST API responses and catch errors. + * + * @param array $res Request response. + * + * @return mixed|WP_Error Request result. + */ + public static function rest_response( $res ) { + if ( is_wp_error( $res ) ) { + return $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 ) + ), + $res + ); + } + + $data = $res['data']; + + // Check for SuiteCRM error response. + if ( isset( $data['name'] ) && isset( $data['number'] ) && isset( $data['description'] ) ) { + // This is an error response. + if ( 'No Error' !== $data['name'] ) { + $error = new WP_Error( + 'suitecrm_error_' . $data['number'], + $data['description'], + $data + ); + + $error_data = array( 'response' => $res ); + if ( self::$request ) { + $error_data['request'] = self::$request; + } + + $error->add_data( $error_data ); + return $error; + } + } + + return $data; + } + + /** + * Login to SuiteCRM and get session ID. + * + * @param array $credentials Credentials array with client_id (username) and client_secret (password). + * @param Backend $backend Bridge backend object. + * + * @return string|WP_Error Session ID on success. + */ + private static function rest_login( $credentials, $backend ) { + if ( self::$session_id ) { + return self::$session_id; + } + + $username = $credentials[0] ?? ''; + $password = $credentials[1] ?? ''; + + // SuiteCRM v4_1 requires MD5 hashed password. + $password_hash = md5( $password ); + + $payload = self::rest_payload( + 'login', + array( + 'user_auth' => array( + 'user_name' => $username, + 'password' => $password_hash, + ), + 'application' => 'FormsBridge', + 'name_value_list' => array(), + ) + ); + + $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['id'] ) ) { + return new WP_Error( + 'login_failed', + __( 'SuiteCRM login failed: No session ID returned', 'forms-bridge' ), + $result + ); + } + + self::$session_id = $result['id']; + return self::$session_id; + } + + /** + * Bridge constructor with addon name provisioning. + * + * @param array $data Bridge data. + */ + public function __construct( $data ) { + parent::__construct( $data, 'suitecrm' ); + } + + /** + * Submits submission to the backend. + * + * @param array $payload Submission data. + * @param array $more_args Additional arguments (e.g., select_fields for get_entry_list). + * + * @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(); + + // Methods that don't require authentication. + $public_methods = array( 'get_server_info' ); + + if ( ! in_array( $this->method, $public_methods, true ) ) { + $session_id = self::rest_login( $login_credentials, $backend ); + + if ( is_wp_error( $session_id ) ) { + return $session_id; + } + } else { + $session_id = ''; + } + + // Build the API request based on method. + $rest_args = $this->build_rest_args( $session_id, $payload, $more_args ); + + $api_payload = self::rest_payload( $this->method, $rest_args ); + + $response = $backend->post( self::ENDPOINT, $api_payload, 'application/x-www-form-urlencoded' ); + + $result = self::rest_response( $response ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + // Normalize response. + $response['data'] = $result; + + return $response; + } + + /** + * Build REST API arguments based on the method. + * + * @param string $session_id Session ID. + * @param array $payload Form submission payload. + * @param array $more_args Additional arguments. + * + * @return array REST API arguments. + */ + private function build_rest_args( $session_id, $payload, $more_args ) { + $module = $this->endpoint; + + switch ( $this->method ) { + case 'get_server_info': + return array(); + + case 'get_available_modules': + return array( + 'session' => $session_id, + ); + + case 'get_module_fields': + return array( + 'session' => $session_id, + 'module_name' => $module, + ); + + case 'get_entry_list': + $args = array( + 'session' => $session_id, + 'module_name' => $module, + 'query' => $more_args['query'] ?? '', + 'order_by' => $more_args['order_by'] ?? '', + 'offset' => $more_args['offset'] ?? 0, + 'select_fields' => $more_args['select_fields'] ?? array(), + 'max_results' => $more_args['max_results'] ?? 20, + 'deleted' => $more_args['deleted'] ?? 0, + ); + return $args; + + case 'get_entry': + return array( + 'session' => $session_id, + 'module_name' => $module, + 'id' => $payload['id'] ?? '', + 'select_fields' => $more_args['select_fields'] ?? array(), + ); + + case 'set_entry': + // Convert payload to name_value_list format. + $name_value_list = array(); + foreach ( $payload as $name => $value ) { + $name_value_list[] = array( + 'name' => $name, + 'value' => $value, + ); + } + + return array( + 'session' => $session_id, + 'module_name' => $module, + 'name_value_list' => $name_value_list, + ); + + case 'set_relationship': + return array( + 'session' => $session_id, + 'module_name' => $module, + 'module_id' => $payload['module_id'] ?? '', + 'link_field_name' => $payload['link_field_name'] ?? '', + 'related_ids' => $payload['related_ids'] ?? array(), + 'name_value_list' => $payload['name_value_list'] ?? array(), + 'delete' => $payload['delete'] ?? 0, + ); + + case 'get_relationships': + return array( + 'session' => $session_id, + 'module_name' => $module, + 'module_id' => $payload['module_id'] ?? '', + 'link_field_name' => $payload['link_field_name'] ?? '', + 'related_module_query' => $more_args['query'] ?? '', + 'related_fields' => $more_args['select_fields'] ?? array(), + ); + + default: + // For custom methods, pass payload as-is with session. + return array_merge( + array( 'session' => $session_id ), + $payload + ); + } + } +} diff --git a/forms-bridge/addons/suitecrm/hooks.php b/forms-bridge/addons/suitecrm/hooks.php new file mode 100644 index 0000000..beca755 --- /dev/null +++ b/forms-bridge/addons/suitecrm/hooks.php @@ -0,0 +1,159 @@ + 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' => __( + 'SuiteCRM user name', + 'forms-bridge' + ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#credential', + 'name' => 'client_secret', + 'description' => __( 'User password', 'forms-bridge' ), + 'label' => __( 'Password', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#backend', + 'name' => 'base_url', + 'label' => __( 'SuiteCRM URL', 'forms-bridge' ), + 'description' => __( + 'Base URL of your SuiteCRM installation (e.g., https://crm.example.com)', + 'forms-bridge' + ), + 'type' => 'url', + 'required' => true, + ), + array( + 'ref' => '#backend', + 'name' => 'name', + 'default' => 'SuiteCRM', + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'label' => __( 'Module', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#bridge', + 'name' => 'method', + 'label' => __( 'Method', 'forms-bridge' ), + 'type' => 'text', + 'value' => 'set_entry', + 'required' => true, + ), + ), + 'bridge' => array( + 'name' => '', + 'form_id' => '', + 'backend' => '', + 'endpoint' => '', + 'method' => 'set_entry', + ), + 'backend' => array( + 'name' => 'SuiteCRM', + '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/suitecrm/templates/accounts.php b/forms-bridge/addons/suitecrm/templates/accounts.php new file mode 100644 index 0000000..ea0aa97 --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/accounts.php @@ -0,0 +1,343 @@ + __( 'Accounts', 'forms-bridge' ), + 'description' => __( + 'Account form template. The resulting bridge will convert form submissions into SuiteCRM accounts (companies/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' => 'entry_list.[].id', + 'label' => 'entry_list.[].name_value_list.user_name.value', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'account_type', + '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' => 'set_entry', + 'custom_fields' => array( + array( + 'name' => 'account_type', + 'value' => 'Prospect', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'name', + 'to' => 'name', + 'cast' => 'string', + ), + array( + 'from' => '?email', + 'to' => 'email1', + 'cast' => 'string', + ), + array( + 'from' => '?phone', + 'to' => 'phone_office', + 'cast' => 'string', + ), + array( + 'from' => '?fax', + 'to' => 'phone_fax', + 'cast' => 'string', + ), + array( + 'from' => '?website', + 'to' => 'website', + 'cast' => 'string', + ), + array( + 'from' => '?description', + 'to' => 'description', + 'cast' => 'string', + ), + array( + 'from' => '?employees', + 'to' => 'employees', + 'cast' => 'string', + ), + array( + 'from' => '?annual_revenue', + 'to' => 'annual_revenue', + 'cast' => 'string', + ), + array( + 'from' => '?address', + 'to' => 'billing_address_street', + 'cast' => 'string', + ), + array( + 'from' => '?city', + 'to' => 'billing_address_city', + 'cast' => 'string', + ), + array( + 'from' => '?state', + 'to' => 'billing_address_state', + 'cast' => 'string', + ), + array( + 'from' => '?postal_code', + 'to' => 'billing_address_postalcode', + 'cast' => 'string', + ), + array( + 'from' => '?country', + 'to' => 'billing_address_country', + 'cast' => 'string', + ), + array( + 'from' => '?account_type', + 'to' => 'account_type', + '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' => __( 'Company Name', 'forms-bridge' ), + 'name' => 'name', + '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/suitecrm/templates/contacts.php b/forms-bridge/addons/suitecrm/templates/contacts.php new file mode 100644 index 0000000..4b6cc46 --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/contacts.php @@ -0,0 +1,210 @@ + __( 'Contacts', 'forms-bridge' ), + 'description' => __( + 'Contact form template. The resulting bridge will convert form submissions into SuiteCRM 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' => 'entry_list.[].id', + 'label' => 'entry_list.[].name_value_list.user_name.value', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'lead_source', + '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' => 'set_entry', + 'custom_fields' => array( + array( + 'name' => 'lead_source', + 'value' => 'Web Site', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'first_name', + 'to' => 'first_name', + 'cast' => 'string', + ), + array( + 'from' => 'last_name', + 'to' => 'last_name', + 'cast' => 'string', + ), + array( + 'from' => 'email', + 'to' => 'email1', + 'cast' => 'string', + ), + array( + 'from' => '?phone', + 'to' => 'phone_work', + 'cast' => 'string', + ), + array( + 'from' => '?mobile', + 'to' => 'phone_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' => 'primary_address_street', + 'cast' => 'string', + ), + array( + 'from' => '?city', + 'to' => 'primary_address_city', + 'cast' => 'string', + ), + array( + 'from' => '?state', + 'to' => 'primary_address_state', + 'cast' => 'string', + ), + array( + 'from' => '?postal_code', + 'to' => 'primary_address_postalcode', + 'cast' => 'string', + ), + array( + 'from' => '?country', + 'to' => 'primary_address_country', + 'cast' => 'string', + ), + array( + 'from' => '?lead_source', + 'to' => 'lead_source', + 'cast' => 'string', + ), + array( + 'from' => '?assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + ), + ), + ), + 'form' => array( + 'fields' => array( + array( + 'label' => __( 'First Name', 'forms-bridge' ), + 'name' => 'first_name', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Last Name', 'forms-bridge' ), + 'name' => 'last_name', + '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/suitecrm/templates/leads.php b/forms-bridge/addons/suitecrm/templates/leads.php new file mode 100644 index 0000000..5d46c41 --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/leads.php @@ -0,0 +1,283 @@ + __( 'Leads', 'forms-bridge' ), + 'description' => __( + 'Lead capture form template. The resulting bridge will convert form submissions into SuiteCRM 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' => 'entry_list.[].id', + 'label' => 'entry_list.[].name_value_list.user_name.value', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'status', + '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' => 'lead_source', + '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' => 'set_entry', + 'custom_fields' => array( + array( + 'name' => 'status', + 'value' => 'New', + ), + array( + 'name' => 'lead_source', + 'value' => 'Web Site', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'first_name', + 'to' => 'first_name', + 'cast' => 'string', + ), + array( + 'from' => 'last_name', + 'to' => 'last_name', + 'cast' => 'string', + ), + array( + 'from' => 'email', + 'to' => 'email1', + 'cast' => 'string', + ), + array( + 'from' => '?phone', + 'to' => 'phone_work', + 'cast' => 'string', + ), + array( + 'from' => '?mobile', + 'to' => 'phone_mobile', + 'cast' => 'string', + ), + array( + 'from' => '?company', + 'to' => 'account_name', + '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' => '?website', + 'to' => 'website', + 'cast' => 'string', + ), + array( + 'from' => '?address', + 'to' => 'primary_address_street', + 'cast' => 'string', + ), + array( + 'from' => '?city', + 'to' => 'primary_address_city', + 'cast' => 'string', + ), + array( + 'from' => '?state', + 'to' => 'primary_address_state', + 'cast' => 'string', + ), + array( + 'from' => '?postal_code', + 'to' => 'primary_address_postalcode', + 'cast' => 'string', + ), + array( + 'from' => '?country', + 'to' => 'primary_address_country', + 'cast' => 'string', + ), + array( + 'from' => '?status', + 'to' => 'status', + 'cast' => 'string', + ), + array( + 'from' => '?lead_source', + 'to' => 'lead_source', + 'cast' => 'string', + ), + array( + 'from' => '?assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + ), + ), + ), + 'form' => array( + 'fields' => array( + array( + 'label' => __( 'First Name', 'forms-bridge' ), + 'name' => 'first_name', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Last Name', 'forms-bridge' ), + 'name' => 'last_name', + '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/suitecrm/templates/opportunities.php b/forms-bridge/addons/suitecrm/templates/opportunities.php new file mode 100644 index 0000000..3874747 --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/opportunities.php @@ -0,0 +1,316 @@ + __( 'Opportunities', 'forms-bridge' ), + 'description' => __( + 'Opportunity form template. The resulting bridge will convert form submissions into SuiteCRM opportunities (sales deals).', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Opportunities', 'forms-bridge' ), + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => 'Opportunities', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'description' => __( + 'User to assign the opportunity to', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'entry_list.[].id', + 'label' => 'entry_list.[].name_value_list.user_name.value', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'account_id', + 'label' => __( 'Account', 'forms-bridge' ), + 'description' => __( + 'Related account for this opportunity', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Accounts', + 'finger' => array( + 'value' => 'entry_list.[].id', + 'label' => 'entry_list.[].name_value_list.name.value', + ), + ), + ), + 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' => 'lead_source', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'description' => __( + 'Source of the opportunity', + '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' => 'Opportunities', + 'method' => 'set_entry', + 'custom_fields' => array( + array( + 'name' => 'sales_stage', + 'value' => 'Prospecting', + ), + array( + 'name' => 'lead_source', + 'value' => 'Web Site', + ), + array( + 'name' => 'opportunity_type', + 'value' => 'New Business', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'name', + 'to' => 'name', + 'cast' => 'string', + ), + array( + 'from' => '?amount', + 'to' => 'amount', + 'cast' => 'string', + ), + array( + 'from' => '?currency', + 'to' => 'currency_id', + 'cast' => 'string', + ), + array( + 'from' => '?date_closed', + 'to' => 'date_closed', + 'cast' => 'string', + ), + array( + 'from' => '?probability', + 'to' => 'probability', + 'cast' => 'string', + ), + array( + 'from' => '?next_step', + 'to' => 'next_step', + 'cast' => 'string', + ), + array( + 'from' => '?description', + 'to' => 'description', + 'cast' => 'string', + ), + array( + 'from' => '?sales_stage', + 'to' => 'sales_stage', + 'cast' => 'string', + ), + array( + 'from' => '?lead_source', + 'to' => 'lead_source', + 'cast' => 'string', + ), + array( + 'from' => '?opportunity_type', + 'to' => 'opportunity_type', + 'cast' => 'string', + ), + array( + 'from' => '?account_id', + 'to' => 'account_id', + 'cast' => 'string', + ), + array( + 'from' => '?assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + ), + ), + ), + 'form' => array( + 'fields' => array( + array( + 'label' => __( 'Opportunity Name', 'forms-bridge' ), + 'name' => 'name', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Amount', 'forms-bridge' ), + 'name' => 'amount', + 'type' => 'number', + 'required' => true, + ), + array( + 'label' => __( 'Expected Close Date', 'forms-bridge' ), + 'name' => 'date_closed', + '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' => 'next_step', + 'type' => 'text', + ), + array( + 'label' => __( 'Description', 'forms-bridge' ), + 'name' => 'description', + 'type' => 'textarea', + ), + ), + ), +); From 229f524bfd6932df80098f830ae7b8a3c298443e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 00:54:01 +0100 Subject: [PATCH 02/12] feat: suitecrm logo --- forms-bridge/addons/suitecrm/assets/logo.png | Bin 0 -> 7359 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 forms-bridge/addons/suitecrm/assets/logo.png diff --git a/forms-bridge/addons/suitecrm/assets/logo.png b/forms-bridge/addons/suitecrm/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..59e5446aa8481ae2c06ea31117b6f6017ffa3aa2 GIT binary patch literal 7359 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 acX15-{|B5dADsgOKw9d0Y7Hug$o~P#vBFRQ literal 0 HcmV?d00001 From 7b24f42635c93ec608863b3ec92394670dc0ae2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 02:33:39 +0100 Subject: [PATCH 03/12] fix: suitecrm addon ping and backend encoding schema --- forms-bridge/addons/suitecrm/class-suitecrm-addon.php | 2 +- forms-bridge/addons/suitecrm/hooks.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/forms-bridge/addons/suitecrm/class-suitecrm-addon.php b/forms-bridge/addons/suitecrm/class-suitecrm-addon.php index 6c8f946..ea5d0c5 100644 --- a/forms-bridge/addons/suitecrm/class-suitecrm-addon.php +++ b/forms-bridge/addons/suitecrm/class-suitecrm-addon.php @@ -53,7 +53,7 @@ public function ping( $backend ) { $bridge = new SuiteCRM_Form_Bridge( array( 'name' => '__suitecrm-' . time(), - 'method' => 'get_server_info', + 'method' => 'get_user_id', 'endpoint' => '', 'backend' => $backend, ) diff --git a/forms-bridge/addons/suitecrm/hooks.php b/forms-bridge/addons/suitecrm/hooks.php index beca755..b53729c 100644 --- a/forms-bridge/addons/suitecrm/hooks.php +++ b/forms-bridge/addons/suitecrm/hooks.php @@ -41,6 +41,7 @@ function ( $schema, $addon ) { 'get_relationships', 'get_module_fields', 'get_available_modules', + 'get_user_id', ); $schema['properties']['method']['default'] = 'set_entry'; @@ -111,6 +112,11 @@ function ( $defaults, $addon, $schema ) { 'name' => 'name', 'default' => 'SuiteCRM', ), + array( + 'ref' => '#backend/headers[]', + 'name' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), array( 'ref' => '#bridge', 'name' => 'endpoint', @@ -137,6 +143,10 @@ function ( $defaults, $addon, $schema ) { 'backend' => array( 'name' => 'SuiteCRM', 'headers' => array( + array( + 'name' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), array( 'name' => 'Accept', 'value' => 'application/json', From 92ce518468b7234f1942ac9fc865e07a1a4373b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 02:34:12 +0100 Subject: [PATCH 04/12] fix: suitecrm form bridge login request --- .../suitecrm/class-suitecrm-form-bridge.php | 70 ++++++++----------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php index fc3d889..7dac904 100644 --- a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php +++ b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php @@ -70,8 +70,7 @@ public static function rest_response( $res ) { } if ( empty( $res['data'] ) ) { - $content_type = - Http_Client::get_content_type( $res['headers'] ) ?? 'undefined'; + $content_type = Http_Client::get_content_type( $res['headers'] ) ?? 'undefined'; return new WP_Error( 'unknown_content_type', @@ -112,18 +111,18 @@ public static function rest_response( $res ) { /** * Login to SuiteCRM and get session ID. * - * @param array $credentials Credentials array with client_id (username) and client_secret (password). - * @param Backend $backend Bridge backend object. + * @param Credential $credential Bridge credential object. + * @param Backend $backend Bridge backend object. * * @return string|WP_Error Session ID on success. */ - private static function rest_login( $credentials, $backend ) { + private static function rest_login( $credential, $backend ) { if ( self::$session_id ) { return self::$session_id; } - $username = $credentials[0] ?? ''; - $password = $credentials[1] ?? ''; + $username = $credential->client_id; + $password = $credential->client_secret; // SuiteCRM v4_1 requires MD5 hashed password. $password_hash = md5( $password ); @@ -131,16 +130,16 @@ private static function rest_login( $credentials, $backend ) { $payload = self::rest_payload( 'login', array( - 'user_auth' => array( + 'user_auth' => array( 'user_name' => $username, 'password' => $password_hash, ), - 'application' => 'FormsBridge', + 'application' => 'FormsBridge', 'name_value_list' => array(), ) ); - $response = $backend->post( self::ENDPOINT, $payload, 'application/x-www-form-urlencoded' ); + $response = $backend->post( self::ENDPOINT, $payload ); $result = self::rest_response( $response ); @@ -213,20 +212,10 @@ static function ( $request ) { 1 ); - // Get credentials for login. - $login_credentials = $credential->authorization(); + $session_id = self::rest_login( $credential, $backend ); - // Methods that don't require authentication. - $public_methods = array( 'get_server_info' ); - - if ( ! in_array( $this->method, $public_methods, true ) ) { - $session_id = self::rest_login( $login_credentials, $backend ); - - if ( is_wp_error( $session_id ) ) { - return $session_id; - } - } else { - $session_id = ''; + if ( is_wp_error( $session_id ) ) { + return $session_id; } // Build the API request based on method. @@ -234,7 +223,7 @@ static function ( $request ) { $api_payload = self::rest_payload( $this->method, $rest_args ); - $response = $backend->post( self::ENDPOINT, $api_payload, 'application/x-www-form-urlencoded' ); + $response = $backend->post( self::ENDPOINT, $api_payload ); $result = self::rest_response( $response ); @@ -261,9 +250,6 @@ private function build_rest_args( $session_id, $payload, $more_args ) { $module = $this->endpoint; switch ( $this->method ) { - case 'get_server_info': - return array(); - case 'get_available_modules': return array( 'session' => $session_id, @@ -271,8 +257,8 @@ private function build_rest_args( $session_id, $payload, $more_args ) { case 'get_module_fields': return array( - 'session' => $session_id, - 'module_name' => $module, + 'session' => $session_id, + 'module_name' => $module, ); case 'get_entry_list': @@ -314,23 +300,23 @@ private function build_rest_args( $session_id, $payload, $more_args ) { case 'set_relationship': return array( - 'session' => $session_id, - 'module_name' => $module, - 'module_id' => $payload['module_id'] ?? '', - 'link_field_name' => $payload['link_field_name'] ?? '', - 'related_ids' => $payload['related_ids'] ?? array(), - 'name_value_list' => $payload['name_value_list'] ?? array(), - 'delete' => $payload['delete'] ?? 0, + 'session' => $session_id, + 'module_name' => $module, + 'module_id' => $payload['module_id'] ?? '', + 'link_field_name' => $payload['link_field_name'] ?? '', + 'related_ids' => $payload['related_ids'] ?? array(), + 'name_value_list' => $payload['name_value_list'] ?? array(), + 'delete' => $payload['delete'] ?? 0, ); case 'get_relationships': return array( - 'session' => $session_id, - 'module_name' => $module, - 'module_id' => $payload['module_id'] ?? '', - 'link_field_name' => $payload['link_field_name'] ?? '', - 'related_module_query' => $more_args['query'] ?? '', - 'related_fields' => $more_args['select_fields'] ?? array(), + 'session' => $session_id, + 'module_name' => $module, + 'module_id' => $payload['module_id'] ?? '', + 'link_field_name' => $payload['link_field_name'] ?? '', + 'related_module_query' => $more_args['query'] ?? '', + 'related_fields' => $more_args['select_fields'] ?? array(), ); default: From 597e89bd84553ba2cc79bc6c93a8364ebdc988de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 02:34:37 +0100 Subject: [PATCH 05/12] feat: refactor suitecrm account bridge template --- forms-bridge/addons/suitecrm/jobs/account.php | 227 +++++++++++++ .../addons/suitecrm/templates/accounts.php | 170 ++++------ .../addons/suitecrm/templates/contacts.php | 156 +++------ .../addons/suitecrm/templates/leads.php | 199 ++++------- .../suitecrm/templates/opportunities.php | 316 ------------------ 5 files changed, 394 insertions(+), 674 deletions(-) create mode 100644 forms-bridge/addons/suitecrm/jobs/account.php delete mode 100644 forms-bridge/addons/suitecrm/templates/opportunities.php diff --git a/forms-bridge/addons/suitecrm/jobs/account.php b/forms-bridge/addons/suitecrm/jobs/account.php new file mode 100644 index 0000000..1e23e15 --- /dev/null +++ b/forms-bridge/addons/suitecrm/jobs/account.php @@ -0,0 +1,227 @@ + __( 'Account', 'forms-bridge' ), + 'description' => __( 'Creates an account on SuiteCRM', 'forms-bridge' ), + 'method' => 'forms_bridge_suitecrm_create_account', + 'input' => array( + array( + 'name' => 'name', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'assigned_user_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'assigned_user_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'account_type', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'industry', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'annual_revenue', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email1', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email_address_non_primary', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'phone_fax', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'phone_office', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'phone_alternate', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'website', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'ownership', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'employees', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'billing_address_street', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'billing_address_postalcode', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'billing_address_city', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'billing_address_state', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'billing_address_country', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'shipping_address_street', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'shipping_address_street_2', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'shipping_address_street_3', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'shipping_address_street_4', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'shipping_address_postalcode', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'shipping__address_city', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'shipping_address_state', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'shipping_address_country', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'sic_code', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'parent_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'campaing_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'campaign_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'description', + '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_suitecrm_create_account( $payload, $bridge ) { + $account = array( + 'name' => $payload['name'], + ); + + $account_fields = array( + 'description', + 'assigned_user_id', + 'assigned_user_name', + 'account_type', + 'industry', + 'annual_revenue', + 'email', + 'email1', + 'email_address_non_primary', + 'phone_fax', + 'phone_office', + 'phone_alternate', + 'website', + 'ownership', + 'employees', + 'billing_address_street', + 'billing_address_postalcode', + 'billing_address_city', + 'billing_address_state', + 'billing_address_country', + 'shipping_address_street', + 'shipping_address_street_2', + 'shipping_address_street_3', + 'shipping_address_street_4', + 'shipping_address_postalcode', + 'shipping__address_city', + 'shipping_address_state', + 'shipping_address_country', + 'sic_code', + 'parent_name', + 'campaign_id', + 'campaign_name', + 'description', + ); + + foreach ( $account_fields as $field ) { + if ( isset( $payload[ $field ] ) ) { + $account[ $field ] = $payload[ $field ]; + } + } + + $response = $bridge->patch( + array( + 'method' => 'set_entry', + 'endpoint' => 'Accounts', + ) + )->submit( $account ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $payload['account_id'] = $response['data']['id']; + return $payload; +} diff --git a/forms-bridge/addons/suitecrm/templates/accounts.php b/forms-bridge/addons/suitecrm/templates/accounts.php index ea0aa97..8537581 100644 --- a/forms-bridge/addons/suitecrm/templates/accounts.php +++ b/forms-bridge/addons/suitecrm/templates/accounts.php @@ -38,21 +38,17 @@ 'options' => array( 'endpoint' => 'Users', 'finger' => array( - 'value' => 'entry_list.[].id', - 'label' => 'entry_list.[].name_value_list.user_name.value', + 'value' => 'entry_list[].id', + 'label' => 'entry_list[].name_value_list.user_name.value', ), ), ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'account_type', - 'label' => __( 'Account Type', 'forms-bridge' ), - 'description' => __( - 'Type of account', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'account_type', + 'label' => __( 'Account Type', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( 'value' => 'Analyst', 'label' => __( 'Analyst', 'forms-bridge' ), @@ -94,18 +90,14 @@ 'label' => __( 'Other', 'forms-bridge' ), ), ), - 'default' => 'Prospect', + 'default' => 'Prospect', ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'industry', - 'label' => __( 'Industry', 'forms-bridge' ), - 'description' => __( - 'Industry sector', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'industry', + 'label' => __( 'Industry', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( 'value' => 'Apparel', 'label' => __( 'Apparel', 'forms-bridge' ), @@ -200,103 +192,59 @@ ), ), ), - ), - 'bridge' => array( - 'endpoint' => 'Accounts', - 'method' => 'set_entry', - 'custom_fields' => array( - array( - 'name' => 'account_type', - 'value' => 'Prospect', - ), - ), - 'mutations' => array( - array( - array( - 'from' => 'name', - 'to' => 'name', - 'cast' => 'string', - ), - array( - 'from' => '?email', - 'to' => 'email1', - 'cast' => 'string', - ), - array( - 'from' => '?phone', - 'to' => 'phone_office', - 'cast' => 'string', - ), - array( - 'from' => '?fax', - 'to' => 'phone_fax', - 'cast' => 'string', - ), - array( - 'from' => '?website', - 'to' => 'website', - 'cast' => 'string', - ), - array( - 'from' => '?description', - 'to' => 'description', - 'cast' => 'string', - ), - array( - 'from' => '?employees', - 'to' => 'employees', - 'cast' => 'string', - ), - array( - 'from' => '?annual_revenue', - 'to' => 'annual_revenue', - 'cast' => 'string', - ), - array( - 'from' => '?address', - 'to' => 'billing_address_street', - 'cast' => 'string', - ), - array( - 'from' => '?city', - 'to' => 'billing_address_city', - 'cast' => 'string', - ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'lead_source', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( - 'from' => '?state', - 'to' => 'billing_address_state', - 'cast' => 'string', + 'value' => 'Web Site', + 'label' => __( 'Web Site', 'forms-bridge' ), ), array( - 'from' => '?postal_code', - 'to' => 'billing_address_postalcode', - 'cast' => 'string', + 'value' => 'Cold Call', + 'label' => __( 'Cold Call', 'forms-bridge' ), ), array( - 'from' => '?country', - 'to' => 'billing_address_country', - 'cast' => 'string', + 'value' => 'Email', + 'label' => __( 'Email', 'forms-bridge' ), ), array( - 'from' => '?account_type', - 'to' => 'account_type', - 'cast' => 'string', + 'value' => 'Word of mouth', + 'label' => __( 'Word of Mouth', 'forms-bridge' ), ), array( - 'from' => '?industry', - 'to' => 'industry', - 'cast' => 'string', + 'value' => 'Campaign', + 'label' => __( 'Campaign', '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' => 'set_entry', + 'workflow' => array( 'account' ), + ), 'form' => array( 'fields' => array( + array( + 'label' => __( 'First Name', 'forms-bridge' ), + 'name' => 'first_name', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Last Name', 'forms-bridge' ), + 'name' => 'last_name', + 'type' => 'text', + 'required' => true, + ), array( 'label' => __( 'Company Name', 'forms-bridge' ), 'name' => 'name', @@ -305,12 +253,12 @@ ), array( 'label' => __( 'Email', 'forms-bridge' ), - 'name' => 'email', + 'name' => 'email1', 'type' => 'email', ), array( 'label' => __( 'Phone', 'forms-bridge' ), - 'name' => 'phone', + 'name' => 'phone_office', 'type' => 'tel', ), array( @@ -320,17 +268,27 @@ ), array( 'label' => __( 'Address', 'forms-bridge' ), - 'name' => 'address', + 'name' => 'billing_address_street', 'type' => 'text', ), array( 'label' => __( 'City', 'forms-bridge' ), - 'name' => 'city', + 'name' => 'billing_address_city', + 'type' => 'text', + ), + array( + 'label' => __( 'Postal Code', 'forms-bridge' ), + 'name' => 'billing_address_postalcode', + 'type' => 'text', + ), + array( + 'label' => __( 'State', 'forms-bridge' ), + 'name' => 'billing_address_state', 'type' => 'text', ), array( 'label' => __( 'Country', 'forms-bridge' ), - 'name' => 'country', + 'name' => 'billing_address_country', 'type' => 'text', ), array( diff --git a/forms-bridge/addons/suitecrm/templates/contacts.php b/forms-bridge/addons/suitecrm/templates/contacts.php index 4b6cc46..df7ad19 100644 --- a/forms-bridge/addons/suitecrm/templates/contacts.php +++ b/forms-bridge/addons/suitecrm/templates/contacts.php @@ -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' => 'entry_list.[].id', - 'label' => 'entry_list.[].name_value_list.user_name.value', + 'value' => 'entry_list[].id', + 'label' => 'entry_list[].name_value_list.user_name.value', ), ), ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'lead_source', - 'label' => __( 'Lead Source', 'forms-bridge' ), - 'description' => __( - 'Source of the contact', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'lead_source', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( 'value' => 'Web Site', 'label' => __( 'Web Site', 'forms-bridge' ), @@ -78,97 +70,12 @@ 'label' => __( 'Other', 'forms-bridge' ), ), ), - 'default' => 'Web Site', + 'default' => 'Web Site', ), ), 'bridge' => array( - 'endpoint' => 'Contacts', - 'method' => 'set_entry', - 'custom_fields' => array( - array( - 'name' => 'lead_source', - 'value' => 'Web Site', - ), - ), - 'mutations' => array( - array( - array( - 'from' => 'first_name', - 'to' => 'first_name', - 'cast' => 'string', - ), - array( - 'from' => 'last_name', - 'to' => 'last_name', - 'cast' => 'string', - ), - array( - 'from' => 'email', - 'to' => 'email1', - 'cast' => 'string', - ), - array( - 'from' => '?phone', - 'to' => 'phone_work', - 'cast' => 'string', - ), - array( - 'from' => '?mobile', - 'to' => 'phone_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' => 'primary_address_street', - 'cast' => 'string', - ), - array( - 'from' => '?city', - 'to' => 'primary_address_city', - 'cast' => 'string', - ), - array( - 'from' => '?state', - 'to' => 'primary_address_state', - 'cast' => 'string', - ), - array( - 'from' => '?postal_code', - 'to' => 'primary_address_postalcode', - 'cast' => 'string', - ), - array( - 'from' => '?country', - 'to' => 'primary_address_country', - 'cast' => 'string', - ), - array( - 'from' => '?lead_source', - 'to' => 'lead_source', - 'cast' => 'string', - ), - array( - 'from' => '?assigned_user_id', - 'to' => 'assigned_user_id', - 'cast' => 'string', - ), - ), - ), + 'endpoint' => 'Contacts', + 'method' => 'set_entry', ), 'form' => array( 'fields' => array( @@ -186,18 +93,43 @@ ), array( 'label' => __( 'Email', 'forms-bridge' ), - 'name' => 'email', + 'name' => 'email1', 'type' => 'email', 'required' => true, ), array( 'label' => __( 'Phone', 'forms-bridge' ), - 'name' => 'phone', + 'name' => 'phone_work', + 'type' => 'tel', + ), + array( + 'label' => __( 'Mobile', 'forms-bridge' ), + 'name' => 'phone_mobile', 'type' => 'tel', ), array( - 'label' => __( 'Title', 'forms-bridge' ), - 'name' => 'title', + '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( diff --git a/forms-bridge/addons/suitecrm/templates/leads.php b/forms-bridge/addons/suitecrm/templates/leads.php index 5d46c41..6fc6de1 100644 --- a/forms-bridge/addons/suitecrm/templates/leads.php +++ b/forms-bridge/addons/suitecrm/templates/leads.php @@ -27,32 +27,24 @@ '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' => 'entry_list.[].id', - 'label' => 'entry_list.[].name_value_list.user_name.value', + 'value' => 'entry_list[].id', + 'label' => 'entry_list[].name_value_list.user_name.value', ), ), ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'status', - 'label' => __( 'Lead Status', 'forms-bridge' ), - 'description' => __( - 'Initial status of the lead', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'status', + 'label' => __( 'Lead Status', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( 'value' => 'New', 'label' => __( 'New', 'forms-bridge' ), @@ -78,18 +70,14 @@ 'label' => __( 'Dead', 'forms-bridge' ), ), ), - 'default' => 'New', + 'default' => 'New', ), array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'lead_source', - 'label' => __( 'Lead Source', 'forms-bridge' ), - 'description' => __( - 'Source of the lead', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'lead_source', + 'label' => __( 'Lead Source', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( array( 'value' => 'Web Site', 'label' => __( 'Web Site', 'forms-bridge' ), @@ -127,116 +115,12 @@ 'label' => __( 'Other', 'forms-bridge' ), ), ), - 'default' => 'Web Site', + 'default' => 'Web Site', ), ), 'bridge' => array( - 'endpoint' => 'Leads', - 'method' => 'set_entry', - 'custom_fields' => array( - array( - 'name' => 'status', - 'value' => 'New', - ), - array( - 'name' => 'lead_source', - 'value' => 'Web Site', - ), - ), - 'mutations' => array( - array( - array( - 'from' => 'first_name', - 'to' => 'first_name', - 'cast' => 'string', - ), - array( - 'from' => 'last_name', - 'to' => 'last_name', - 'cast' => 'string', - ), - array( - 'from' => 'email', - 'to' => 'email1', - 'cast' => 'string', - ), - array( - 'from' => '?phone', - 'to' => 'phone_work', - 'cast' => 'string', - ), - array( - 'from' => '?mobile', - 'to' => 'phone_mobile', - 'cast' => 'string', - ), - array( - 'from' => '?company', - 'to' => 'account_name', - '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' => '?website', - 'to' => 'website', - 'cast' => 'string', - ), - array( - 'from' => '?address', - 'to' => 'primary_address_street', - 'cast' => 'string', - ), - array( - 'from' => '?city', - 'to' => 'primary_address_city', - 'cast' => 'string', - ), - array( - 'from' => '?state', - 'to' => 'primary_address_state', - 'cast' => 'string', - ), - array( - 'from' => '?postal_code', - 'to' => 'primary_address_postalcode', - 'cast' => 'string', - ), - array( - 'from' => '?country', - 'to' => 'primary_address_country', - 'cast' => 'string', - ), - array( - 'from' => '?status', - 'to' => 'status', - 'cast' => 'string', - ), - array( - 'from' => '?lead_source', - 'to' => 'lead_source', - 'cast' => 'string', - ), - array( - 'from' => '?assigned_user_id', - 'to' => 'assigned_user_id', - 'cast' => 'string', - ), - ), - ), + 'endpoint' => 'Leads', + 'method' => 'set_entry', ), 'form' => array( 'fields' => array( @@ -254,18 +138,28 @@ ), array( 'label' => __( 'Email', 'forms-bridge' ), - 'name' => 'email', + 'name' => 'email1', 'type' => 'email', 'required' => true, ), array( 'label' => __( 'Phone', 'forms-bridge' ), - 'name' => 'phone', + 'name' => 'phone_work', + 'type' => 'tel', + ), + array( + 'label' => __( 'Mobile', 'forms-bridge' ), + 'name' => 'phone_mobile', 'type' => 'tel', ), array( 'label' => __( 'Company', 'forms-bridge' ), - 'name' => 'company', + 'name' => 'account_name', + 'type' => 'text', + ), + array( + 'label' => __( 'Title', 'forms-bridge' ), + 'name' => 'title', 'type' => 'text', ), array( @@ -273,6 +167,31 @@ '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/opportunities.php b/forms-bridge/addons/suitecrm/templates/opportunities.php deleted file mode 100644 index 3874747..0000000 --- a/forms-bridge/addons/suitecrm/templates/opportunities.php +++ /dev/null @@ -1,316 +0,0 @@ - __( 'Opportunities', 'forms-bridge' ), - 'description' => __( - 'Opportunity form template. The resulting bridge will convert form submissions into SuiteCRM opportunities (sales deals).', - 'forms-bridge' - ), - 'fields' => array( - array( - 'ref' => '#form', - 'name' => 'title', - 'default' => __( 'Opportunities', 'forms-bridge' ), - ), - array( - 'ref' => '#bridge', - 'name' => 'endpoint', - 'value' => 'Opportunities', - ), - array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'assigned_user_id', - 'label' => __( 'Assigned User', 'forms-bridge' ), - 'description' => __( - 'User to assign the opportunity to', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - 'endpoint' => 'Users', - 'finger' => array( - 'value' => 'entry_list.[].id', - 'label' => 'entry_list.[].name_value_list.user_name.value', - ), - ), - ), - array( - 'ref' => '#bridge/custom_fields[]', - 'name' => 'account_id', - 'label' => __( 'Account', 'forms-bridge' ), - 'description' => __( - 'Related account for this opportunity', - 'forms-bridge' - ), - 'type' => 'select', - 'options' => array( - 'endpoint' => 'Accounts', - 'finger' => array( - 'value' => 'entry_list.[].id', - 'label' => 'entry_list.[].name_value_list.name.value', - ), - ), - ), - 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' => 'lead_source', - 'label' => __( 'Lead Source', 'forms-bridge' ), - 'description' => __( - 'Source of the opportunity', - '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' => 'Opportunities', - 'method' => 'set_entry', - 'custom_fields' => array( - array( - 'name' => 'sales_stage', - 'value' => 'Prospecting', - ), - array( - 'name' => 'lead_source', - 'value' => 'Web Site', - ), - array( - 'name' => 'opportunity_type', - 'value' => 'New Business', - ), - ), - 'mutations' => array( - array( - array( - 'from' => 'name', - 'to' => 'name', - 'cast' => 'string', - ), - array( - 'from' => '?amount', - 'to' => 'amount', - 'cast' => 'string', - ), - array( - 'from' => '?currency', - 'to' => 'currency_id', - 'cast' => 'string', - ), - array( - 'from' => '?date_closed', - 'to' => 'date_closed', - 'cast' => 'string', - ), - array( - 'from' => '?probability', - 'to' => 'probability', - 'cast' => 'string', - ), - array( - 'from' => '?next_step', - 'to' => 'next_step', - 'cast' => 'string', - ), - array( - 'from' => '?description', - 'to' => 'description', - 'cast' => 'string', - ), - array( - 'from' => '?sales_stage', - 'to' => 'sales_stage', - 'cast' => 'string', - ), - array( - 'from' => '?lead_source', - 'to' => 'lead_source', - 'cast' => 'string', - ), - array( - 'from' => '?opportunity_type', - 'to' => 'opportunity_type', - 'cast' => 'string', - ), - array( - 'from' => '?account_id', - 'to' => 'account_id', - 'cast' => 'string', - ), - array( - 'from' => '?assigned_user_id', - 'to' => 'assigned_user_id', - 'cast' => 'string', - ), - ), - ), - ), - 'form' => array( - 'fields' => array( - array( - 'label' => __( 'Opportunity Name', 'forms-bridge' ), - 'name' => 'name', - 'type' => 'text', - 'required' => true, - ), - array( - 'label' => __( 'Amount', 'forms-bridge' ), - 'name' => 'amount', - 'type' => 'number', - 'required' => true, - ), - array( - 'label' => __( 'Expected Close Date', 'forms-bridge' ), - 'name' => 'date_closed', - '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' => 'next_step', - 'type' => 'text', - ), - array( - 'label' => __( 'Description', 'forms-bridge' ), - 'name' => 'description', - 'type' => 'textarea', - ), - ), - ), -); From 5f22c97923703ba6d96670e7380295380e38dc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 03:34:07 +0100 Subject: [PATCH 06/12] feat: suitecrm addon tests --- forms-bridge/deps/http | 2 +- forms-bridge/deps/plugin | 2 +- tests/addons/test-suitecrm.php | 679 +++++++++++++++++++++++++++++++++ tests/test-custom-hooks.php | 2 + 4 files changed, 683 insertions(+), 2 deletions(-) create mode 100644 tests/addons/test-suitecrm.php diff --git a/forms-bridge/deps/http b/forms-bridge/deps/http index 7218bf4..a7e220b 160000 --- a/forms-bridge/deps/http +++ b/forms-bridge/deps/http @@ -1 +1 @@ -Subproject commit 7218bf42d6b821abb7ea04dccdef686e7d6ee98d +Subproject commit a7e220bffbd25587bd68ad70d589a172f8e799bb diff --git a/forms-bridge/deps/plugin b/forms-bridge/deps/plugin index 88c43d2..e398460 160000 --- a/forms-bridge/deps/plugin +++ b/forms-bridge/deps/plugin @@ -1 +1 @@ -Subproject commit 88c43d2f09f0cdf6d14eac99f14abf80832c20d1 +Subproject commit e3984600e84eb926d14b4398b22da2e273574cf7 diff --git a/tests/addons/test-suitecrm.php b/tests/addons/test-suitecrm.php new file mode 100644 index 0000000..c05047d --- /dev/null +++ b/tests/addons/test-suitecrm.php @@ -0,0 +1,679 @@ + 'suitecrm-test-credential', + 'schema' => 'Basic', + 'client_id' => 'admin', + 'client_secret' => 'password123', + ) + ), + ); + } + + /** + * Test backend provider. + * + * @return Backend[] + */ + public static function backends_provider() { + return array( + new Backend( + array( + 'name' => 'suitecrm-test-backend', + 'base_url' => 'https://crm.example.coop', + 'credential' => 'suitecrm-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 the body to determine the method being called. + $body = array(); + if ( ! empty( $args['body'] ) ) { + if ( is_string( $args['body'] ) ) { + parse_str( $args['body'], $body ); + } else { + $body = $args['body']; + } + } + + $method = $body['method'] ?? ''; + + // Return appropriate mock response based on method. + if ( self::$mock_response ) { + $response_body = self::$mock_response; + } else { + $response_body = self::get_mock_response( $method, $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 method. + * + * @param string $method API method name. + * @param array $body Request body. + * + * @return array Mock response. + */ + private static function get_mock_response( $method, $body ) { + switch ( $method ) { + case 'login': + return array( + 'id' => 'test-session-id-12345', + 'module_name' => 'Users', + 'name_value_list' => array( + 'user_id' => array( + 'name' => 'user_id', + 'value' => '1', + ), + 'user_name' => array( + 'name' => 'user_name', + 'value' => 'admin', + ), + ), + ); + + case 'get_user_id': + return '1'; + + case 'get_available_modules': + return array( + 'modules' => array( + array( + 'module_key' => 'Contacts', + 'module_label' => 'Contacts', + ), + array( + 'module_key' => 'Leads', + 'module_label' => 'Leads', + ), + array( + 'module_key' => 'Accounts', + 'module_label' => 'Accounts', + ), + array( + 'module_key' => 'Opportunities', + 'module_label' => 'Opportunities', + ), + ), + ); + + case 'get_module_fields': + return array( + 'module_name' => 'Contacts', + 'module_fields' => array( + 'first_name' => array( + 'name' => 'first_name', + 'type' => 'varchar', + 'label' => 'First Name', + 'required' => 1, + ), + 'last_name' => array( + 'name' => 'last_name', + 'type' => 'varchar', + 'label' => 'Last Name', + 'required' => 1, + ), + 'email1' => array( + 'name' => 'email1', + 'type' => 'varchar', + 'label' => 'Email', + 'required' => 0, + ), + 'phone_work' => array( + 'name' => 'phone_work', + 'type' => 'phone', + 'label' => 'Office Phone', + 'required' => 0, + ), + ), + ); + + case 'get_entry_list': + return array( + 'result_count' => 2, + 'total_count' => 2, + 'next_offset' => 2, + 'entry_list' => array( + array( + 'id' => 'contact-id-1', + 'module_name' => 'Contacts', + 'name_value_list' => array( + 'id' => array( + 'name' => 'id', + 'value' => 'contact-id-1', + ), + 'first_name' => array( + 'name' => 'first_name', + 'value' => 'John', + ), + 'last_name' => array( + 'name' => 'last_name', + 'value' => 'Doe', + ), + ), + ), + array( + 'id' => 'contact-id-2', + 'module_name' => 'Contacts', + 'name_value_list' => array( + 'id' => array( + 'name' => 'id', + 'value' => 'contact-id-2', + ), + 'first_name' => array( + 'name' => 'first_name', + 'value' => 'Jane', + ), + 'last_name' => array( + 'name' => 'last_name', + 'value' => 'Smith', + ), + ), + ), + ), + 'relationship_list' => array(), + ); + + case 'set_entry': + return array( + 'id' => 'new-contact-id-123', + 'entry_list' => array( + array( + 'name' => 'id', + 'value' => 'new-contact-id-123', + ), + ), + ); + + case 'get_entry': + return array( + 'entry_list' => array( + array( + 'id' => 'contact-id-1', + 'module_name' => 'Contacts', + 'name_value_list' => array( + 'id' => array( + 'name' => 'id', + 'value' => 'contact-id-1', + ), + 'first_name' => array( + 'name' => 'first_name', + 'value' => 'John', + ), + 'last_name' => array( + 'name' => 'last_name', + 'value' => 'Doe', + ), + ), + ), + ), + ); + + default: + return array( 'success' => true ); + } + } + + /** + * 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\SuiteCRM_Addon' ) ); + $this->assertEquals( 'SuiteCRM', SuiteCRM_Addon::TITLE ); + $this->assertEquals( 'suitecrm', SuiteCRM_Addon::NAME ); + $this->assertEquals( '\FORMS_BRIDGE\SuiteCRM_Form_Bridge', SuiteCRM_Addon::BRIDGE ); + } + + /** + * Test that the form bridge class exists. + */ + public function test_form_bridge_class_exists() { + $this->assertTrue( class_exists( 'FORMS_BRIDGE\SuiteCRM_Form_Bridge' ) ); + } + + /** + * Test bridge validation with valid data. + */ + public function test_bridge_validation() { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'test-suitecrm-bridge', + 'backend' => 'suitecrm-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'set_entry', + ) + ); + + $this->assertTrue( $bridge->is_valid ); + } + + /** + * Test bridge validation with invalid data. + */ + public function test_bridge_validation_invalid() { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'invalid-bridge', + // Missing required fields. + ) + ); + + $this->assertFalse( $bridge->is_valid ); + } + + /** + * Test REST payload building. + */ + public function test_rest_payload() { + $payload = SuiteCRM_Form_Bridge::rest_payload( + 'login', + array( + 'user_auth' => array( + 'user_name' => 'admin', + 'password' => md5( 'password' ), + ), + ) + ); + + $this->assertArrayHasKey( 'method', $payload ); + $this->assertArrayHasKey( 'input_type', $payload ); + $this->assertArrayHasKey( 'response_type', $payload ); + $this->assertArrayHasKey( 'rest_data', $payload ); + + $this->assertEquals( 'login', $payload['method'] ); + $this->assertEquals( 'JSON', $payload['input_type'] ); + $this->assertEquals( 'JSON', $payload['response_type'] ); + } + + /** + * Test successful login flow. + */ + public function test_login_flow() { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'test-login-bridge', + 'backend' => 'suitecrm-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'get_entry_list', + ) + ); + + $response = $bridge->submit( + array(), + array( 'max_results' => 10 ) + ); + + $this->assertFalse( is_wp_error( $response ) ); + + // Verify the request URL contains the SuiteCRM endpoint. + $this->assertStringContainsString( '/service/v4_1/rest.php', self::$request['url'] ); + } + + /** + * Test set_entry operation. + */ + public function test_set_entry() { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'test-set-entry-bridge', + 'backend' => 'suitecrm-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'set_entry', + ) + ); + + $payload = array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email1' => 'john.doe@example.com', + ); + + $response = $bridge->submit( $payload ); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertArrayHasKey( 'id', $response['data'] ); + $this->assertEquals( 'new-contact-id-123', $response['data']['id'] ); + } + + /** + * Test get_entry_list operation. + */ + public function test_get_entry_list() { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'test-get-entry-list-bridge', + 'backend' => 'suitecrm-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'get_entry_list', + ) + ); + + $response = $bridge->submit( + array(), + array( + 'select_fields' => array( 'id', 'first_name', 'last_name' ), + 'max_results' => 20, + ) + ); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertArrayHasKey( 'entry_list', $response['data'] ); + $this->assertCount( 2, $response['data']['entry_list'] ); + } + + /** + * Test get_server_info operation (no auth required). + */ + public function test_get_user_id() { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'test-server-info-bridge', + 'backend' => 'suitecrm-test-backend', + 'endpoint' => '', + 'method' => 'get_user_id', + ) + ); + + $response = $bridge->submit(); + + $this->assertFalse( is_wp_error( $response ) ); + $this->assertArrayHasKey( 'data', $response ); + $this->assertEquals( '1', $response['data'] ); + } + + /** + * Test error response handling. + */ + public function test_error_response_handling() { + self::$mock_response = array( + 'name' => 'Invalid Login', + 'number' => '10', + 'description' => 'Login attempt failed please check the username and password', + ); + + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'test-error-bridge', + 'backend' => 'suitecrm-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'set_entry', + ) + ); + + $response = $bridge->submit( array( 'first_name' => 'Test' ) ); + + $this->assertTrue( is_wp_error( $response ) ); + $this->assertStringContainsString( 'suitecrm_error', $response->get_error_code() ); + } + + /** + * Test addon ping method. + */ + public function test_addon_ping() { + $addon = Addon::addon( 'suitecrm' ); + $response = $addon->ping( 'suitecrm-test-backend' ); + + $this->assertTrue( $response ); + } + + /** + * Test addon get_endpoints method. + */ + public function test_addon_get_endpoints() { + Backend::temp_registration( + array( + 'name' => 'suitecrm-test-backend', + 'base_url' => 'https://crm.example.coop', + 'credential' => 'suitecrm-test-credential', + 'headers' => array(), + ) + ); + + $addon = Addon::addon( 'suitecrm' ); + $endpoints = $addon->get_endpoints( 'suitecrm-test-backend' ); + + $this->assertIsArray( $endpoints ); + $this->assertContains( 'Contacts', $endpoints ); + $this->assertContains( 'Leads', $endpoints ); + $this->assertContains( 'Accounts', $endpoints ); + $this->assertContains( 'Opportunities', $endpoints ); + } + + /** + * Test addon get_endpoint_schema method. + */ + public function test_addon_get_endpoint_schema() { + Backend::temp_registration( + array( + 'name' => 'suitecrm-test-backend', + 'base_url' => 'https://crm.example.coop', + 'credential' => 'suitecrm-test-credential', + 'headers' => array(), + ) + ); + + $addon = Addon::addon( 'suitecrm' ); + $schema = $addon->get_endpoint_schema( + 'Contacts', + 'suitecrm-test-backend', + 'set_entry' + ); + + $this->assertIsArray( $schema ); + $this->assertNotEmpty( $schema ); + + $field_names = array_column( $schema, 'name' ); + $this->assertContains( 'first_name', $field_names ); + $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. + */ + public function test_bridge_schema_hook() { + $schema = \FORMS_BRIDGE\Form_Bridge::schema( 'suitecrm' ); + + $this->assertArrayHasKey( 'properties', $schema ); + $this->assertArrayHasKey( 'method', $schema['properties'] ); + + // Verify the method enum contains SuiteCRM-specific methods. + $this->assertContains( 'set_entry', $schema['properties']['method']['enum'] ); + $this->assertContains( 'get_entry', $schema['properties']['method']['enum'] ); + $this->assertContains( 'get_entry_list', $schema['properties']['method']['enum'] ); + } + + /** + * Test MD5 password hashing in login. + */ + public function test_password_md5_hashing() { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'test-md5-bridge', + 'backend' => 'suitecrm-test-backend', + 'endpoint' => 'Contacts', + 'method' => 'get_entry_list', + ) + ); + + $response = $bridge->submit(); + + // Check that a request was made. + $this->assertNotNull( self::$request ); + + // The first request should be a login request. + // We can't easily verify the MD5 hash in the intercepted request, + // but we can verify the flow completed successfully. + $this->assertFalse( is_wp_error( $response ) ); + } + + /** + * Test invalid backend handling. + */ + public function test_invalid_backend() { + $bridge = new SuiteCRM_Form_Bridge( + array( + 'name' => 'test-invalid-backend-bridge', + 'backend' => 'non-existent-backend', + 'endpoint' => 'Contacts', + 'method' => 'set_entry', + ) + ); + + $response = $bridge->submit( array( 'first_name' => 'Test' ) ); + + $this->assertTrue( is_wp_error( $response ) ); + $this->assertEquals( 'invalid_backend', $response->get_error_code() ); + } +} diff --git a/tests/test-custom-hooks.php b/tests/test-custom-hooks.php index a6c0b4b..c3507bc 100644 --- a/tests/test-custom-hooks.php +++ b/tests/test-custom-hooks.php @@ -123,6 +123,7 @@ public static function set_up_before_class() { } public function set_up() { + parent::set_up(); add_filter( 'pre_http_request', array( $this, 'pre_http_request' ), 10, 3 ); } @@ -135,6 +136,7 @@ public static function tear_down_after_class() { public function tear_down() { remove_filter( 'pre_http_request', array( $this, 'pre_http_request' ), 10, 3 ); + parent::tear_down(); } public function test_backends() { From 8718e717c7687152286a65cec642c127cbb41c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 11:23:37 +0100 Subject: [PATCH 07/12] fix: suitecrm accounts bridge template endpoint --- forms-bridge/addons/suitecrm/jobs/account.php | 5 +++++ forms-bridge/addons/suitecrm/templates/accounts.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/forms-bridge/addons/suitecrm/jobs/account.php b/forms-bridge/addons/suitecrm/jobs/account.php index 1e23e15..7905329 100644 --- a/forms-bridge/addons/suitecrm/jobs/account.php +++ b/forms-bridge/addons/suitecrm/jobs/account.php @@ -131,6 +131,10 @@ 'name' => 'sic_code', 'schema' => array( 'type' => 'string' ), ), + array( + 'name' => 'parent_id', + 'schema' => array( 'type' => 'string' ), + ), array( 'name' => 'parent_name', 'schema' => array( 'type' => 'string' ), @@ -199,6 +203,7 @@ function forms_bridge_suitecrm_create_account( $payload, $bridge ) { 'shipping_address_state', 'shipping_address_country', 'sic_code', + 'parent_id', 'parent_name', 'campaign_id', 'campaign_name', diff --git a/forms-bridge/addons/suitecrm/templates/accounts.php b/forms-bridge/addons/suitecrm/templates/accounts.php index 8537581..74403ff 100644 --- a/forms-bridge/addons/suitecrm/templates/accounts.php +++ b/forms-bridge/addons/suitecrm/templates/accounts.php @@ -24,7 +24,7 @@ array( 'ref' => '#bridge', 'name' => 'endpoint', - 'value' => 'Accounts', + 'value' => 'Contacts', ), array( 'ref' => '#bridge/custom_fields[]', From 0b2d0408b33ad511ac3355aec71ca9d0b16d3c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 11:48:25 +0100 Subject: [PATCH 08/12] feat: suitecrm meetings template --- .../addons/suitecrm/templates/accounts.php | 2 +- .../addons/suitecrm/templates/meetings.php | 286 ++++++++++++++++++ 2 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 forms-bridge/addons/suitecrm/templates/meetings.php diff --git a/forms-bridge/addons/suitecrm/templates/accounts.php b/forms-bridge/addons/suitecrm/templates/accounts.php index 74403ff..c3290e9 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 form template. The resulting bridge will convert form submissions into SuiteCRM accounts (companies/organizations).', + 'Account bridge template. The resulting bridge will convert form submissions into SuiteCRM accounts (companies/organizations).', 'forms-bridge' ), 'fields' => array( diff --git a/forms-bridge/addons/suitecrm/templates/meetings.php b/forms-bridge/addons/suitecrm/templates/meetings.php new file mode 100644 index 0000000..61bfe4f --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/meetings.php @@ -0,0 +1,286 @@ + __( 'Meetings', 'forms-bridge' ), + 'description' => __( + 'Meetings bridge template. The resulting bridge will convert form submissions into SuiteCRM meetings.', + 'forms-bridge', + ), + 'fields' => array( + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Accounts', 'forms-bridge' ), + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => 'Meetings', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'duration', + 'label' => __( 'Duration (Hours)', 'forms-bridge' ), + 'type' => 'number', + 'default' => 1, + 'required' => true, + ), + 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' => 'entry_list[].id', + 'label' => 'entry_list[].name_value_list.user_name.value', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'lead_source', + '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' => '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' => 'Meetings', + 'method' => 'set_entry', + 'workflow' => array( 'contact' ), + ), + 'form' => array( + 'title' => __( 'Meetings', 'forms-bridge' ), + 'fields' => array( + array( + 'label' => __( 'First Name', 'forms-bridge' ), + 'name' => 'first_name', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Last Name', 'forms-bridge' ), + 'name' => 'last_name', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Email', 'forms-bridge' ), + 'name' => 'email1', + 'type' => 'email', + 'required' => true, + ), + array( + 'label' => __( 'Phone', 'forms-bridge' ), + 'name' => 'phone_work', + 'type' => 'tel', + ), + array( + 'label' => __( 'Mobile', 'forms-bridge' ), + 'name' => 'phone_mobile', + 'type' => 'tel', + ), + 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', + ), + ), + ), + ), + ), +); From de37f74a5c77b8db1d7f37742ac0df4b84095531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Mon, 24 Nov 2025 18:20:22 +0100 Subject: [PATCH 09/12] feat: suitecrm meeting invitees --- .../dolibarr/jobs/appointment-dates.php | 7 +- .../suitecrm/class-suitecrm-form-bridge.php | 10 +- forms-bridge/addons/suitecrm/jobs/account.php | 6 +- forms-bridge/addons/suitecrm/jobs/contact.php | 281 ++++++++++++++++++ .../addons/suitecrm/jobs/meeting-invitees.php | 115 +++++++ .../addons/suitecrm/templates/meetings.php | 131 +++++++- 6 files changed, 536 insertions(+), 14 deletions(-) create mode 100644 forms-bridge/addons/suitecrm/jobs/contact.php create mode 100644 forms-bridge/addons/suitecrm/jobs/meeting-invitees.php diff --git a/forms-bridge/addons/dolibarr/jobs/appointment-dates.php b/forms-bridge/addons/dolibarr/jobs/appointment-dates.php index 6a6a66a..57bf582 100644 --- a/forms-bridge/addons/dolibarr/jobs/appointment-dates.php +++ b/forms-bridge/addons/dolibarr/jobs/appointment-dates.php @@ -1,4 +1,9 @@ $session_id, - 'module_name' => $module, - 'id' => $payload['id'] ?? '', - 'select_fields' => $more_args['select_fields'] ?? array(), + 'session' => $session_id, + 'module_name' => $module, + 'id' => $payload['id'] ?? '', + 'select_fields' => $more_args['select_fields'] ?? array(), + 'link_name_to_fields_array' => array(), + 'track_view' => false, ); case 'set_entry': diff --git a/forms-bridge/addons/suitecrm/jobs/account.php b/forms-bridge/addons/suitecrm/jobs/account.php index 7905329..6df1543 100644 --- a/forms-bridge/addons/suitecrm/jobs/account.php +++ b/forms-bridge/addons/suitecrm/jobs/account.php @@ -11,7 +11,7 @@ return array( 'title' => __( 'Account', 'forms-bridge' ), - 'description' => __( 'Creates an account on SuiteCRM', 'forms-bridge' ), + 'description' => __( 'Creates an account in SuiteCRM', 'forms-bridge' ), 'method' => 'forms_bridge_suitecrm_create_account', 'input' => array( array( @@ -116,7 +116,7 @@ 'schema' => array( 'type' => 'string' ), ), array( - 'name' => 'shipping__address_city', + 'name' => 'shipping_address_city', 'schema' => array( 'type' => 'string' ), ), array( @@ -199,7 +199,7 @@ function forms_bridge_suitecrm_create_account( $payload, $bridge ) { 'shipping_address_street_3', 'shipping_address_street_4', 'shipping_address_postalcode', - 'shipping__address_city', + 'shipping_address_city', 'shipping_address_state', 'shipping_address_country', 'sic_code', diff --git a/forms-bridge/addons/suitecrm/jobs/contact.php b/forms-bridge/addons/suitecrm/jobs/contact.php new file mode 100644 index 0000000..3740869 --- /dev/null +++ b/forms-bridge/addons/suitecrm/jobs/contact.php @@ -0,0 +1,281 @@ + __( 'Contact', 'forms-bridge' ), + 'description' => __( 'Creates a contact in SuiteCRM', 'forms-bridge' ), + 'method' => 'forms_bridge_suitecrm_create_contact', + 'input' => array( + array( + 'name' => 'first_name', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'last_name', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'assigned_user_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'assigned_user_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'salutation', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'full_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'birthdate', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'title', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'photo', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'department', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'do_not_call', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'phone_home', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'phone_work', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'phone_mobile', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'phone_other', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'phone_fax', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email1', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email2', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email_address_non_primary', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'email_and_name1', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'assistant', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'assistant_phone', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'primary_address_street', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'primary_address_street2', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'primary_address_street3', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'primary_address_city', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'primary_address_postalcode', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'primary_address_state', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'primary_address_country', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'lawful_basis_source', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'lead_source', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'account_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'account_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'opportunity_role', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'opportunity_role_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'campaign_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'campaign_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'accept_status_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'accept_status_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'event_status_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'event_status_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'event_invite_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'event_accept_status', + '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 Form_Bridge $bridge Bridge object. + * + * @return array + */ +function forms_bridge_suitecrm_create_contact( $payload, $bridge ) { + $account = array( + 'first_name' => $payload['first_name'], + 'last_name' => $payload['last_name'], + ); + + $account_fields = array( + 'assigned_user_id', + 'assigned_user_name', + 'salutation', + 'full_name', + 'birthdate', + 'title', + 'photo', + 'department', + 'do_not_call', + 'phone_home', + 'phone_work', + 'phone_mobile', + 'phone_fax', + 'phone_other', + 'email', + 'email1', + 'email2', + 'email_address_non_primary', + 'email_and_name1', + 'assistant', + 'assistant_phone', + 'primary_address_street', + 'primary_address_street2', + 'primary_address_street3', + 'primary_address_postalcode', + 'primary_address_city', + 'primary_address_state', + 'primary_address_country', + 'lawful_basis_source', + 'lead_source', + 'account_name', + 'account_id', + 'opportunity_role', + 'opportunity_role_id', + 'campaign_name', + 'campaign_id', + 'accept_status_name', + 'accept_status_id', + 'event_status_id', + 'event_invite_id', + 'event_accept_status', + ); + + foreach ( $account_fields as $field ) { + if ( isset( $payload[ $field ] ) ) { + $account[ $field ] = $payload[ $field ]; + } + } + + $response = $bridge->patch( + array( + 'method' => 'set_entry', + 'endpoint' => 'Contacts', + ) + )->submit( $account ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $payload['contact_id'] = $response['data']['id']; + return $payload; +} diff --git a/forms-bridge/addons/suitecrm/jobs/meeting-invitees.php b/forms-bridge/addons/suitecrm/jobs/meeting-invitees.php new file mode 100644 index 0000000..05deb21 --- /dev/null +++ b/forms-bridge/addons/suitecrm/jobs/meeting-invitees.php @@ -0,0 +1,115 @@ + __( 'Meeting invitees', 'forms-bridge' ), + 'description' => __( 'Adds invitees to a SuiteCRM meeting', 'forms-bridge' ), + 'method' => 'forms_bridge_suitecrm_add_meeting_invitees', + 'input' => array( + array( + 'name' => 'contact_id', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'assigned_user_id', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + ), + 'output' => array( + array( + 'name' => 'contact_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'assigned_user_id', + 'schema' => array( 'type' => 'string' ), + ), + ), +); + +/** + * Adds invitees to a meeting (contact and assigned user). + * + * @param array $payload Bridge payload. + * + * @return array|WP_Error + */ +function forms_bridge_suitecrm_add_meeting_invitees( $payload ) { + $invitees = array( + 'contact_id' => $payload['contact_id'], + 'assigned_user_id' => $payload['assigned_user_id'], + ); + + add_action( + 'forms_bridge_after_submission', + function ( $bridge, $response, $payload, $attachments ) use ( $invitees ) { + $meeting_id = $response['data']['id']; + + // Add contact relationship. + if ( ! empty( $invitees['contact_id'] ) ) { + $contact_relationship = array( + 'module_id' => $meeting_id, + 'link_field_name' => 'contacts', + 'related_ids' => array( $invitees['contact_id'] ), + ); + + $response = $bridge->patch( + array( + 'method' => 'set_relationship', + 'endpoint' => 'Meetings', + ) + )->submit( $contact_relationship ); + + if ( is_wp_error( $response ) ) { + do_action( + 'forms_bridge_on_failure', + $bridge, + $response, + $payload, + $attachments, + ); + } + } + + // Add assigned user relationship. + if ( ! empty( $invitees['assigned_user_id'] ) ) { + $user_relationship = array( + 'module_id' => $meeting_id, + 'link_field_name' => 'users', + 'related_ids' => array( $invitees['assigned_user_id'] ), + ); + + $response = $bridge->patch( + array( + 'method' => 'set_relationship', + 'endpoint' => 'Meetings', + ) + )->submit( $user_relationship ); + + if ( is_wp_error( $response ) ) { + do_action( + 'forms_bridge_on_failure', + $bridge, + $response, + $payload, + $attachments, + ); + } + } + }, + 10, + 4 + ); + + return $payload; +} diff --git a/forms-bridge/addons/suitecrm/templates/meetings.php b/forms-bridge/addons/suitecrm/templates/meetings.php index 61bfe4f..1bf252d 100644 --- a/forms-bridge/addons/suitecrm/templates/meetings.php +++ b/forms-bridge/addons/suitecrm/templates/meetings.php @@ -19,7 +19,7 @@ array( 'ref' => '#form', 'name' => 'title', - 'default' => __( 'Accounts', 'forms-bridge' ), + 'default' => __( 'Meetings', 'forms-bridge' ), ), array( 'ref' => '#bridge', @@ -28,15 +28,41 @@ ), array( 'ref' => '#bridge/custom_fields[]', - 'name' => 'duration', + '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', + 'name' => 'meeting_assigned_user_id', 'label' => __( 'Assigned User', 'forms-bridge' ), 'description' => __( 'User to assign the account to', @@ -50,6 +76,7 @@ 'label' => 'entry_list[].name_value_list.user_name.value', ), ), + 'required' => true, ), array( 'ref' => '#bridge/custom_fields[]', @@ -86,9 +113,90 @@ ), ), 'bridge' => array( - 'endpoint' => 'Meetings', - 'method' => 'set_entry', - 'workflow' => array( 'contact' ), + 'endpoint' => 'Meetings', + 'method' => 'set_entry', + 'custom_fields' => array( + array( + 'name' => 'meeting_status', + 'value' => 'Planned', + ), + array( + 'name' => 'meeting_type', + 'value' => 'Sugar', + ), + array( + 'name' => 'parent_type', + 'value' => 'Contacts', + ), + ), + 'workflow' => array( 'date-fields-to-date', 'contact', 'meeting-invitees' ), + 'mutations' => array( + array(), + array( + array( + 'from' => 'first_name', + 'to' => 'meeting_name[0]', + 'cast' => 'copy', + ), + array( + 'from' => 'last_name', + 'to' => 'meeting_name[1]', + 'cast' => 'copy', + ), + array( + 'from' => 'meeting_name', + 'to' => 'meeting_name', + 'cast' => 'concat', + ), + ), + array( + array( + 'from' => 'contact_id', + 'to' => 'parent_id', + 'cast' => 'copy', + ), + array( + 'from' => 'meeting_assigned_user_id', + 'to' => 'assigned_user_id', + 'cast' => 'string', + ), + array( + 'from' => 'meeting_type', + 'to' => 'type', + 'cast' => 'string', + ), + array( + 'from' => 'meeting_status', + 'to' => 'status', + 'cast' => 'string', + ), + array( + 'from' => 'meeting_name', + 'to' => 'name', + 'cast' => 'string', + ), + array( + 'from' => 'meeting_description', + 'to' => 'description', + 'cast' => 'string', + ), + array( + 'from' => 'datetime', + 'to' => 'date_start', + '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' ), @@ -121,6 +229,12 @@ 'name' => 'phone_mobile', 'type' => 'tel', ), + array( + 'name' => 'date', + 'label' => __( 'Date', 'forms-bridge' ), + 'type' => 'date', + 'required' => true, + ), array( 'name' => 'hour', 'label' => __( 'Hour', 'forms-bridge' ), @@ -281,6 +395,11 @@ ), ), ), + array( + 'name' => 'meeting_description', + 'type' => 'textarea', + 'label' => __( 'Comments', 'forms-bridge' ), + ), ), ), ); From e78e7c475f4e3d4a8858b76db9c831a656f93523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Tue, 25 Nov 2025 01:06:32 +0100 Subject: [PATCH 10/12] feat: remove suitecrm more_args bridge submit argument --- .../suitecrm/class-suitecrm-form-bridge.php | 39 +++++++------------ tests/addons/test-suitecrm.php | 6 +-- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php index 3ae063e..73f5c36 100644 --- a/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php +++ b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php @@ -7,7 +7,6 @@ namespace FORMS_BRIDGE; -use HTTP_BRIDGE\Http_Client; use WP_Error; if ( ! defined( 'ABSPATH' ) ) { @@ -70,15 +69,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', + 'SuiteCRM null response body', $res ); } @@ -172,11 +165,11 @@ public function __construct( $data ) { * Submits submission to the backend. * * @param array $payload Submission data. - * @param array $more_args Additional arguments (e.g., select_fields for get_entry_list). + * @param array $attachments Submission attachment files. * * @return array|WP_Error HTTP response. */ - public function submit( $payload = array(), $more_args = array() ) { + public function submit( $payload = array(), $attachments = array() ) { if ( ! $this->is_valid ) { return new WP_Error( 'invalid_bridge', @@ -219,7 +212,7 @@ static function ( $request ) { } // Build the API request based on method. - $rest_args = $this->build_rest_args( $session_id, $payload, $more_args ); + $rest_args = $this->build_rest_args( $session_id, $payload ); $api_payload = self::rest_payload( $this->method, $rest_args ); @@ -233,7 +226,6 @@ static function ( $request ) { // Normalize response. $response['data'] = $result; - return $response; } @@ -242,11 +234,10 @@ static function ( $request ) { * * @param string $session_id Session ID. * @param array $payload Form submission payload. - * @param array $more_args Additional arguments. * * @return array REST API arguments. */ - private function build_rest_args( $session_id, $payload, $more_args ) { + private function build_rest_args( $session_id, $payload ) { $module = $this->endpoint; switch ( $this->method ) { @@ -265,12 +256,12 @@ private function build_rest_args( $session_id, $payload, $more_args ) { $args = array( 'session' => $session_id, 'module_name' => $module, - 'query' => $more_args['query'] ?? '', - 'order_by' => $more_args['order_by'] ?? '', - 'offset' => $more_args['offset'] ?? 0, - 'select_fields' => $more_args['select_fields'] ?? array(), - 'max_results' => $more_args['max_results'] ?? 20, - 'deleted' => $more_args['deleted'] ?? 0, + 'query' => $payload['query'] ?? '', + 'order_by' => $payload['order_by'] ?? '', + 'offset' => $payload['offset'] ?? 0, + 'select_fields' => $payload['select_fields'] ?? array(), + 'max_results' => $payload['max_results'] ?? 20, + 'deleted' => $payload['deleted'] ?? 0, ); return $args; @@ -279,7 +270,7 @@ private function build_rest_args( $session_id, $payload, $more_args ) { 'session' => $session_id, 'module_name' => $module, 'id' => $payload['id'] ?? '', - 'select_fields' => $more_args['select_fields'] ?? array(), + 'select_fields' => $payload['select_fields'] ?? array(), 'link_name_to_fields_array' => array(), 'track_view' => false, ); @@ -317,8 +308,8 @@ private function build_rest_args( $session_id, $payload, $more_args ) { 'module_name' => $module, 'module_id' => $payload['module_id'] ?? '', 'link_field_name' => $payload['link_field_name'] ?? '', - 'related_module_query' => $more_args['query'] ?? '', - 'related_fields' => $more_args['select_fields'] ?? array(), + 'related_module_query' => $payload['query'] ?? '', + 'related_fields' => $payload['select_fields'] ?? array(), ); default: diff --git a/tests/addons/test-suitecrm.php b/tests/addons/test-suitecrm.php index c05047d..dd84106 100644 --- a/tests/addons/test-suitecrm.php +++ b/tests/addons/test-suitecrm.php @@ -398,10 +398,7 @@ public function test_login_flow() { ) ); - $response = $bridge->submit( - array(), - array( 'max_results' => 10 ) - ); + $response = $bridge->submit( array( 'max_results' => 10 ) ); $this->assertFalse( is_wp_error( $response ) ); @@ -450,7 +447,6 @@ public function test_get_entry_list() { ); $response = $bridge->submit( - array(), array( 'select_fields' => array( 'id', 'first_name', 'last_name' ), 'max_results' => 20, From 6433ee29fb400c7737f1666bc934716d1b9be174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Tue, 25 Nov 2025 01:08:42 +0100 Subject: [PATCH 11/12] feat: suitecrm contacts duplicities --- forms-bridge/addons/suitecrm/jobs/account.php | 30 ++++++++ forms-bridge/addons/suitecrm/jobs/contact.php | 59 ++++++++++++--- .../addons/suitecrm/jobs/skip-contact.php | 75 +++++++++++++++++++ .../addons/suitecrm/templates/accounts.php | 4 +- .../addons/suitecrm/templates/contacts.php | 3 +- .../addons/suitecrm/templates/leads.php | 2 +- .../addons/suitecrm/templates/meetings.php | 2 +- 7 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 forms-bridge/addons/suitecrm/jobs/skip-contact.php diff --git a/forms-bridge/addons/suitecrm/jobs/account.php b/forms-bridge/addons/suitecrm/jobs/account.php index 6df1543..9d2176e 100644 --- a/forms-bridge/addons/suitecrm/jobs/account.php +++ b/forms-bridge/addons/suitecrm/jobs/account.php @@ -216,6 +216,36 @@ function forms_bridge_suitecrm_create_account( $payload, $bridge ) { } } + $query = "accounts.name = '{$account['name']}'"; + + $response = $bridge->patch( + array( + 'method' => 'get_entry_list', + 'endpoint' => 'Accounts', + ) + )->submit( array( 'query' => $query ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $account_id = $response['data']['entry_list'][0]['id']; + if ( ! empty( $account_id ) ) { + $response = $bridge->patch( + array( + 'method' => 'set_entry', + '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' => 'set_entry', diff --git a/forms-bridge/addons/suitecrm/jobs/contact.php b/forms-bridge/addons/suitecrm/jobs/contact.php index 3740869..b7a4f4b 100644 --- a/forms-bridge/addons/suitecrm/jobs/contact.php +++ b/forms-bridge/addons/suitecrm/jobs/contact.php @@ -15,14 +15,13 @@ 'method' => 'forms_bridge_suitecrm_create_contact', 'input' => array( array( - 'name' => 'first_name', + 'name' => 'last_name', 'schema' => array( 'type' => 'string' ), 'required' => true, ), array( - 'name' => 'last_name', - 'schema' => array( 'type' => 'string' ), - 'required' => true, + 'name' => 'first_name', + 'schema' => array( 'type' => 'string' ), ), array( 'name' => 'assigned_user_id', @@ -210,12 +209,12 @@ * @return array */ function forms_bridge_suitecrm_create_contact( $payload, $bridge ) { - $account = array( - 'first_name' => $payload['first_name'], - 'last_name' => $payload['last_name'], + $contact = array( + 'last_name' => $payload['last_name'], ); - $account_fields = array( + $contact_fields = array( + 'first_name', 'assigned_user_id', 'assigned_user_name', 'salutation', @@ -230,8 +229,8 @@ function forms_bridge_suitecrm_create_contact( $payload, $bridge ) { 'phone_mobile', 'phone_fax', 'phone_other', - 'email', 'email1', + 'email', 'email2', 'email_address_non_primary', 'email_and_name1', @@ -259,18 +258,54 @@ function forms_bridge_suitecrm_create_contact( $payload, $bridge ) { 'event_accept_status', ); - foreach ( $account_fields as $field ) { + foreach ( $contact_fields as $field ) { if ( isset( $payload[ $field ] ) ) { - $account[ $field ] = $payload[ $field ]; + $contact[ $field ] = $payload[ $field ]; + } + } + + $query = "contacts.last_name = '{$contact['last_name']}'"; + + if ( isset( $contact['last_name'] ) ) { + $query .= " AND contacts.first_name = '{$contact['first_name']}'"; + } + + $response = $bridge->patch( + array( + 'method' => 'get_entry_list', + 'endpoint' => 'Contacts', + ) + )->submit( array( 'query' => $query ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $contact_id = $response['data']['entry_list'][0]['id'] ?? null; + if ( ! empty( $contact_id ) ) { + $response = $bridge->patch( + array( + 'name' => '__suitecrm-' . time(), + 'method' => 'set_entry', + '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' => '__suitecrm-' . time(), 'method' => 'set_entry', 'endpoint' => 'Contacts', ) - )->submit( $account ); + )->submit( $contact ); if ( is_wp_error( $response ) ) { return $response; diff --git a/forms-bridge/addons/suitecrm/jobs/skip-contact.php b/forms-bridge/addons/suitecrm/jobs/skip-contact.php new file mode 100644 index 0000000..d06ebc9 --- /dev/null +++ b/forms-bridge/addons/suitecrm/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_suitecrm_skip_contact', + 'input' => array( + array( + 'name' => 'last_name', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'first_name', + 'schema' => array( 'type' => 'string' ), + ), + ), + 'output' => array( + array( + 'name' => 'last_name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'first_name', + 'schema' => array( 'type' => 'string' ), + ), + ), +); + +/** + * Look up existing contacts by name and skips bridge submission if found. + * + * @param array $payload Bridge payload. + * @param SuiteCRM_Form_Bridge $bridge Bridge object. + */ +function forms_bridge_suitecrm_skip_contact( $payload, $bridge ) { + $query = "contacts.last_name = '{$payload['last_name']}'"; + + if ( isset( $payload['first_name'] ) ) { + $query .= " AND contacts.first_name = '{$payload['first_name']}'"; + } + + $response = $bridge->patch( + array( + 'method' => 'get_entry_list', + 'endpoint' => 'Contacts', + ) + )->submit( array( 'query' => $query ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $contact_id = $response['data']['entry_list'][0]['id'] ?? null; + if ( ! empty( $contact_id ) ) { + $result = forms_bridge_suitecrm_create_contact( $payload, $bridge ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + return; + } + + return $payload; +} diff --git a/forms-bridge/addons/suitecrm/templates/accounts.php b/forms-bridge/addons/suitecrm/templates/accounts.php index c3290e9..ab6746f 100644 --- a/forms-bridge/addons/suitecrm/templates/accounts.php +++ b/forms-bridge/addons/suitecrm/templates/accounts.php @@ -39,7 +39,7 @@ 'endpoint' => 'Users', 'finger' => array( 'value' => 'entry_list[].id', - 'label' => 'entry_list[].name_value_list.user_name.value', + 'label' => 'entry_list[].name_value_list.name.value', ), ), ), @@ -229,7 +229,7 @@ 'bridge' => array( 'endpoint' => 'Contacts', 'method' => 'set_entry', - 'workflow' => array( 'account' ), + 'workflow' => array( 'account', 'skip-contact' ), ), 'form' => array( 'fields' => array( diff --git a/forms-bridge/addons/suitecrm/templates/contacts.php b/forms-bridge/addons/suitecrm/templates/contacts.php index df7ad19..415e51e 100644 --- a/forms-bridge/addons/suitecrm/templates/contacts.php +++ b/forms-bridge/addons/suitecrm/templates/contacts.php @@ -35,7 +35,7 @@ 'endpoint' => 'Users', 'finger' => array( 'value' => 'entry_list[].id', - 'label' => 'entry_list[].name_value_list.user_name.value', + 'label' => 'entry_list[].name_value_list.name.value', ), ), ), @@ -76,6 +76,7 @@ 'bridge' => array( 'endpoint' => 'Contacts', 'method' => 'set_entry', + 'workflow' => array( 'skip-contact' ), ), 'form' => array( 'fields' => array( diff --git a/forms-bridge/addons/suitecrm/templates/leads.php b/forms-bridge/addons/suitecrm/templates/leads.php index 6fc6de1..fde7796 100644 --- a/forms-bridge/addons/suitecrm/templates/leads.php +++ b/forms-bridge/addons/suitecrm/templates/leads.php @@ -35,7 +35,7 @@ 'endpoint' => 'Users', 'finger' => array( 'value' => 'entry_list[].id', - 'label' => 'entry_list[].name_value_list.user_name.value', + 'label' => 'entry_list[].name_value_list.name.value', ), ), ), diff --git a/forms-bridge/addons/suitecrm/templates/meetings.php b/forms-bridge/addons/suitecrm/templates/meetings.php index 1bf252d..26bdf6d 100644 --- a/forms-bridge/addons/suitecrm/templates/meetings.php +++ b/forms-bridge/addons/suitecrm/templates/meetings.php @@ -73,7 +73,7 @@ 'endpoint' => 'Users', 'finger' => array( 'value' => 'entry_list[].id', - 'label' => 'entry_list[].name_value_list.user_name.value', + 'label' => 'entry_list[].name_value_list.name.value', ), ), 'required' => true, From 401cf7c378a2a07aa5166081b0eafd18d9237ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= Date: Tue, 25 Nov 2025 01:16:59 +0100 Subject: [PATCH 12/12] feat: add suitecrm to readmes --- README.md | 31 ++++++++++++++++--------------- forms-bridge/readme.txt | 31 ++++++++++++++++--------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 674ec97..f83c189 100644 --- a/README.md +++ b/README.md @@ -36,21 +36,22 @@ to work with specific APIs, new workflow jobs and bridge templates. Forms Bridge has the following addons: -- [REST API](https://en.wikipedia.org/wiki/REST) -- [Bigin](https://www.bigin.com/developer/docs/apis/v2/?source=developer) -- [Brevo](https://developers.brevo.com/) -- [Dolibarr]() -- [FinanCoop](https://somit.coop/financoop/) -- [Google Sheets](https://workspace.google.com/products/sheets/) -- [Holded](https://developers.holded.com/reference/api-key) -- [Listmonk](https://listmonk.app/docs/apis/apis/) -- [Nextcloud](https://docs.nextcloud.com/server/20/user_manual/en/files/access_webdav.html) -- [Mailchimp](https://mailchimp.com/developer/) -- [Odoo](https://www.odoo.com/) -- [Rocket.Chat](https://developer.rocket.chat/apidocs) -- [Slack](https://docs.slack.dev/reference/) -- [Zoho CRM](https://www.zoho.com/developer/rest-api.html) -- [Zulip](https://zulip.com/api/rest) +- [REST API](https://formsbridge.codeccoop.org/documentation/#backends) +- [Bigin](https://formsbridge.codeccoop.org/documentation/bigin/) +- [Brevo](https://formsbridge.codeccoop.org/documentation/brevo/) +- [Dolibarr](https://formsbridge.codeccoop.org/documentation/dolibarr/) +- [FinanCoop](https://formsbridge.codeccoop.org/documentation/financoop/) +- [Google Sheets](https://formsbridge.codeccoop.org/documentation/google-sheets/) +- [Holded](https://formsbridge.codeccoop.org/documentation/holded/) +- [Listmonk](https://formsbridge.codeccoop.org/documentation/listmonk/) +- [Nextcloud](https://formsbridge.codeccoop.org/documentation/nextcloud/) +- [Mailchimp](https://formsbridge.codeccoop.org/documentation/mailchimp/) +- [Odoo](https://formsbridge.codeccoop.org/documentation/odoo/) +- [Rocket.Chat](https://formsbridge.codeccoop.org/documentation/rocket-chat/) +- [Slack](https://formsbridge.codeccoop.org/documentation/slack/) +- [SuiteCRM](https://formsbridge.codeccoop.org/documentation/suitecrm/) +- [Zoho CRM](https://formsbridge.codeccoop.org/documentation/zoho-crm/) +- [Zulip](https://formsbridge.codeccoop.org/documentation/zulip/) ## Backends diff --git a/forms-bridge/readme.txt b/forms-bridge/readme.txt index 70758d3..2125bb0 100644 --- a/forms-bridge/readme.txt +++ b/forms-bridge/readme.txt @@ -31,21 +31,22 @@ Forms Bridge comes with free addons. Each addon adds to the plugin new bridges t Forms Bridge has the following addons: -* [REST API](https://en.wikipedia.org/wiki/REST) -* [Bigin](https://www.bigin.com/developer/docs/apis/v2/?source=developer) -* [Brevo](https://developers.brevo.com/) -* [Dolibarr](https://wiki.dolibarr.org/index.php/Module_Web_Services_API_REST_(developer)) -* [FinanCoop](https://somit.coop/financoop/) -* [Google Sheets](https://workspace.google.com/products/sheets/) -* [Holded](https://developers.holded.com/reference/api-key) -* [Listmonk](https://listmonk.app/docs/apis/apis/) -* [Nextcloud](https://docs.nextcloud.com/server/20/user_manual/en/files/access_webdav.html) -* [Mailchimp](https://mailchimp.com/developer/) -* [Odoo](https://www.odoo.com/) -* [Rocket.Chat](https://developer.rocket.chat/apidocs) -* [Slack](https://docs.slack.dev/reference/) -* [Zoho CRM](https://www.zoho.com/developer/rest-api.html) -* [Zulip](https://zulip.com/api/rest) +* [REST API](https://formsbridge.codeccoop.org/documentation/#backends) +* [Bigin](https://formsbridge.codeccoop.org/documentation/bigin/) +* [Brevo](https://formsbridge.codeccoop.org/documentation/brevo/) +* [Dolibarr](https://formsbridge.codeccoop.org/documentation/dolibarr/) +* [FinanCoop](https://formsbridge.codeccoop.org/documentation/financoop/) +* [Google Sheets](https://formsbridge.codeccoop.org/documentation/google-sheets/) +* [Holded](https://formsbridge.codeccoop.org/documentation/holded/) +* [Listmonk](https://formsbridge.codeccoop.org/documentation/listmonk/) +* [Nextcloud](https://formsbridge.codeccoop.org/documentation/nextcloud/) +* [Mailchimp](https://formsbridge.codeccoop.org/documentation/mailchimp/) +* [Odoo](https://formsbridge.codeccoop.org/documentation/odoo/) +* [Rocket.Chat](https://formsbridge.codeccoop.org/documentation/rocket-chat/) +* [Slack](https://formsbridge.codeccoop.org/documentation/slack/) +* [SuiteCRM](https://formsbridge.codeccoop.org/documentation/suitecrm/) +* [Zoho CRM](https://formsbridge.codeccoop.org/documentation/zoho-crm/) +* [Zulip](https://formsbridge.codeccoop.org/documentation/zulip/) == Backends ==