__( 'Moneybird', 'advanced-form-integration' ), 'tasks' => array( 'create_contact' => __( 'Create Contact', 'advanced-form-integration' ), 'create_sales_invoice' => __( 'Create Sales Invoice', 'advanced-form-integration' ), ), ); return $actions; } public function register_settings_tab( $providers ) { $providers['moneybird'] = __( 'Moneybird', 'advanced-form-integration' ); return $providers; } public function render_settings( $current_tab ) { if ( 'moneybird' !== $current_tab ) { return; } if ( ! class_exists( 'ADFOIN_Account_Manager' ) ) { require_once plugin_dir_path( __FILE__ ) . '../../includes/class-adfoin-account-manager.php'; } $fields = array( array( 'name' => 'access_token', 'label' => __( 'API Token', 'advanced-form-integration' ), 'type' => 'password', 'required' => true, 'mask' => true, 'show_in_table' => true, ), ); $instructions = '
    '; $instructions .= '
  1. ' . __( 'In Moneybird, open Settings → Connections and under External applications click New API token.', 'advanced-form-integration' ) . '
  2. '; $instructions .= '
  3. ' . __( 'Give it a name and tick the Sales invoices and Documents permissions, then copy the token.', 'advanced-form-integration' ) . '
  4. '; $instructions .= '
  5. ' . __( 'Click Add Account, enter a Title, paste the token into API Token and click Save Account.', 'advanced-form-integration' ) . '
  6. '; $instructions .= '
'; ADFOIN_Account_Manager::render_settings_view( 'moneybird', __( 'Moneybird', 'advanced-form-integration' ), $fields, $instructions ); } public function render_action_template() { ?> set_credentials_by_id( $cred_id ); } $fields = array(); if ( 'create_sales_invoice' === $task ) { $fields = $this->get_sales_invoice_fields(); } else { $fields = $this->get_contact_fields(); } wp_send_json_success( $fields ); } /** * AJAX: list administrations the connected account has access to. * * /administrations.json is the one Moneybird endpoint that lives * OUTSIDE the per-administration prefix, so it bypasses the regular * moneybird_request() helper. Cached per credential for 1 hour to * keep the action-editor responsive. */ public function ajax_get_administrations() { adfoin_verify_nonce(); $cred_id = isset( $_POST['credId'] ) ? sanitize_text_field( wp_unslash( $_POST['credId'] ) ) : ''; if ( ! $cred_id ) { wp_send_json_error( array( 'message' => __( 'Missing credential id.', 'advanced-form-integration' ) ) ); } $cache_key = 'adfoin_moneybird_admins_' . md5( $cred_id ); $cached = get_transient( $cache_key ); if ( is_array( $cached ) ) { wp_send_json_success( $cached ); } $this->set_credentials_by_id( $cred_id ); if ( ! $this->access_token ) { wp_send_json_error( array( 'message' => __( 'Moneybird API token is missing. Edit the account and add a token.', 'advanced-form-integration' ) ) ); } $response = wp_remote_get( self::API_BASE . '/administrations.json', array( 'timeout' => 30, 'headers' => array( 'Authorization' => 'Bearer ' . $this->access_token, 'Accept' => 'application/json', ), ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( array( 'message' => $response->get_error_message() ) ); } $status = (int) wp_remote_retrieve_response_code( $response ); if ( $status >= 400 ) { wp_send_json_error( array( 'message' => sprintf( 'HTTP %d', $status ) ) ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); $list = array(); if ( is_array( $body ) ) { foreach ( $body as $admin ) { if ( empty( $admin['id'] ) ) { continue; } $label = isset( $admin['name'] ) ? $admin['name'] : (string) $admin['id']; if ( ! empty( $admin['country'] ) ) { $label .= ' (' . $admin['country'] . ')'; } $list[ (string) $admin['id'] ] = $label; } } set_transient( $cache_key, $list, HOUR_IN_SECONDS ); wp_send_json_success( $list ); } protected function get_contact_fields() { // Whitelist matches Moneybird's documented Contact resource: // https://developer.moneybird.com/api/contacts // Removed: `mobile` (not a Moneybird field), `invoice_email` // (the doc'd field is `send_invoices_to_email`), `notes` (lives // on a sub-resource POST /contacts/{id}/notes, not on the // contact body). administration_id is now picked via the // dedicated dropdown in the template, not via an editable-field. return array( array( 'key' => 'contact_id', 'value' => __( 'Contact ID (update existing)', 'advanced-form-integration' ) ), array( 'key' => 'company_name', 'value' => __( 'Company Name', 'advanced-form-integration' ) ), array( 'key' => 'firstname', 'value' => __( 'First Name', 'advanced-form-integration' ) ), array( 'key' => 'lastname', 'value' => __( 'Last Name', 'advanced-form-integration' ) ), array( 'key' => 'attention', 'value' => __( 'Attention', 'advanced-form-integration' ) ), array( 'key' => 'customer_id', 'value' => __( 'Customer ID', 'advanced-form-integration' ), 'description' => __( 'Optional. If set, AFI looks up an existing contact by this ID before creating.', 'advanced-form-integration' ) ), array( 'key' => 'email', 'value' => __( 'Email', 'advanced-form-integration' ) ), array( 'key' => 'phone', 'value' => __( 'Phone', 'advanced-form-integration' ) ), array( 'key' => 'address1', 'value' => __( 'Address Line 1', 'advanced-form-integration' ) ), array( 'key' => 'address2', 'value' => __( 'Address Line 2', 'advanced-form-integration' ) ), array( 'key' => 'zipcode', 'value' => __( 'Postal Code', 'advanced-form-integration' ) ), array( 'key' => 'city', 'value' => __( 'City', 'advanced-form-integration' ) ), array( 'key' => 'country', 'value' => __( 'Country (ISO 2-letter)', 'advanced-form-integration' ) ), array( 'key' => 'tax_number', 'value' => __( 'VAT / Tax Number', 'advanced-form-integration' ) ), array( 'key' => 'chamber_of_commerce', 'value' => __( 'Chamber of Commerce', 'advanced-form-integration' ) ), array( 'key' => 'send_invoices_to_email', 'value' => __( 'Send Invoices To Email', 'advanced-form-integration' ) ), array( 'key' => 'send_invoices_to_attention', 'value' => __( 'Send Invoices To Attention', 'advanced-form-integration' ) ), ); } protected function get_sales_invoice_fields() { $contact_fields = $this->get_contact_fields(); $invoice_fields = array( array( 'key' => 'contact_id', 'value' => __( 'Contact ID (existing contact)', 'advanced-form-integration' ) ), array( 'key' => 'reference', 'value' => __( 'Reference', 'advanced-form-integration' ) ), array( 'key' => 'invoice_date', 'value' => __( 'Invoice Date (YYYY-MM-DD)', 'advanced-form-integration' ) ), array( 'key' => 'due_date', 'value' => __( 'Due Date (YYYY-MM-DD)', 'advanced-form-integration' ) ), array( 'key' => 'workflow_id', 'value' => __( 'Workflow ID', 'advanced-form-integration' ) ), array( 'key' => 'document_style_id', 'value' => __( 'Document Style ID', 'advanced-form-integration' ) ), array( 'key' => 'prices_are_incl_tax', 'value' => __( 'Prices Are Inclusive Tax (true/false)', 'advanced-form-integration' ) ), array( 'key' => 'payment_conditions', 'value' => __( 'Payment Conditions', 'advanced-form-integration' ), 'type' => 'textarea' ), array( 'key' => 'details_json', 'value' => __( 'Invoice Lines (JSON Array)', 'advanced-form-integration' ), 'type' => 'textarea', 'description' => __( 'Example: [{"description":"Service","price":100,"tax_rate_id":"..."}]', 'advanced-form-integration' ) ), array( 'key' => 'custom_fields_json', 'value' => __( 'Custom Fields (JSON)', 'advanced-form-integration' ), 'type' => 'textarea' ), array( 'key' => 'send_email', 'value' => __( 'Send Invoice Email (true/false)', 'advanced-form-integration' ) ), array( 'key' => 'email_message', 'value' => __( 'Invoice Email Message', 'advanced-form-integration' ), 'type' => 'textarea' ), ); // Remove duplicate contact_id entry from base contact fields when merging. $contact_fields = array_filter( $contact_fields, function ( $field ) { return 'contact_id' !== $field['key']; } ); return array_merge( $contact_fields, $invoice_fields ); } public function handle_job_queue( $data ) { $record = isset( $data['record'] ) ? $data['record'] : array(); $posted_data = isset( $data['posted_data'] ) ? $data['posted_data'] : array(); if ( empty( $record ) ) { return; } $record_data = json_decode( $record['data'], true ); $cred_id = isset( $record_data['field_data']['credId'] ) ? $record_data['field_data']['credId'] : ''; $this->set_credentials_by_id( $cred_id ); $task = isset( $record['task'] ) ? $record['task'] : ''; if ( 'create_sales_invoice' === $task ) { $this->process_sales_invoice( $record, $posted_data ); } else { $this->process_contact( $record, $posted_data ); } } protected function process_contact( $record, $posted_data ) { $record_data = json_decode( $record['data'], true ); if ( adfoin_check_conditional_logic( $record_data['action_data']['cl'] ?? array(), $posted_data ) ) { return; } $field_data = isset( $record_data['field_data'] ) ? $record_data['field_data'] : array(); $administration_id = isset( $field_data['administrationId'] ) ? trim( (string) $field_data['administrationId'] ) : ''; if ( '' === $administration_id ) { adfoin_add_to_log( new WP_Error( 'moneybird_missing_administration', __( 'Moneybird administration ID is required.', 'advanced-form-integration' ) ), '', array(), $record ); return; } $contact_payload = $this->collect_contact_payload( $field_data, $posted_data, true ); if ( is_wp_error( $contact_payload ) ) { adfoin_add_to_log( $contact_payload, '', array(), $record ); return; } $contact_id = $this->parse_field_value( $field_data, 'contact_id', $posted_data ); // No explicit Contact ID? Try Moneybird's customer_id lookup // endpoint so re-submissions update the same contact instead of // creating duplicates. This is the standard upsert path — // critical for WooCommerce flows where the same shopper might // hit checkout multiple times. if ( ! $contact_id && ! empty( $contact_payload['customer_id'] ) ) { $contact_id = $this->find_contact_id_by_customer_id( $administration_id, $contact_payload['customer_id'] ); } if ( $contact_id ) { $this->moneybird_request( $administration_id, 'contacts/' . rawurlencode( $contact_id ), 'PATCH', array( 'contact' => $contact_payload ), $record ); return; } $this->moneybird_request( $administration_id, 'contacts', 'POST', array( 'contact' => $contact_payload ), $record ); } /** * Look up an existing contact by the user-supplied `customer_id` * field via Moneybird's GET /contacts/customer_id/{customer_id} * endpoint. Returns the Moneybird ID, or '' if not found / errored. * * Reused by Pro to make the "WooCommerce order → Moneybird invoice" * flow idempotent: pass the WC user ID (or email) as customer_id * and the second submission updates instead of duplicating. */ public function find_contact_id_by_customer_id( $administration_id, $customer_id ) { if ( '' === (string) $administration_id || '' === (string) $customer_id ) { return ''; } // Empty $record on purpose — a 404 here ("not found") is the // expected signal that no existing contact matches, NOT an // error worth logging against the submission. $response = $this->moneybird_request( $administration_id, 'contacts/customer_id/' . rawurlencode( $customer_id ), 'GET', array(), array() ); if ( is_wp_error( $response ) ) { return ''; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); return isset( $body['id'] ) ? (string) $body['id'] : ''; } protected function process_sales_invoice( $record, $posted_data ) { $record_data = json_decode( $record['data'], true ); if ( adfoin_check_conditional_logic( $record_data['action_data']['cl'] ?? array(), $posted_data ) ) { return; } $field_data = isset( $record_data['field_data'] ) ? $record_data['field_data'] : array(); $administration_id = isset( $field_data['administrationId'] ) ? trim( (string) $field_data['administrationId'] ) : ''; if ( '' === $administration_id ) { adfoin_add_to_log( new WP_Error( 'moneybird_missing_administration', __( 'Moneybird administration ID is required.', 'advanced-form-integration' ) ), '', array(), $record ); return; } $contact_id = $this->parse_field_value( $field_data, 'contact_id', $posted_data ); if ( '' === $contact_id ) { $contact_payload = $this->collect_contact_payload( $field_data, $posted_data, false ); if ( is_wp_error( $contact_payload ) ) { adfoin_add_to_log( $contact_payload, '', array(), $record ); return; } if ( empty( $contact_payload ) ) { adfoin_add_to_log( new WP_Error( 'moneybird_missing_contact', __( 'Provide a contact ID or map contact fields to create a new contact.', 'advanced-form-integration' ) ), '', array(), $record ); return; } // Upsert by customer_id when supplied — keeps repeat // submissions (same shopper / form filler) on the same // Moneybird contact instead of duplicating. if ( ! empty( $contact_payload['customer_id'] ) ) { $contact_id = $this->find_contact_id_by_customer_id( $administration_id, $contact_payload['customer_id'] ); } if ( $contact_id ) { $this->moneybird_request( $administration_id, 'contacts/' . rawurlencode( $contact_id ), 'PATCH', array( 'contact' => $contact_payload ), $record ); } else { $response = $this->moneybird_request( $administration_id, 'contacts', 'POST', array( 'contact' => $contact_payload ), $record ); if ( is_wp_error( $response ) ) { return; } $contact_data = json_decode( wp_remote_retrieve_body( $response ), true ); $contact_id = $contact_data['id'] ?? ''; if ( '' === $contact_id ) { adfoin_add_to_log( new WP_Error( 'moneybird_contact_missing_id', __( 'Moneybird contact ID not returned.', 'advanced-form-integration' ) ), '', array(), $record ); return; } } } $invoice_payload = $this->collect_invoice_payload( $field_data, $posted_data, $contact_id ); if ( is_wp_error( $invoice_payload ) ) { adfoin_add_to_log( $invoice_payload, '', array(), $record ); return; } $response = $this->moneybird_request( $administration_id, 'sales_invoices', 'POST', array( 'sales_invoice' => $invoice_payload ), $record ); if ( is_wp_error( $response ) ) { return; } $send_invoice = $this->parse_field_value( $field_data, 'send_email', $posted_data ); if ( '' !== $send_invoice && $this->truthy( $send_invoice ) ) { $invoice_data = json_decode( wp_remote_retrieve_body( $response ), true ); $invoice_id = $invoice_data['id'] ?? ''; if ( $invoice_id ) { $email_payload = array(); $message = $this->parse_field_value( $field_data, 'email_message', $posted_data ); if ( '' !== $message ) { $email_payload['email'] = array( 'message' => $message ); } // Per Moneybird docs the email endpoint is `/sends_an_invoice`, // not `/send_invoice`. The old path returned 404. $this->moneybird_request( $administration_id, 'sales_invoices/' . rawurlencode( $invoice_id ) . '/sends_an_invoice', 'PATCH', $email_payload, $record ); } } } protected function collect_contact_payload( $field_data, $posted_data, $require_minimum ) { // Map of (form-field-key) → (Moneybird Contact API field name). // Field names verified against https://developer.moneybird.com/api/contacts. $map = array( 'company_name' => 'company_name', 'firstname' => 'firstname', 'lastname' => 'lastname', 'attention' => 'attention', 'customer_id' => 'customer_id', 'email' => 'email', 'phone' => 'phone', 'address1' => 'address1', 'address2' => 'address2', 'zipcode' => 'zipcode', 'city' => 'city', 'country' => 'country', 'tax_number' => 'tax_number', 'chamber_of_commerce' => 'chamber_of_commerce', 'send_invoices_to_email' => 'send_invoices_to_email', 'send_invoices_to_attention' => 'send_invoices_to_attention', ); $payload = array(); foreach ( $map as $key => $api_field ) { $value = $this->parse_field_value( $field_data, $key, $posted_data ); if ( '' === $value ) { continue; } $payload[ $api_field ] = $value; } if ( $require_minimum ) { $has_name = ! empty( $payload['company_name'] ) || ( ! empty( $payload['firstname'] ) && ! empty( $payload['lastname'] ) ); if ( ! $has_name ) { return new WP_Error( 'moneybird_contact_missing_name', __( 'Moneybird requires a company name or first and last name.', 'advanced-form-integration' ) ); } } return $payload; } protected function collect_invoice_payload( $field_data, $posted_data, $contact_id ) { $details_json = $this->parse_field_value( $field_data, 'details_json', $posted_data ); $details = array(); if ( '' !== $details_json ) { $decoded = json_decode( $details_json, true ); if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $decoded ) ) { return new WP_Error( 'moneybird_invalid_details_json', __( 'Invoice lines JSON is invalid.', 'advanced-form-integration' ) ); } $details = $decoded; } if ( empty( $details ) ) { return new WP_Error( 'moneybird_missing_invoice_lines', __( 'Provide at least one invoice line in the Invoice Lines (JSON) field.', 'advanced-form-integration' ) ); } $payload = array( 'contact_id' => $contact_id, 'details_attributes' => $details, ); $map = array( 'reference' => 'reference', 'invoice_date' => 'invoice_date', 'due_date' => 'due_date', 'workflow_id' => 'workflow_id', 'document_style_id' => 'document_style_id', 'payment_conditions' => 'payment_conditions', ); foreach ( $map as $key => $api_field ) { $value = $this->parse_field_value( $field_data, $key, $posted_data ); if ( '' === $value ) { continue; } $payload[ $api_field ] = $value; } $incl_tax = $this->parse_field_value( $field_data, 'prices_are_incl_tax', $posted_data ); if ( '' !== $incl_tax ) { $payload['prices_are_incl_tax'] = $this->truthy( $incl_tax ); } $custom_fields = $this->parse_field_value( $field_data, 'custom_fields_json', $posted_data ); if ( '' !== $custom_fields ) { $decoded = json_decode( $custom_fields, true ); if ( JSON_ERROR_NONE === json_last_error() && is_array( $decoded ) ) { $payload['custom_fields'] = $decoded; } } return $payload; } /** * Moneybird API request. Reused by the Pro add-on. * * @return array|WP_Error */ public function moneybird_request( $administration_id, $endpoint, $method = 'POST', $body = array(), $record = array() ) { if ( ! $this->access_token ) { $error = new WP_Error( 'moneybird_missing_token', __( 'Moneybird API token is missing. Edit the account and add a token.', 'advanced-form-integration' ) ); if ( $record ) { adfoin_add_to_log( $error, '', array(), $record ); } return $error; } $url = self::API_BASE . '/' . rawurlencode( $administration_id ) . '/' . ltrim( $endpoint, '/' ); $args = array( 'timeout' => 30, 'method' => strtoupper( $method ), 'headers' => array( 'Authorization' => 'Bearer ' . $this->access_token, 'Accept' => 'application/json', ), ); if ( in_array( $args['method'], array( 'POST', 'PATCH', 'PUT' ), true ) ) { $args['headers']['Content-Type'] = 'application/json'; $args['body'] = wp_json_encode( $body ); } $response = wp_remote_request( esc_url_raw( $url ), $args ); $status = wp_remote_retrieve_response_code( $response ); if ( $record ) { adfoin_add_to_log( $response, $url, $args, $record ); } if ( is_wp_error( $response ) ) { return $response; } if ( $status >= 400 ) { $body = wp_remote_retrieve_body( $response ); $error = $body ? $body : __( 'Moneybird request failed.', 'advanced-form-integration' ); return new WP_Error( 'moneybird_http_error', $error, array( 'status' => $status ) ); } return $response; } protected function parse_field_value( $field_data, $key, $posted_data ) { if ( ! isset( $field_data[ $key ] ) ) { return ''; } $value = adfoin_get_parsed_values( $field_data[ $key ], $posted_data ); if ( is_array( $value ) ) { return ''; } return is_string( $value ) ? trim( $value ) : ''; } protected function truthy( $value ) { $value = strtolower( trim( (string) $value ) ); return in_array( $value, array( '1', 'yes', 'true', 'on' ), true ); } /** * Account manager credential methods */ public function get_credentials() { adfoin_require_manage_options(); if ( ! wp_verify_nonce( isset( $_POST['_nonce'] ) ? $_POST['_nonce'] : '', 'advanced-form-integration' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed', 'advanced-form-integration' ) ) ); } wp_send_json_success( $this->safe_credentials_list() ); } /** * AJAX: save (or update) an API-token account from the settings modal. * The token is checked against Moneybird before it is stored. */ public function save_credentials() { adfoin_require_manage_options(); if ( ! wp_verify_nonce( isset( $_POST['_nonce'] ) ? $_POST['_nonce'] : '', 'advanced-form-integration' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed', 'advanced-form-integration' ) ) ); } $platform = 'moneybird'; $credentials = adfoin_read_credentials( $platform ); if ( ! is_array( $credentials ) ) { $credentials = array(); } if ( isset( $_POST['delete_index'] ) ) { $index = intval( wp_unslash( $_POST['delete_index'] ) ); if ( isset( $credentials[ $index ] ) ) { array_splice( $credentials, $index, 1 ); adfoin_save_credentials( $platform, $credentials ); wp_send_json_success( array( 'message' => 'Deleted' ) ); } wp_send_json_error( __( 'Invalid index', 'advanced-form-integration' ) ); } $id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : ''; $title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : ''; $api_token = isset( $_POST['access_token'] ) ? sanitize_text_field( wp_unslash( $_POST['access_token'] ) ) : ''; $existing = array(); foreach ( $credentials as $cred ) { if ( $id && isset( $cred['id'] ) && $cred['id'] === $id ) { $existing = $cred; break; } } if ( '' === $api_token ) { wp_send_json_error( array( 'message' => __( 'Enter a Moneybird API token.', 'advanced-form-integration' ) ) ); } $check = $this->verify_api_token( $api_token ); if ( is_wp_error( $check ) ) { wp_send_json_error( array( 'message' => $check->get_error_message() ) ); } if ( empty( $id ) ) { $id = wp_generate_uuid4(); } // The API token is used directly as the Bearer token; there is no refresh step. $new_data = array( 'id' => $id, 'title' => $title, 'access_token' => $api_token, ); if ( $existing ) { foreach ( $credentials as &$cred ) { if ( isset( $cred['id'] ) && $cred['id'] === $id ) { $cred = $new_data; break; } } unset( $cred ); } else { $credentials[] = $new_data; } adfoin_save_credentials( $platform, $credentials ); wp_send_json_success( array( 'message' => __( 'Connected', 'advanced-form-integration' ) ) ); } /** * Check a personal API token against GET /administrations before saving it. * * @return true|WP_Error */ protected function verify_api_token( $api_token ) { $response = wp_remote_get( self::API_BASE . '/administrations', array( 'timeout' => 30, 'headers' => array( 'Authorization' => 'Bearer ' . $api_token, 'Accept' => 'application/json', ), ) ); if ( is_wp_error( $response ) ) { return $response; } $status = wp_remote_retrieve_response_code( $response ); if ( 401 === $status || 403 === $status ) { return new WP_Error( 'moneybird_invalid_token', __( 'Moneybird rejected this API token. Check that it was copied in full and has not been revoked.', 'advanced-form-integration' ) ); } if ( $status >= 400 ) { /* translators: %d: HTTP status code */ return new WP_Error( 'moneybird_token_check_failed', sprintf( __( 'Moneybird returned HTTP %d while checking the API token.', 'advanced-form-integration' ), $status ) ); } return true; } /** * Accounts for the settings table and the account picker, without the token. */ protected function safe_credentials_list() { $credentials = adfoin_read_credentials( 'moneybird' ); $list = array(); foreach ( (array) $credentials as $cred ) { if ( ! is_array( $cred ) ) { continue; } $connected = ! empty( $cred['access_token'] ); unset( $cred['access_token'] ); $cred['connected'] = $connected; $cred['connection_failed'] = false; $list[] = $cred; } return $list; } /** * Load the API token for a saved account. */ public function set_credentials_by_id( $cred_id ) { $this->cred_id = $cred_id; $this->access_token = ''; foreach ( (array) adfoin_read_credentials( 'moneybird' ) as $cred ) { if ( isset( $cred['id'] ) && $cred['id'] === $cred_id ) { $this->access_token = isset( $cred['access_token'] ) ? $cred['access_token'] : ''; break; } } } } ADFOIN_Moneybird::get_instance(); function adfoin_moneybird_send_data( $record, $posted_data ) { ADFOIN_Moneybird::get_instance()->handle_job_queue( array( 'record' => $record, 'posted_data' => $posted_data, ) ); }