diff --git a/README.md b/README.md index 674ec973..f83c189e 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/addons/dolibarr/jobs/appointment-dates.php b/forms-bridge/addons/dolibarr/jobs/appointment-dates.php index 6a6a66a3..57bf582a 100644 --- a/forms-bridge/addons/dolibarr/jobs/appointment-dates.php +++ b/forms-bridge/addons/dolibarr/jobs/appointment-dates.php @@ -1,4 +1,9 @@ '__suitecrm-' . time(), + 'method' => 'get_user_id', + '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 00000000..73f5c36e --- /dev/null +++ b/forms-bridge/addons/suitecrm/class-suitecrm-form-bridge.php @@ -0,0 +1,323 @@ + $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'] ) ) { + return new WP_Error( + 'bad_request', + 'SuiteCRM null response body', + $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 Credential $credential Bridge credential object. + * @param Backend $backend Bridge backend object. + * + * @return string|WP_Error Session ID on success. + */ + private static function rest_login( $credential, $backend ) { + if ( self::$session_id ) { + return self::$session_id; + } + + $username = $credential->client_id; + $password = $credential->client_secret; + + // 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 ); + + $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 $attachments Submission attachment files. + * + * @return array|WP_Error HTTP response. + */ + public function submit( $payload = array(), $attachments = 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 + ); + + $session_id = self::rest_login( $credential, $backend ); + + if ( is_wp_error( $session_id ) ) { + return $session_id; + } + + // Build the API request based on method. + $rest_args = $this->build_rest_args( $session_id, $payload ); + + $api_payload = self::rest_payload( $this->method, $rest_args ); + + $response = $backend->post( self::ENDPOINT, $api_payload ); + + $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. + * + * @return array REST API arguments. + */ + private function build_rest_args( $session_id, $payload ) { + $module = $this->endpoint; + + switch ( $this->method ) { + 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' => $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; + + case 'get_entry': + return array( + 'session' => $session_id, + 'module_name' => $module, + 'id' => $payload['id'] ?? '', + 'select_fields' => $payload['select_fields'] ?? array(), + 'link_name_to_fields_array' => array(), + 'track_view' => false, + ); + + 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' => $payload['query'] ?? '', + 'related_fields' => $payload['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 00000000..b53729c3 --- /dev/null +++ b/forms-bridge/addons/suitecrm/hooks.php @@ -0,0 +1,169 @@ + 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' => '#backend/headers[]', + 'name' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), + 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' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), + 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/jobs/account.php b/forms-bridge/addons/suitecrm/jobs/account.php new file mode 100644 index 00000000..9d2176e6 --- /dev/null +++ b/forms-bridge/addons/suitecrm/jobs/account.php @@ -0,0 +1,262 @@ + __( 'Account', 'forms-bridge' ), + 'description' => __( 'Creates an account in 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_id', + '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_id', + 'parent_name', + 'campaign_id', + 'campaign_name', + 'description', + ); + + foreach ( $account_fields as $field ) { + if ( isset( $payload[ $field ] ) ) { + $account[ $field ] = $payload[ $field ]; + } + } + + $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', + '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/jobs/contact.php b/forms-bridge/addons/suitecrm/jobs/contact.php new file mode 100644 index 00000000..b7a4f4ba --- /dev/null +++ b/forms-bridge/addons/suitecrm/jobs/contact.php @@ -0,0 +1,316 @@ + __( 'Contact', 'forms-bridge' ), + 'description' => __( 'Creates a contact in SuiteCRM', 'forms-bridge' ), + 'method' => 'forms_bridge_suitecrm_create_contact', + 'input' => array( + array( + 'name' => 'last_name', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'first_name', + 'schema' => array( 'type' => 'string' ), + ), + 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 ) { + $contact = array( + 'last_name' => $payload['last_name'], + ); + + $contact_fields = array( + 'first_name', + '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', + 'email1', + 'email', + '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 ( $contact_fields as $field ) { + if ( isset( $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( $contact ); + + 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 00000000..05deb21e --- /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/jobs/skip-contact.php b/forms-bridge/addons/suitecrm/jobs/skip-contact.php new file mode 100644 index 00000000..d06ebc95 --- /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 new file mode 100644 index 00000000..ab6746f3 --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/accounts.php @@ -0,0 +1,301 @@ + __( 'Accounts', 'forms-bridge' ), + 'description' => __( + 'Account bridge 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' => 'Contacts', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'assigned_user_id', + 'label' => __( 'Assigned User', 'forms-bridge' ), + 'description' => __( + 'User to assign the account to', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'entry_list[].id', + 'label' => 'entry_list[].name_value_list.name.value', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'account_type', + 'label' => __( 'Account Type', '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' ), + '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' ), + ), + ), + ), + 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' => 'Contacts', + 'method' => 'set_entry', + 'workflow' => array( 'account', 'skip-contact' ), + ), + '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', + 'type' => 'text', + 'required' => true, + ), + array( + 'label' => __( 'Email', 'forms-bridge' ), + 'name' => 'email1', + 'type' => 'email', + ), + array( + 'label' => __( 'Phone', 'forms-bridge' ), + 'name' => 'phone_office', + 'type' => 'tel', + ), + array( + 'label' => __( 'Website', 'forms-bridge' ), + 'name' => 'website', + 'type' => 'url', + ), + array( + 'label' => __( 'Address', 'forms-bridge' ), + 'name' => 'billing_address_street', + 'type' => 'text', + ), + array( + 'label' => __( 'City', 'forms-bridge' ), + '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' => 'billing_address_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 00000000..415e51ee --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/contacts.php @@ -0,0 +1,143 @@ + __( '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' ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'entry_list[].id', + 'label' => 'entry_list[].name_value_list.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' => 'Contacts', + 'method' => 'set_entry', + 'workflow' => array( 'skip-contact' ), + ), + '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' => '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( + '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' => __( '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 00000000..fde77969 --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/leads.php @@ -0,0 +1,202 @@ + __( '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' ), + 'type' => 'select', + 'options' => array( + 'endpoint' => 'Users', + 'finger' => array( + 'value' => 'entry_list[].id', + 'label' => 'entry_list[].name_value_list.name.value', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'status', + 'label' => __( 'Lead Status', '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' ), + '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', + ), + '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' => '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( + 'label' => __( 'Company', 'forms-bridge' ), + 'name' => 'account_name', + 'type' => 'text', + ), + array( + 'label' => __( 'Title', 'forms-bridge' ), + 'name' => 'title', + 'type' => 'text', + ), + array( + 'label' => __( 'Website', 'forms-bridge' ), + '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', + 'type' => 'textarea', + ), + ), + ), +); diff --git a/forms-bridge/addons/suitecrm/templates/meetings.php b/forms-bridge/addons/suitecrm/templates/meetings.php new file mode 100644 index 00000000..26bdf6db --- /dev/null +++ b/forms-bridge/addons/suitecrm/templates/meetings.php @@ -0,0 +1,405 @@ + __( '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' => __( 'Meetings', 'forms-bridge' ), + ), + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => 'Meetings', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'duration_hours', + 'label' => __( 'Duration (Hours)', 'forms-bridge' ), + 'type' => 'number', + 'default' => 1, + 'required' => true, + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'duration_minutes', + 'label' => __( 'Duration (Minutes)', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( + array( + 'value' => '0', + 'label' => '.00', + ), + array( + 'value' => '15', + 'label' => '.15', + ), + array( + 'value' => '30', + 'label' => '.30', + ), + array( + 'value' => '45', + 'label' => '.45', + ), + ), + 'default' => '00', + 'required' => true, + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'meeting_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.name.value', + ), + ), + 'required' => true, + ), + 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', + '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' ), + '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' => 'date', + 'label' => __( 'Date', 'forms-bridge' ), + 'type' => 'date', + 'required' => true, + ), + array( + 'name' => 'hour', + 'label' => __( 'Hour', 'forms-bridge' ), + 'type' => 'select', + 'required' => true, + 'options' => array( + array( + 'label' => __( '1 AM', 'forms-bridge' ), + 'value' => '01', + ), + array( + 'label' => __( '2 AM', 'forms-bridge' ), + 'value' => '02', + ), + array( + 'label' => __( '3 AM', 'forms-bridge' ), + 'value' => '03', + ), + array( + 'label' => __( '4 AM', 'forms-bridge' ), + 'value' => '04', + ), + array( + 'label' => __( '5 AM', 'forms-bridge' ), + 'value' => '05', + ), + array( + 'label' => __( '6 AM', 'forms-bridge' ), + 'value' => '06', + ), + array( + 'label' => __( '7 AM', 'forms-bridge' ), + 'value' => '07', + ), + array( + 'label' => __( '8 AM', 'forms-bridge' ), + 'value' => '08', + ), + array( + 'label' => __( '9 AM', 'forms-bridge' ), + 'value' => '09', + ), + array( + 'label' => __( '10 AM', 'forms-bridge' ), + 'value' => '10', + ), + array( + 'label' => __( '11 AM', 'forms-bridge' ), + 'value' => '11', + ), + array( + 'label' => __( '12 AM', 'forms-bridge' ), + 'value' => '12', + ), + array( + 'label' => __( '1 PM', 'forms-bridge' ), + 'value' => '13', + ), + array( + 'label' => __( '2 PM', 'forms-bridge' ), + 'value' => '14', + ), + array( + 'label' => __( '3 PM', 'forms-bridge' ), + 'value' => '15', + ), + array( + 'label' => __( '4 PM', 'forms-bridge' ), + 'value' => '16', + ), + array( + 'label' => __( '5 PM', 'forms-bridge' ), + 'value' => '17', + ), + array( + 'label' => __( '6 PM', 'forms-bridge' ), + 'value' => '18', + ), + array( + 'label' => __( '7 PM', 'forms-bridge' ), + 'value' => '19', + ), + array( + 'label' => __( '8 PM', 'forms-bridge' ), + 'value' => '20', + ), + array( + 'label' => __( '9 PM', 'forms-bridge' ), + 'value' => '21', + ), + array( + 'label' => __( '10 PM', 'forms-bridge' ), + 'value' => '22', + ), + array( + 'label' => __( '11 PM', 'forms-bridge' ), + 'value' => '23', + ), + array( + 'label' => __( '12 PM', 'forms-bridge' ), + 'value' => '24', + ), + ), + ), + array( + 'name' => 'minute', + 'label' => __( 'Minute', 'forms-bridge' ), + 'type' => 'select', + 'required' => true, + 'options' => array( + array( + 'label' => '00', + 'value' => '00.0', + ), + array( + 'label' => '05', + 'value' => '05', + ), + array( + 'label' => '10', + 'value' => '10', + ), + array( + 'label' => '15', + 'value' => '15', + ), + array( + 'label' => '20', + 'value' => '20', + ), + array( + 'label' => '25', + 'value' => '25', + ), + array( + 'label' => '30', + 'value' => '30', + ), + array( + 'label' => '35', + 'value' => '35', + ), + array( + 'label' => '40', + 'value' => '40', + ), + array( + 'label' => '45', + 'value' => '45', + ), + array( + 'label' => '50', + 'value' => '50', + ), + array( + 'label' => '55', + 'value' => '55', + ), + ), + ), + array( + 'name' => 'meeting_description', + 'type' => 'textarea', + 'label' => __( 'Comments', 'forms-bridge' ), + ), + ), + ), +); diff --git a/forms-bridge/deps/http b/forms-bridge/deps/http index 7218bf42..a7e220bf 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 88c43d2f..e3984600 160000 --- a/forms-bridge/deps/plugin +++ b/forms-bridge/deps/plugin @@ -1 +1 @@ -Subproject commit 88c43d2f09f0cdf6d14eac99f14abf80832c20d1 +Subproject commit e3984600e84eb926d14b4398b22da2e273574cf7 diff --git a/forms-bridge/readme.txt b/forms-bridge/readme.txt index 70758d30..2125bb08 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 == diff --git a/tests/addons/test-suitecrm.php b/tests/addons/test-suitecrm.php new file mode 100644 index 00000000..dd841068 --- /dev/null +++ b/tests/addons/test-suitecrm.php @@ -0,0 +1,675 @@ + '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( '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( + '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 a6c0b4b1..c3507bcd 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() {