Plugin Directory

Changeset 3454574


Ignore:
Timestamp:
02/05/2026 11:42:21 AM (2 weeks ago)
Author:
shift8
Message:

Contact, business partner match logic, new tests

Location:
shift8-integration-for-gravity-forms-and-sap-business-one/trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • shift8-integration-for-gravity-forms-and-sap-business-one/trunk/cli-test-submission.php

    r3454222 r3454574  
    13221322
    13231323WP_CLI::add_command('shift8-gravitysap-masterdata', 'Shift8_GravitySAP_MasterData_Command');
     1324
     1325/**
     1326 * Test Business Partner lookup for duplicate detection
     1327 *
     1328 * This command tests the performance and accuracy of looking up existing
     1329 * Business Partners in SAP B1 based on name, country, and postal code.
     1330 */
     1331class Shift8_GravitySAP_BP_Lookup_Command {
     1332   
     1333    /**
     1334     * Search for existing Business Partners matching criteria
     1335     *
     1336     * ## OPTIONS
     1337     *
     1338     * --name=<name>
     1339     * : Business Partner name to search for (case-insensitive)
     1340     *
     1341     * --country=<country>
     1342     * : 2-letter country code (e.g., US, CA, GB)
     1343     *
     1344     * --postal=<postal>
     1345     * : Postal/ZIP code
     1346     *
     1347     * [--verbose]
     1348     * : Show detailed timing and query information
     1349     *
     1350     * ## EXAMPLES
     1351     *
     1352     *     wp shift8-gravitysap-bp-lookup search --name="Test Company" --country=US --postal=12345
     1353     *     wp shift8-gravitysap-bp-lookup search --name="Acme Corp" --country=CA --postal="M5V 1A1" --verbose
     1354     *
     1355     * @param array $args
     1356     * @param array $assoc_args
     1357     */
     1358    public function search($args, $assoc_args) {
     1359        $name = isset($assoc_args['name']) ? sanitize_text_field($assoc_args['name']) : '';
     1360        $country = isset($assoc_args['country']) ? strtoupper(sanitize_text_field($assoc_args['country'])) : '';
     1361        $postal = isset($assoc_args['postal']) ? sanitize_text_field($assoc_args['postal']) : '';
     1362        $verbose = isset($assoc_args['verbose']);
     1363       
     1364        if (empty($name)) {
     1365            WP_CLI::error('Please specify a Business Partner name: --name="Company Name"');
     1366            return;
     1367        }
     1368       
     1369        if (empty($country)) {
     1370            WP_CLI::error('Please specify a country code: --country=US');
     1371            return;
     1372        }
     1373       
     1374        if (empty($postal)) {
     1375            WP_CLI::error('Please specify a postal code: --postal=12345');
     1376            return;
     1377        }
     1378       
     1379        WP_CLI::line('');
     1380        WP_CLI::line('=== Business Partner Lookup Test ===');
     1381        WP_CLI::line('');
     1382        WP_CLI::line('Search Criteria:');
     1383        WP_CLI::line("  Name:    {$name} (case-insensitive)");
     1384        WP_CLI::line("  Country: {$country}");
     1385        WP_CLI::line("  Postal:  {$postal}");
     1386        WP_CLI::line('');
     1387       
     1388        try {
     1389            // Get SAP settings
     1390            $sap_settings = get_option('shift8_gravitysap_settings', array());
     1391           
     1392            if (empty($sap_settings['sap_endpoint']) || empty($sap_settings['sap_username']) || empty($sap_settings['sap_password'])) {
     1393                WP_CLI::error('SAP connection settings not configured.');
     1394                return;
     1395            }
     1396           
     1397            // Decrypt password
     1398            $sap_settings['sap_password'] = shift8_gravitysap_decrypt_password($sap_settings['sap_password']);
     1399           
     1400            // Create SAP service
     1401            require_once plugin_dir_path(__FILE__) . 'includes/class-shift8-gravitysap-sap-service.php';
     1402            $sap_service = new Shift8_GravitySAP_SAP_Service($sap_settings);
     1403           
     1404            // Authenticate
     1405            $reflection = new ReflectionClass($sap_service);
     1406            $auth_method = $reflection->getMethod('ensure_authenticated');
     1407            $auth_method->setAccessible(true);
     1408           
     1409            $auth_start = microtime(true);
     1410            if (!$auth_method->invoke($sap_service)) {
     1411                WP_CLI::error('Failed to authenticate with SAP B1');
     1412                return;
     1413            }
     1414            $auth_time = round((microtime(true) - $auth_start) * 1000, 2);
     1415           
     1416            if ($verbose) {
     1417                WP_CLI::line("Authentication time: {$auth_time}ms");
     1418            }
     1419           
     1420            // Perform the lookup using centralized method
     1421            $lookup_start = microtime(true);
     1422            $result = $sap_service->find_existing_business_partner($name, $country, $postal);
     1423            $lookup_time = round((microtime(true) - $lookup_start) * 1000, 2);
     1424           
     1425            WP_CLI::line('');
     1426            WP_CLI::line(str_repeat('-', 60));
     1427            WP_CLI::line('');
     1428           
     1429            if ($result['found']) {
     1430                WP_CLI::success("Found matching Business Partner!");
     1431                WP_CLI::line('');
     1432                WP_CLI::line("  CardCode: {$result['card_code']}");
     1433                WP_CLI::line("  CardName: {$result['card_name']}");
     1434                if (!empty($result['address_country'])) {
     1435                    WP_CLI::line("  Country:  {$result['address_country']}");
     1436                }
     1437                if (!empty($result['address_postal'])) {
     1438                    WP_CLI::line("  Postal:   {$result['address_postal']}");
     1439                }
     1440            } else {
     1441                WP_CLI::warning("No matching Business Partner found.");
     1442                WP_CLI::line("  A new Business Partner would be created for this submission.");
     1443            }
     1444           
     1445            WP_CLI::line('');
     1446            WP_CLI::line('Performance Metrics:');
     1447            WP_CLI::line("  Authentication: {$auth_time}ms");
     1448            WP_CLI::line("  Lookup Query:   {$lookup_time}ms");
     1449            WP_CLI::line("  Total Time:     " . round($auth_time + $lookup_time, 2) . "ms");
     1450           
     1451            if (isset($result['records_scanned'])) {
     1452                WP_CLI::line("  Records Scanned: {$result['records_scanned']}");
     1453            }
     1454           
     1455            WP_CLI::line('');
     1456           
     1457            // Performance recommendation
     1458            $total_time = $auth_time + $lookup_time;
     1459            if ($total_time > 3000) {
     1460                WP_CLI::warning("Lookup time exceeds 3 seconds. Consider moving SAP processing to a background cron job.");
     1461            } elseif ($total_time > 1000) {
     1462                WP_CLI::line("Note: Lookup time is moderate. For high-traffic forms, consider async processing.");
     1463            } else {
     1464                WP_CLI::success("Lookup time is acceptable for synchronous processing.");
     1465            }
     1466           
     1467            WP_CLI::line('');
     1468           
     1469        } catch (Exception $e) {
     1470            WP_CLI::error('Error: ' . $e->getMessage());
     1471        }
     1472    }
     1473   
     1474    /**
     1475     * Find an existing Business Partner matching the criteria
     1476     *
     1477     * SAP B1 Service Layer has limited OData support - it doesn't support:
     1478     * - tolower() for case-insensitive comparisons
     1479     * - any() for filtering on collections
     1480     *
     1481     * Strategy: Query BPs with CardName containing the search term, then filter
     1482     * client-side for exact match (case-insensitive) and address criteria.
     1483     *
     1484     * @param ReflectionMethod $request_method The SAP make_request method
     1485     * @param object $sap_service The SAP service instance
     1486     * @param string $name Business Partner name (case-insensitive)
     1487     * @param string $country 2-letter country code
     1488    /**
     1489     * Benchmark the lookup performance with multiple queries
     1490     *
     1491     * ## OPTIONS
     1492     *
     1493     * [--iterations=<num>]
     1494     * : Number of test iterations (default: 5)
     1495     *
     1496     * ## EXAMPLES
     1497     *
     1498     *     wp shift8-gravitysap-bp-lookup benchmark
     1499     *     wp shift8-gravitysap-bp-lookup benchmark --iterations=10
     1500     *
     1501     * @param array $args
     1502     * @param array $assoc_args
     1503     */
     1504    public function benchmark($args, $assoc_args) {
     1505        $iterations = isset($assoc_args['iterations']) ? absint($assoc_args['iterations']) : 5;
     1506       
     1507        if ($iterations < 1 || $iterations > 20) {
     1508            WP_CLI::error('Iterations must be between 1 and 20');
     1509            return;
     1510        }
     1511       
     1512        WP_CLI::line('');
     1513        WP_CLI::line('=== Business Partner Lookup Benchmark ===');
     1514        WP_CLI::line('');
     1515        WP_CLI::line("Running {$iterations} iterations with sample queries...");
     1516        WP_CLI::line('');
     1517       
     1518        try {
     1519            // Get SAP settings
     1520            $sap_settings = get_option('shift8_gravitysap_settings', array());
     1521           
     1522            if (empty($sap_settings['sap_endpoint'])) {
     1523                WP_CLI::error('SAP connection settings not configured.');
     1524                return;
     1525            }
     1526           
     1527            // Decrypt password
     1528            $sap_settings['sap_password'] = shift8_gravitysap_decrypt_password($sap_settings['sap_password']);
     1529           
     1530            // Create SAP service
     1531            require_once plugin_dir_path(__FILE__) . 'includes/class-shift8-gravitysap-sap-service.php';
     1532            $sap_service = new Shift8_GravitySAP_SAP_Service($sap_settings);
     1533           
     1534            // Authenticate once
     1535            $reflection = new ReflectionClass($sap_service);
     1536            $auth_method = $reflection->getMethod('ensure_authenticated');
     1537            $auth_method->setAccessible(true);
     1538           
     1539            if (!$auth_method->invoke($sap_service)) {
     1540                WP_CLI::error('Failed to authenticate with SAP B1');
     1541                return;
     1542            }
     1543           
     1544            $request_method = $reflection->getMethod('make_request');
     1545            $request_method->setAccessible(true);
     1546           
     1547            // Run benchmark queries (simple count query, no filtering)
     1548            $times = array();
     1549           
     1550            for ($i = 1; $i <= $iterations; $i++) {
     1551                $start = microtime(true);
     1552               
     1553                // Simple query to measure baseline latency
     1554                $response = $request_method->invoke(
     1555                    $sap_service,
     1556                    'GET',
     1557                    '/BusinessPartners?$top=1&$select=CardCode,CardName'
     1558                );
     1559               
     1560                $elapsed = round((microtime(true) - $start) * 1000, 2);
     1561                $times[] = $elapsed;
     1562               
     1563                $status = is_wp_error($response) ? 'ERROR' : 'OK';
     1564                WP_CLI::line("  Iteration {$i}: {$elapsed}ms ({$status})");
     1565            }
     1566           
     1567            // Calculate statistics
     1568            $avg = round(array_sum($times) / count($times), 2);
     1569            $min = round(min($times), 2);
     1570            $max = round(max($times), 2);
     1571           
     1572            WP_CLI::line('');
     1573            WP_CLI::line(str_repeat('-', 40));
     1574            WP_CLI::line('');
     1575            WP_CLI::line('Results:');
     1576            WP_CLI::line("  Average: {$avg}ms");
     1577            WP_CLI::line("  Min:     {$min}ms");
     1578            WP_CLI::line("  Max:     {$max}ms");
     1579            WP_CLI::line('');
     1580           
     1581            // Recommendations
     1582            if ($avg > 2000) {
     1583                WP_CLI::warning("Average response time > 2s. Strongly recommend async processing via cron.");
     1584            } elseif ($avg > 500) {
     1585                WP_CLI::line("Average response time is moderate. Consider async processing for better UX.");
     1586            } else {
     1587                WP_CLI::success("Response times are good for synchronous processing.");
     1588            }
     1589           
     1590            WP_CLI::line('');
     1591           
     1592        } catch (Exception $e) {
     1593            WP_CLI::error('Error: ' . $e->getMessage());
     1594        }
     1595    }
     1596}
     1597
     1598WP_CLI::add_command('shift8-gravitysap-bp-lookup', 'Shift8_GravitySAP_BP_Lookup_Command');
  • shift8-integration-for-gravity-forms-and-sap-business-one/trunk/includes/class-shift8-gravitysap-sap-service.php

    r3375274 r3454574  
    247247            throw new Exception('SAP Business Partner creation failed: ' . esc_html($error_message));
    248248        }
     249    }
     250
     251    /**
     252     * Find an existing Contact Person on a Business Partner by name and/or email
     253     *
     254     * Searches the Business Partner's ContactEmployees for a match using case-insensitive
     255     * comparison on Name AND Email (both must match if both are provided).
     256     *
     257     * @since 1.4.4
     258     * @param string $card_code     The Business Partner CardCode
     259     * @param string $contact_name  The contact person's full name to match
     260     * @param string $contact_email The contact person's email to match (optional but recommended)
     261     * @return array|null The matching contact data with InternalCode, or null if not found
     262     */
     263    public function find_existing_contact($card_code, $contact_name, $contact_email = '') {
     264        shift8_gravitysap_debug_log('=== SEARCHING FOR EXISTING CONTACT PERSON ===', array(
     265            'CardCode' => $card_code,
     266            'SearchName' => $contact_name,
     267            'SearchEmail' => $contact_email
     268        ));
     269
     270        if (empty($card_code) || empty($contact_name)) {
     271            shift8_gravitysap_debug_log('Find Contact: Missing CardCode or contact name');
     272            return null;
     273        }
     274
     275        // Fetch the Business Partner with its contacts
     276        $bp_data = $this->get_business_partner($card_code);
     277       
     278        if (!$bp_data || empty($bp_data['ContactEmployees'])) {
     279            shift8_gravitysap_debug_log('Find Contact: No existing contacts found on BP', array(
     280                'CardCode' => $card_code
     281            ));
     282            return null;
     283        }
     284
     285        // Normalize search values for case-insensitive comparison
     286        $search_name = strtolower(trim($contact_name));
     287        $search_email = !empty($contact_email) ? strtolower(trim($contact_email)) : '';
     288
     289        shift8_gravitysap_debug_log('Find Contact: Searching through contacts', array(
     290            'ContactCount' => count($bp_data['ContactEmployees']),
     291            'NormalizedName' => $search_name,
     292            'NormalizedEmail' => $search_email
     293        ));
     294
     295        foreach ($bp_data['ContactEmployees'] as $contact) {
     296            $existing_name = isset($contact['Name']) ? strtolower(trim($contact['Name'])) : '';
     297            $existing_email = isset($contact['E_Mail']) ? strtolower(trim($contact['E_Mail'])) : '';
     298
     299            // Match logic: Name must match, and if email is provided, email must also match
     300            $name_matches = ($existing_name === $search_name);
     301            $email_matches = empty($search_email) || ($existing_email === $search_email);
     302
     303            if ($name_matches && $email_matches) {
     304                shift8_gravitysap_debug_log('✅ Found matching contact', array(
     305                    'MatchedName' => $contact['Name'] ?? 'N/A',
     306                    'MatchedEmail' => $contact['E_Mail'] ?? 'N/A',
     307                    'InternalCode' => $contact['InternalCode'] ?? 'N/A'
     308                ));
     309                return $contact;
     310            }
     311        }
     312
     313        shift8_gravitysap_debug_log('Find Contact: No matching contact found', array(
     314            'CardCode' => $card_code,
     315            'SearchedName' => $contact_name,
     316            'SearchedEmail' => $contact_email
     317        ));
     318
     319        return null;
     320    }
     321
     322    /**
     323     * Add a Contact Person to an existing Business Partner
     324     *
     325     * Uses PATCH to update the Business Partner with a new ContactEmployees entry.
     326     * SAP B1 will append the new contact to existing contacts.
     327     *
     328     * @since 1.4.2
     329     * @param string $card_code    The Business Partner CardCode
     330     * @param array  $contact_data Contact person data (FirstName, LastName, Phone1, E_Mail, Address)
     331     * @return array|null The contact person data with InternalCode if successful, null on failure
     332     */
     333    public function add_contact_to_business_partner($card_code, $contact_data) {
     334        shift8_gravitysap_debug_log('=== ADDING CONTACT TO EXISTING BUSINESS PARTNER ===', array(
     335            'CardCode' => $card_code,
     336            'ContactData' => $contact_data
     337        ));
     338
     339        // Validate inputs
     340        if (empty($card_code)) {
     341            shift8_gravitysap_debug_log('Add Contact: Missing CardCode');
     342            return null;
     343        }
     344
     345        if (empty($contact_data) || !is_array($contact_data)) {
     346            shift8_gravitysap_debug_log('Add Contact: No contact data provided');
     347            return null;
     348        }
     349
     350        // Ensure authenticated
     351        if (!$this->ensure_authenticated()) {
     352            shift8_gravitysap_debug_log('Add Contact: Authentication failed');
     353            return null;
     354        }
     355
     356        // Build the contact person object
     357        $contact_person = array();
     358
     359        // Check if Name is already provided in contact_data (from map_form_data_to_sap_fields)
     360        $first_name = isset($contact_data['FirstName']) ? trim($contact_data['FirstName']) : '';
     361        $last_name = isset($contact_data['LastName']) ? trim($contact_data['LastName']) : '';
     362        $existing_name = isset($contact_data['Name']) ? trim($contact_data['Name']) : '';
     363       
     364        if (!empty($first_name) && !empty($last_name)) {
     365            $contact_person['Name'] = $first_name . ' ' . $last_name;
     366            $contact_person['FirstName'] = $first_name;
     367            $contact_person['LastName'] = $last_name;
     368        } elseif (!empty($first_name)) {
     369            $contact_person['Name'] = $first_name;
     370            $contact_person['FirstName'] = $first_name;
     371        } elseif (!empty($last_name)) {
     372            $contact_person['Name'] = $last_name;
     373            $contact_person['LastName'] = $last_name;
     374        } elseif (!empty($existing_name)) {
     375            // Use the pre-constructed Name if FirstName/LastName not available
     376            $contact_person['Name'] = $existing_name;
     377        } else {
     378            // No name provided - use a default or skip
     379            shift8_gravitysap_debug_log('Add Contact: No name provided, skipping contact creation');
     380            return null;
     381        }
     382
     383        // Add optional contact fields
     384        if (!empty($contact_data['Phone1'])) {
     385            $contact_person['Phone1'] = $contact_data['Phone1'];
     386        }
     387        if (!empty($contact_data['E_Mail'])) {
     388            $contact_person['E_Mail'] = $contact_data['E_Mail'];
     389        }
     390        if (!empty($contact_data['Address'])) {
     391            $contact_person['Address'] = $contact_data['Address'];
     392        }
     393
     394        // Build PATCH payload - SAP B1 appends to existing ContactEmployees
     395        $patch_data = array(
     396            'ContactEmployees' => array($contact_person)
     397        );
     398
     399        shift8_gravitysap_debug_log('Add Contact: Sending PATCH request', array(
     400            'CardCode' => $card_code,
     401            'PatchData' => $patch_data
     402        ));
     403
     404        // Send PATCH request to update the Business Partner
     405        $endpoint = "/BusinessPartners('" . rawurlencode($card_code) . "')";
     406        $response = $this->make_request('PATCH', $endpoint, $patch_data);
     407
     408        if (is_wp_error($response)) {
     409            shift8_gravitysap_debug_log('Add Contact: PATCH request failed', array(
     410                'error' => $response->get_error_message()
     411            ));
     412            return null;
     413        }
     414
     415        $response_code = wp_remote_retrieve_response_code($response);
     416
     417        // 204 No Content is success for PATCH
     418        if ($response_code === 204) {
     419            shift8_gravitysap_debug_log('✅ Contact Person added successfully to ' . $card_code);
     420           
     421            // Return the contact person data (SAP doesn't return the created contact in PATCH response)
     422            // We need to fetch the BP to get the InternalCode of the newly added contact
     423            $contact_person['CardCode'] = $card_code;
     424           
     425            // Try to get the newly created contact's InternalCode by fetching the BP
     426            $bp_response = $this->get_business_partner($card_code);
     427            if ($bp_response && isset($bp_response['ContactEmployees'])) {
     428                // Find the contact we just added (last one in the array, or match by name)
     429                $contacts = $bp_response['ContactEmployees'];
     430                foreach (array_reverse($contacts) as $contact) {
     431                    if (isset($contact['Name']) && $contact['Name'] === $contact_person['Name']) {
     432                        $contact_person['InternalCode'] = $contact['InternalCode'] ?? null;
     433                        break;
     434                    }
     435                }
     436            }
     437           
     438            return $contact_person;
     439        } else {
     440            // Handle error response
     441            $body = wp_remote_retrieve_body($response);
     442            $error_data = json_decode($body, true);
     443           
     444            $error_message = 'Unknown error';
     445            if (!empty($error_data['error']['message']['value'])) {
     446                $error_message = $error_data['error']['message']['value'];
     447            }
     448           
     449            shift8_gravitysap_debug_log('Add Contact: PATCH failed', array(
     450                'status_code' => $response_code,
     451                'error' => $error_message
     452            ));
     453           
     454            return null;
     455        }
     456    }
     457
     458    /**
     459     * Get a Business Partner by CardCode
     460     *
     461     * @since 1.4.2
     462     * @param string $card_code The Business Partner CardCode
     463     * @return array|null The Business Partner data or null on failure
     464     */
     465    public function get_business_partner($card_code) {
     466        if (empty($card_code)) {
     467            return null;
     468        }
     469
     470        if (!$this->ensure_authenticated()) {
     471            return null;
     472        }
     473
     474        $endpoint = "/BusinessPartners('" . rawurlencode($card_code) . "')";
     475        $response = $this->make_request('GET', $endpoint);
     476
     477        if (is_wp_error($response)) {
     478            return null;
     479        }
     480
     481        $response_code = wp_remote_retrieve_response_code($response);
     482        if ($response_code === 200) {
     483            $body = wp_remote_retrieve_body($response);
     484            return json_decode($body, true);
     485        }
     486
     487        return null;
    249488    }
    250489
     
    10611300        return false;
    10621301    }
     1302
     1303    /**
     1304     * Find an existing Business Partner matching name, country, and postal code
     1305     *
     1306     * This method searches SAP B1 for an existing Business Partner that matches:
     1307     * - CardName (case-insensitive)
     1308     * - Country (from BPAddresses)
     1309     * - ZipCode/Postal (from BPAddresses)
     1310     *
     1311     * @since 1.3.9
     1312     * @param string $name Business Partner name (case-insensitive comparison)
     1313     * @param string $country 2-letter country code
     1314     * @param string $postal Postal/ZIP code
     1315     * @return array Result with 'found', 'card_code', 'card_name', etc.
     1316     */
     1317    public function find_existing_business_partner($name, $country, $postal) {
     1318        $result = array(
     1319            'found' => false,
     1320            'card_code' => null,
     1321            'card_name' => null,
     1322            'address_country' => null,
     1323            'address_postal' => null,
     1324            'records_scanned' => 0,
     1325            'error' => null
     1326        );
     1327
     1328        // Validate inputs
     1329        if (empty($name) || empty($country) || empty($postal)) {
     1330            $result['error'] = 'Name, country, and postal code are all required for lookup';
     1331            shift8_gravitysap_debug_log('BP Lookup: Missing required parameters', array(
     1332                'name' => !empty($name),
     1333                'country' => !empty($country),
     1334                'postal' => !empty($postal)
     1335            ));
     1336            return $result;
     1337        }
     1338
     1339        // Ensure authenticated
     1340        if (!$this->ensure_authenticated()) {
     1341            $result['error'] = 'Failed to authenticate with SAP';
     1342            return $result;
     1343        }
     1344
     1345        shift8_gravitysap_debug_log('=== STARTING BUSINESS PARTNER LOOKUP ===', array(
     1346            'name' => $name,
     1347            'country' => $country,
     1348            'postal' => $postal
     1349        ));
     1350
     1351        // Normalize search name for case-insensitive comparison
     1352        $search_name_normalized = strtolower(trim($name));
     1353
     1354        // Escape single quotes for OData query
     1355        $escaped_name = str_replace("'", "''", $name);
     1356
     1357        // Select fields including BPAddresses
     1358        $select = 'CardCode,CardName,BPAddresses';
     1359
     1360        // Strategy 1: Try exact CardName match first (most efficient)
     1361        $filter = "CardName eq '{$escaped_name}'";
     1362        $query = "/BusinessPartners?\$filter=" . rawurlencode($filter) . "&\$select={$select}";
     1363
     1364        shift8_gravitysap_debug_log('BP Lookup Strategy 1: Exact match', array('filter' => $filter));
     1365
     1366        $response = $this->make_request('GET', $query);
     1367        $exact_matches = $this->parse_bp_lookup_response($response);
     1368
     1369        foreach ($exact_matches as $bp) {
     1370            if ($this->bp_has_matching_address($bp, $country, $postal)) {
     1371                $result['found'] = true;
     1372                $result['card_code'] = $bp['CardCode'];
     1373                $result['card_name'] = $bp['CardName'];
     1374                $result['address_country'] = $country;
     1375                $result['address_postal'] = $postal;
     1376                $result['records_scanned'] = count($exact_matches);
     1377
     1378                shift8_gravitysap_debug_log('BP Lookup: MATCH FOUND (exact)', array(
     1379                    'card_code' => $bp['CardCode'],
     1380                    'card_name' => $bp['CardName']
     1381                ));
     1382                return $result;
     1383            }
     1384        }
     1385
     1386        // Strategy 2: Broader search with 'startswith' for case variations
     1387        $first_word = explode(' ', trim($name))[0];
     1388        $escaped_first_word = str_replace("'", "''", $first_word);
     1389
     1390        if (strlen($first_word) >= 3) {
     1391            $filter = "startswith(CardName, '{$escaped_first_word}')";
     1392            $query = "/BusinessPartners?\$filter=" . rawurlencode($filter) . "&\$select={$select}&\$top=100";
     1393
     1394            shift8_gravitysap_debug_log('BP Lookup Strategy 2: Startswith', array('filter' => $filter));
     1395
     1396            $response = $this->make_request('GET', $query);
     1397            $startswith_matches = $this->parse_bp_lookup_response($response);
     1398
     1399            $result['records_scanned'] += count($startswith_matches);
     1400
     1401            foreach ($startswith_matches as $bp) {
     1402                $bp_name_normalized = strtolower(trim($bp['CardName'] ?? ''));
     1403
     1404                // Case-insensitive name match AND address match
     1405                if ($bp_name_normalized === $search_name_normalized &&
     1406                    $this->bp_has_matching_address($bp, $country, $postal)) {
     1407                   
     1408                    $result['found'] = true;
     1409                    $result['card_code'] = $bp['CardCode'];
     1410                    $result['card_name'] = $bp['CardName'];
     1411                    $result['address_country'] = $country;
     1412                    $result['address_postal'] = $postal;
     1413
     1414                    shift8_gravitysap_debug_log('BP Lookup: MATCH FOUND (case-insensitive)', array(
     1415                        'card_code' => $bp['CardCode'],
     1416                        'card_name' => $bp['CardName']
     1417                    ));
     1418                    return $result;
     1419                }
     1420            }
     1421        }
     1422
     1423        shift8_gravitysap_debug_log('BP Lookup: No match found', array(
     1424            'records_scanned' => $result['records_scanned']
     1425        ));
     1426
     1427        return $result;
     1428    }
     1429
     1430    /**
     1431     * Parse Business Partner lookup response
     1432     *
     1433     * @since 1.3.9
     1434     * @param mixed $response HTTP response
     1435     * @return array Array of Business Partners
     1436     */
     1437    private function parse_bp_lookup_response($response) {
     1438        if (is_wp_error($response)) {
     1439            shift8_gravitysap_debug_log('BP Lookup query failed', array(
     1440                'error' => $response->get_error_message()
     1441            ));
     1442            return array();
     1443        }
     1444
     1445        $code = wp_remote_retrieve_response_code($response);
     1446        if ($code !== 200) {
     1447            $body = wp_remote_retrieve_body($response);
     1448            $error_data = json_decode($body, true);
     1449            shift8_gravitysap_debug_log('BP Lookup query error', array(
     1450                'http_code' => $code,
     1451                'error' => $error_data['error']['message']['value'] ?? 'Unknown error'
     1452            ));
     1453            return array();
     1454        }
     1455
     1456        $body = wp_remote_retrieve_body($response);
     1457        $data = json_decode($body, true);
     1458
     1459        if (!isset($data['value'])) {
     1460            return array();
     1461        }
     1462
     1463        shift8_gravitysap_debug_log('BP Lookup query result', array(
     1464            'count' => count($data['value'])
     1465        ));
     1466
     1467        return $data['value'];
     1468    }
     1469
     1470    /**
     1471     * Check if Business Partner has matching address
     1472     *
     1473     * @since 1.3.9
     1474     * @param array $bp Business Partner data
     1475     * @param string $country Country code
     1476     * @param string $postal Postal code
     1477     * @return bool True if address matches
     1478     */
     1479    private function bp_has_matching_address($bp, $country, $postal) {
     1480        if (!isset($bp['BPAddresses']) || !is_array($bp['BPAddresses'])) {
     1481            return false;
     1482        }
     1483
     1484        foreach ($bp['BPAddresses'] as $addr) {
     1485            $addr_country = $addr['Country'] ?? '';
     1486            $addr_postal = $addr['ZipCode'] ?? '';
     1487
     1488            if ($addr_country === $country && $addr_postal === $postal) {
     1489                return true;
     1490            }
     1491        }
     1492
     1493        return false;
     1494    }
    10631495}
  • shift8-integration-for-gravity-forms-and-sap-business-one/trunk/readme.txt

    r3454441 r3454574  
    55* Requires at least: 5.0
    66* Tested up to: 6.8
    7 * Stable tag: 1.3.8
     7* Stable tag: 1.4.4
    88* Requires PHP: 7.4
    99* License: GPLv3
     
    9696This is ideal for sample request forms, multi-product orders, and service selection forms.
    9797
     98= How do I prevent duplicate Business Partners? =
     99
     100Enable **Check for existing Business Partner** in your form's SAP Integration settings. The plugin will search SAP B1 for existing Business Partners matching:
     101
     102* Business Partner Name (case-insensitive)
     103* Country (from address)
     104* Postal/ZIP Code (from address)
     105
     106If a match is found, the plugin uses the existing Business Partner instead of creating a duplicate. You can test this with WP-CLI:
     107
     108`wp shift8-gravitysap-bp-lookup search --name="Test Company" --country="CA" --postal="M5V 1A1"`
     109
     110= How are Contact Persons linked to Sales Quotations? =
     111
     112When a Business Partner match is found (or a new one is created), the plugin automatically:
     113
     1141. Adds a Contact Person to the Business Partner using the form's contact data
     1152. Retrieves the Contact Person's InternalCode from SAP
     1163. Links the Contact Person to the Sales Quotation via SAP's ContactPersonCode field
     117
     118**Result in SAP B1:**
     119* Contact Person appears in the Business Partner's "Contact Persons" tab
     120* Sales Quotation dropdown shows the correct Contact Person selected
     121* Contact Person is properly linked for subsequent documents
     122
     123**Technical Note:** SAP B1's ContactPersonCode field requires the numeric InternalCode, not the text Name. The plugin handles this automatically.
     124
    98125== Screenshots ==
    99126
     
    104131
    105132== Changelog ==
     133
     134= 1.4.4 =
     135* **NEW**: Duplicate contact detection - checks if contact already exists before adding
     136* **NEW**: `find_existing_contact()` method for case-insensitive name + email matching
     137* **IMPROVED**: Reuses existing contacts instead of creating duplicates on repeat submissions
     138* **TESTING**: Added 10 new tests for contact duplicate detection (edge cases included)
     139* **TESTING**: Now 138 tests with 306 assertions - All passing
     140
     141= 1.4.3 =
     142* **FIX**: Contact Person now correctly linked to Sales Quotation using SAP's InternalCode (numeric) instead of Name (string)
     143* **IMPROVED**: After adding Contact Person to existing BP, plugin now fetches BP to retrieve the contact's InternalCode
     144* **IMPROVED**: Sales Quotation ContactPersonCode field now uses correct integer value for proper SAP B1 linking
     145* **TESTING**: Added 3 additional Contact Person tests (InternalCode retrieval, existing Name field, first name only)
     146* **TESTING**: Now 128 tests with 291 assertions - All passing
     147* **DOCUMENTATION**: Added comprehensive contactPersonLinking section to .cursorrules
     148
     149= 1.4.2 =
     150* **NEW**: Contact Person now added to existing Business Partner when match is found
     151* **NEW**: Contact Person linked to Sales Quotation via ContactPersonCode field
     152* **IMPROVED**: SAP Service class now includes `add_contact_to_business_partner()` and `get_business_partner()` methods
     153* **TESTING**: Added 6 new unit tests for Contact Person functionality
     154* **TESTING**: Now 125 tests with 283 assertions - All passing
     155
     156= 1.4.1 =
     157* **TESTING**: Added comprehensive test coverage for async processing (9 new tests)
     158* **TESTING**: Added comprehensive test coverage for Business Partner lookup (10 new tests)
     159* **TESTING**: Now 119 tests with 272 assertions - All passing
     160* **DOCUMENTATION**: Updated .cursorrules with async processing, BP lookup, and testing patterns
     161
     162= 1.4.0 =
     163* **NEW**: Async processing for form submissions - SAP integration now runs in a non-blocking background request
     164* **NEW**: "Check for Existing Business Partner" now integrated into form submission flow
     165* **NEW**: Test Integration button now supports existing BP lookup when enabled
     166* **IMPROVED**: Form submissions no longer block on SAP API response time (1-2 seconds faster)
     167* **IMPROVED**: Centralized Business Partner lookup logic for code reuse across WP-CLI, form processing, and test integration
     168
     169= 1.3.9 =
     170* **NEW**: Added "Check for Existing Business Partner" toggle setting
     171* **NEW**: Added WP-CLI command `wp shift8-gravitysap-bp-lookup search` to test duplicate detection
     172* **NEW**: Added WP-CLI command `wp shift8-gravitysap-bp-lookup benchmark` to measure SAP query performance
     173* **ENHANCEMENT**: Duplicate detection matches on Business Partner Name (case-insensitive), Country, and Postal Code
    106174
    107175= 1.3.8 =
  • shift8-integration-for-gravity-forms-and-sap-business-one/trunk/shift8-gravitysap.php

    r3454441 r3454574  
    44 * Plugin URI: https://github.com/stardothosting/shift8-gravitysap
    55 * Description: Integrates Gravity Forms with SAP Business One, automatically creating Business Partners from form submissions.
    6  * Version: 1.3.8
     6 * Version: 1.4.4
    77 * Author: Shift8 Web
    88 * Author URI: https://shift8web.ca
     
    2828
    2929// Plugin constants
    30 define('SHIFT8_GRAVITYSAP_VERSION', '1.3.8');
     30define('SHIFT8_GRAVITYSAP_VERSION', '1.4.4');
    3131define('SHIFT8_GRAVITYSAP_PLUGIN_FILE', __FILE__);
    3232define('SHIFT8_GRAVITYSAP_PLUGIN_DIR', plugin_dir_path(__FILE__));
     
    230230        add_action('wp_ajax_load_itemcodes', array($this, 'ajax_load_itemcodes'));
    231231        add_action('admin_footer', array($this, 'add_retry_button_script'));
     232       
     233        // Async SAP processing endpoint (accessible without login for loopback requests)
     234        add_action('wp_ajax_shift8_gravitysap_async_process', array($this, 'ajax_async_sap_process'));
     235        add_action('wp_ajax_nopriv_shift8_gravitysap_async_process', array($this, 'ajax_async_sap_process'));
    232236       
    233237       
     
    525529                        <p class="description">
    526530                            <?php esc_html_e('When enabled, a Sales Quotation will be created and linked to the newly created Business Partner. Configure quotation field mapping below.', 'shift8-gravity-forms-sap-b1-integration'); ?>
     531                        </p>
     532                    </td>
     533                </tr>
     534               
     535                <tr>
     536                    <th scope="row">
     537                        <label for="sap_check_existing_bp"><?php esc_html_e('Check for Existing Business Partner', 'shift8-gravity-forms-sap-b1-integration'); ?></label>
     538                    </th>
     539                    <td>
     540                        <input type="checkbox" id="sap_check_existing_bp" name="sap_check_existing_bp" value="1" <?php checked(rgar($settings, 'check_existing_bp'), '1'); ?> />
     541                        <label for="sap_check_existing_bp"><?php esc_html_e('Check if a Business Partner already exists before creating a new one', 'shift8-gravity-forms-sap-b1-integration'); ?></label>
     542                        <p class="description">
     543                            <?php esc_html_e('When enabled, the system will search SAP for an existing Business Partner matching:', 'shift8-gravity-forms-sap-b1-integration'); ?>
     544                            <br>
     545                            <strong>&bull;</strong> <?php esc_html_e('Business Partner Name (case-insensitive)', 'shift8-gravity-forms-sap-b1-integration'); ?>
     546                            <br>
     547                            <strong>&bull;</strong> <?php esc_html_e('Country', 'shift8-gravity-forms-sap-b1-integration'); ?>
     548                            <br>
     549                            <strong>&bull;</strong> <?php esc_html_e('Postal/ZIP Code', 'shift8-gravity-forms-sap-b1-integration'); ?>
     550                            <br><br>
     551                            <?php esc_html_e('If a match is found, a Sales Quotation will be added to the existing Business Partner instead of creating a new one.', 'shift8-gravity-forms-sap-b1-integration'); ?>
    527552                        </p>
    528553                    </td>
     
    14961521            'card_code_prefix' => sanitize_text_field(rgpost('sap_card_code_prefix')),
    14971522            'create_quotation' => rgpost('sap_create_quotation') === '1' ? '1' : '0',
     1523            'check_existing_bp' => rgpost('sap_check_existing_bp') === '1' ? '1' : '0',
    14981524            'field_mapping' => array(),
    14991525            'quotation_field_mapping' => array(),
     
    16001626        $settings = rgar($form, 'sap_integration_settings');
    16011627       
    1602         // Initialize status tracking
    1603         if (!empty($entry['id'])) {
    1604             $this->update_entry_sap_status($entry['id'], 'processing', '', '');
    1605         }
    1606        
    16071628        if (empty($settings['enabled']) || $settings['enabled'] !== '1') {
    16081629            if (!empty($entry['id'])) {
     
    16111632            return;
    16121633        }
     1634       
     1635        // Get plugin settings (SAP connection details)
     1636        $plugin_settings = get_option('shift8_gravitysap_settings', array());
     1637       
     1638        // Check if connection settings are configured
     1639        if (empty($plugin_settings['sap_endpoint']) || empty($plugin_settings['sap_username']) || empty($plugin_settings['sap_password'])) {
     1640            $this->update_entry_sap_status($entry['id'], 'failed', '', 'SAP connection settings incomplete');
     1641            shift8_gravitysap_debug_log('SAP connection settings INCOMPLETE');
     1642            return;
     1643        }
     1644       
     1645        // Set initial status to pending (will be processed async)
     1646        $this->update_entry_sap_status($entry['id'], 'pending', '', '');
     1647       
     1648        // Generate a secure token for async processing
     1649        $async_token = wp_generate_password(32, false);
     1650        gform_update_meta($entry['id'], 'sap_async_token', wp_hash($async_token));
     1651       
     1652        // Fire non-blocking loopback request to process SAP integration asynchronously
     1653        $this->fire_async_sap_process($entry['id'], $form['id'], $async_token);
     1654       
     1655        shift8_gravitysap_debug_log('🚀 SAP async processing initiated', array(
     1656            'entry_id' => $entry['id'],
     1657            'form_id' => $form['id']
     1658        ));
     1659    }
     1660   
     1661    /**
     1662     * Fire non-blocking loopback request for async SAP processing
     1663     *
     1664     * @since 1.4.0
     1665     * @param int    $entry_id    Entry ID
     1666     * @param int    $form_id     Form ID
     1667     * @param string $async_token Security token for verification
     1668     */
     1669    private function fire_async_sap_process($entry_id, $form_id, $async_token) {
     1670        $ajax_url = admin_url('admin-ajax.php');
     1671       
     1672        $args = array(
     1673            'timeout'   => 0.01, // Essentially non-blocking
     1674            'blocking'  => false, // Don't wait for response
     1675            'sslverify' => apply_filters('https_local_ssl_verify', false),
     1676            'body'      => array(
     1677                'action'      => 'shift8_gravitysap_async_process',
     1678                'entry_id'    => $entry_id,
     1679                'form_id'     => $form_id,
     1680                'async_token' => $async_token,
     1681            ),
     1682        );
     1683       
     1684        wp_remote_post($ajax_url, $args);
     1685    }
     1686   
     1687    /**
     1688     * AJAX handler for async SAP processing
     1689     *
     1690     * Processes the SAP integration asynchronously via loopback request.
     1691     * This is called by fire_async_sap_process() and runs in a separate PHP process.
     1692     *
     1693     * @since 1.4.0
     1694     */
     1695    public function ajax_async_sap_process() {
     1696        // Verify request parameters
     1697        $entry_id = isset($_POST['entry_id']) ? absint(wp_unslash($_POST['entry_id'])) : 0;
     1698        $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
     1699        $async_token = isset($_POST['async_token']) ? sanitize_text_field(wp_unslash($_POST['async_token'])) : '';
     1700       
     1701        if (empty($entry_id) || empty($form_id) || empty($async_token)) {
     1702            shift8_gravitysap_debug_log('❌ Async SAP process: Missing required parameters');
     1703            wp_die();
     1704        }
     1705       
     1706        // Verify the async token
     1707        $stored_token_hash = gform_get_meta($entry_id, 'sap_async_token');
     1708        if (empty($stored_token_hash) || !hash_equals($stored_token_hash, wp_hash($async_token))) {
     1709            shift8_gravitysap_debug_log('❌ Async SAP process: Invalid token', array(
     1710                'entry_id' => $entry_id
     1711            ));
     1712            wp_die();
     1713        }
     1714       
     1715        // Clear the token (single use)
     1716        gform_delete_meta($entry_id, 'sap_async_token');
     1717       
     1718        shift8_gravitysap_debug_log('=== ASYNC SAP PROCESSING STARTED ===', array(
     1719            'entry_id' => $entry_id,
     1720            'form_id' => $form_id
     1721        ));
     1722       
     1723        // Get entry and form data
     1724        $entry = GFAPI::get_entry($entry_id);
     1725        $form = GFAPI::get_form($form_id);
     1726       
     1727        if (is_wp_error($entry) || !$form) {
     1728            shift8_gravitysap_debug_log('❌ Async SAP process: Failed to load entry or form', array(
     1729                'entry_id' => $entry_id,
     1730                'form_id' => $form_id
     1731            ));
     1732            wp_die();
     1733        }
     1734       
     1735        // Process the SAP integration synchronously (we're now in async context)
     1736        $this->process_sap_integration_sync($entry, $form);
     1737       
     1738        wp_die();
     1739    }
     1740   
     1741    /**
     1742     * Synchronous SAP integration processing
     1743     *
     1744     * This is the actual SAP processing logic, called either by async handler
     1745     * or directly for retry/testing purposes.
     1746     *
     1747     * @since 1.4.0
     1748     * @param array $entry Entry data
     1749     * @param array $form  Form data
     1750     */
     1751    public function process_sap_integration_sync($entry, $form) {
     1752        $settings = rgar($form, 'sap_integration_settings');
    16131753       
    16141754        try {
     
    16181758                throw new Exception('SAP connection settings are incomplete - check plugin settings');
    16191759            }
     1760           
     1761            // Set status to processing
     1762            $this->update_entry_sap_status($entry['id'], 'processing', '', '');
    16201763           
    16211764            // STEP 2: Validate required fields BEFORE attempting SAP connection
     
    16531796            $sap_service = new Shift8_GravitySAP_SAP_Service($plugin_settings);
    16541797           
    1655             // STEP 6: Create Business Partner in SAP
    1656             $result = $sap_service->create_business_partner($business_partner_data);
    1657            
    1658             // STEP 7: Handle success
    1659             if ($result && isset($result['CardCode'])) {
    1660                 $this->update_entry_sap_status($entry['id'], 'success', $result['CardCode'], '');
    1661                
    1662                 // Add success note to entry
     1798            // STEP 6: Check for existing Business Partner if enabled
     1799            $existing_card_code = null;
     1800            if (rgar($settings, 'check_existing_bp') === '1') {
     1801                $existing_card_code = $this->check_for_existing_business_partner($sap_service, $business_partner_data);
     1802            }
     1803           
     1804            // STEP 7: Use existing CardCode or create new Business Partner
     1805            // Initialize contact person info for use in Sales Quotation
     1806            // SAP B1 uses InternalCode (numeric) for ContactPersonCode on documents
     1807            $contact_person_code = null;
     1808            $contact_person_name = null;
     1809           
     1810            if ($existing_card_code) {
     1811                // Use existing Business Partner
     1812                $card_code = $existing_card_code;
     1813               
     1814                // Handle Contact Person for existing Business Partner
     1815                if (!empty($business_partner_data['ContactEmployees']) && is_array($business_partner_data['ContactEmployees'])) {
     1816                    $contact_data = $business_partner_data['ContactEmployees'][0];
     1817                   
     1818                    // Build contact name for matching
     1819                    $search_name = '';
     1820                    if (!empty($contact_data['Name'])) {
     1821                        $search_name = $contact_data['Name'];
     1822                    } elseif (!empty($contact_data['FirstName']) || !empty($contact_data['LastName'])) {
     1823                        $search_name = trim(($contact_data['FirstName'] ?? '') . ' ' . ($contact_data['LastName'] ?? ''));
     1824                    }
     1825                    $search_email = $contact_data['E_Mail'] ?? '';
     1826                   
     1827                    shift8_gravitysap_debug_log('Checking for existing Contact Person', array(
     1828                        'CardCode' => $card_code,
     1829                        'SearchName' => $search_name,
     1830                        'SearchEmail' => $search_email
     1831                    ));
     1832                   
     1833                    // First, check if contact already exists (case-insensitive name + email match)
     1834                    $existing_contact = null;
     1835                    if (!empty($search_name)) {
     1836                        $existing_contact = $sap_service->find_existing_contact($card_code, $search_name, $search_email);
     1837                    }
     1838                   
     1839                    if ($existing_contact) {
     1840                        // Use existing contact
     1841                        $contact_person_name = $existing_contact['Name'] ?? null;
     1842                        $contact_person_code = $existing_contact['InternalCode'] ?? null;
     1843                       
     1844                        GFFormsModel::add_note(
     1845                            $entry['id'],
     1846                            0,
     1847                            'Shift8 SAP Integration',
     1848                            sprintf(
     1849                                /* translators: 1: Contact name, 2: CardCode, 3: InternalCode */
     1850                                esc_html__('👤 Found existing Contact Person "%1$s" (Code: %3$s) on Business Partner %2$s', 'shift8-gravity-forms-sap-b1-integration'),
     1851                                esc_html($contact_person_name ?? 'Unknown'),
     1852                                esc_html($card_code),
     1853                                esc_html($contact_person_code ?? 'N/A')
     1854                            )
     1855                        );
     1856                       
     1857                        shift8_gravitysap_debug_log('✅ Using existing Contact Person', array(
     1858                            'ContactName' => $contact_person_name,
     1859                            'ContactPersonCode' => $contact_person_code,
     1860                            'CardCode' => $card_code
     1861                        ));
     1862                    } else {
     1863                        // No existing contact found - add new one
     1864                        shift8_gravitysap_debug_log('No existing contact found, adding new Contact Person', array(
     1865                            'CardCode' => $card_code,
     1866                            'ContactData' => $contact_data
     1867                        ));
     1868                       
     1869                        $contact_result = $sap_service->add_contact_to_business_partner($card_code, $contact_data);
     1870                       
     1871                        if ($contact_result) {
     1872                            $contact_person_name = $contact_result['Name'] ?? null;
     1873                            // SAP B1 uses InternalCode (numeric) for ContactPersonCode on documents
     1874                            $contact_person_code = $contact_result['InternalCode'] ?? null;
     1875                           
     1876                            GFFormsModel::add_note(
     1877                                $entry['id'],
     1878                                0,
     1879                                'Shift8 SAP Integration',
     1880                                sprintf(
     1881                                    /* translators: 1: Contact name, 2: CardCode, 3: InternalCode */
     1882                                    esc_html__('👤 Contact Person "%1$s" (Code: %3$s) added to existing Business Partner %2$s', 'shift8-gravity-forms-sap-b1-integration'),
     1883                                    esc_html($contact_person_name ?? 'Unknown'),
     1884                                    esc_html($card_code),
     1885                                    esc_html($contact_person_code ?? 'N/A')
     1886                                )
     1887                            );
     1888                           
     1889                            shift8_gravitysap_debug_log('✅ Contact Person added successfully', array(
     1890                                'ContactName' => $contact_person_name,
     1891                                'ContactPersonCode' => $contact_person_code,
     1892                                'CardCode' => $card_code,
     1893                                'InternalCode' => $contact_result['InternalCode'] ?? 'N/A'
     1894                            ));
     1895                        } else {
     1896                            // Log warning but don't fail - contact is optional
     1897                            shift8_gravitysap_debug_log('⚠️ Could not add Contact Person to existing BP (non-fatal)', array(
     1898                                'CardCode' => $card_code,
     1899                                'ContactData' => $contact_data
     1900                            ));
     1901                           
     1902                            GFFormsModel::add_note(
     1903                                $entry['id'],
     1904                                0,
     1905                                'Shift8 SAP Integration',
     1906                                sprintf(
     1907                                    /* translators: %s: CardCode */
     1908                                    esc_html__('⚠️ Could not add Contact Person to existing Business Partner %s (Sales Quotation will proceed without contact)', 'shift8-gravity-forms-sap-b1-integration'),
     1909                                    esc_html($card_code)
     1910                                )
     1911                            );
     1912                        }
     1913                    }
     1914                }
     1915               
     1916                $this->update_entry_sap_status($entry['id'], 'success', $card_code, '');
     1917               
     1918                GFFormsModel::add_note(
     1919                    $entry['id'],
     1920                    0,
     1921                    'Shift8 SAP Integration',
     1922                    sprintf(
     1923                        /* translators: %s: SAP CardCode */
     1924                        esc_html__('🔗 MATCHED: Found existing Business Partner in SAP with CardCode: %s', 'shift8-gravity-forms-sap-b1-integration'),
     1925                        esc_html($card_code)
     1926                    )
     1927                );
     1928               
     1929                shift8_gravitysap_debug_log('🔗 MATCHED EXISTING BUSINESS PARTNER', array(
     1930                    'entry_id' => $entry['id'],
     1931                    'CardCode' => $card_code
     1932                ));
     1933            } else {
     1934                // Create new Business Partner in SAP
     1935                $result = $sap_service->create_business_partner($business_partner_data);
     1936               
     1937                if (!$result || !isset($result['CardCode'])) {
     1938                    throw new Exception('SAP returned success but no CardCode - check SAP logs');
     1939                }
     1940               
     1941                $card_code = $result['CardCode'];
     1942               
     1943                // Extract contact person info from the created BP
     1944                // The contact person is included in the BP creation - check if SAP returned the InternalCode
     1945                if (!empty($result['ContactEmployees']) && is_array($result['ContactEmployees'])) {
     1946                    // SAP returned the ContactEmployees with InternalCodes
     1947                    $contact_person_name = $result['ContactEmployees'][0]['Name'] ?? null;
     1948                    $contact_person_code = $result['ContactEmployees'][0]['InternalCode'] ?? null;
     1949                } elseif (!empty($business_partner_data['ContactEmployees']) && is_array($business_partner_data['ContactEmployees'])) {
     1950                    // Fallback: get name from input data, but we need to fetch BP to get InternalCode
     1951                    $contact_person_name = $business_partner_data['ContactEmployees'][0]['Name'] ?? null;
     1952                   
     1953                    // Fetch the created BP to get the contact's InternalCode
     1954                    $created_bp = $sap_service->get_business_partner($card_code);
     1955                    if ($created_bp && !empty($created_bp['ContactEmployees'])) {
     1956                        foreach ($created_bp['ContactEmployees'] as $contact) {
     1957                            if (isset($contact['Name']) && $contact['Name'] === $contact_person_name) {
     1958                                $contact_person_code = $contact['InternalCode'] ?? null;
     1959                                break;
     1960                            }
     1961                        }
     1962                    }
     1963                }
     1964               
     1965                $this->update_entry_sap_status($entry['id'], 'success', $card_code, '');
     1966               
    16631967                GFFormsModel::add_note(
    16641968                    $entry['id'],
     
    16681972                        /* translators: %s: SAP Business Partner Card Code */
    16691973                        esc_html__('✅ SUCCESS: Business Partner created in SAP B1. Card Code: %s', 'shift8-gravity-forms-sap-b1-integration'),
    1670                         esc_html($result['CardCode'])
     1974                        esc_html($card_code)
    16711975                    )
    16721976                );
    16731977               
    16741978                shift8_gravitysap_debug_log('✅ SAP CONFIRMATION: Business Partner created successfully', array(
    1675                     'CardCode' => $result['CardCode'],
     1979                    'CardCode' => $card_code,
    16761980                    'CardName' => $result['CardName'] ?? 'N/A',
    16771981                    'CardType' => $result['CardType'] ?? 'N/A',
    16781982                    'Series' => $result['Series'] ?? 'N/A',
    16791983                    'EntryID' => $entry['id'],
    1680                     'FormID' => $form['id']
     1984                    'FormID' => $form['id'],
     1985                    'ContactPersonName' => $contact_person_name ?? 'N/A',
     1986                    'ContactPersonCode' => $contact_person_code ?? 'N/A'
    16811987                ));
    1682                
    1683                 // STEP 8: Create Sales Quotation if enabled
    1684                 if (!empty($settings['create_quotation']) && $settings['create_quotation'] === '1') {
    1685                     try {
    1686                         $quotation_result = $this->create_sales_quotation_from_entry($entry, $form, $settings, $result['CardCode'], $sap_service);
     1988            }
     1989           
     1990            // STEP 8: Create Sales Quotation if enabled
     1991            if (!empty($settings['create_quotation']) && $settings['create_quotation'] === '1') {
     1992                try {
     1993                    // Pass the InternalCode for ContactPersonCode (SAP expects numeric code, not name)
     1994                    $quotation_result = $this->create_sales_quotation_from_entry($entry, $form, $settings, $card_code, $sap_service, $contact_person_code);
     1995                   
     1996                    if ($quotation_result && isset($quotation_result['DocEntry'])) {
     1997                        // Update entry with quotation info
     1998                        gform_update_meta($entry['id'], 'sap_b1_quotation_docentry', $quotation_result['DocEntry']);
     1999                        gform_update_meta($entry['id'], 'sap_b1_quotation_docnum', $quotation_result['DocNum'] ?? '');
    16872000                       
    1688                         if ($quotation_result && isset($quotation_result['DocEntry'])) {
    1689                             // Update entry with quotation info
    1690                             gform_update_meta($entry['id'], 'sap_b1_quotation_docentry', $quotation_result['DocEntry']);
    1691                             gform_update_meta($entry['id'], 'sap_b1_quotation_docnum', $quotation_result['DocNum'] ?? '');
    1692                            
    1693                             // Add quotation note
    1694                             GFFormsModel::add_note(
    1695                                 $entry['id'],
    1696                                 0,
    1697                                 'Shift8 SAP Integration',
    1698                                 sprintf(
    1699                                     /* translators: 1: DocNum, 2: DocEntry */
    1700                                     esc_html__('✅ SUCCESS: Sales Quotation created in SAP B1. Doc #%1$s (Entry: %2$s)', 'shift8-gravity-forms-sap-b1-integration'),
    1701                                     esc_html($quotation_result['DocNum'] ?? 'N/A'),
    1702                                     esc_html($quotation_result['DocEntry'])
    1703                                 )
    1704                             );
    1705                            
    1706                             shift8_gravitysap_debug_log('✅ SAP QUOTATION: Sales Quotation created successfully', array(
    1707                                 'DocEntry' => $quotation_result['DocEntry'],
    1708                                 'DocNum' => $quotation_result['DocNum'] ?? 'N/A',
    1709                                 'CardCode' => $result['CardCode'],
    1710                                 'EntryID' => $entry['id']
    1711                             ));
    1712                         }
    1713                     } catch (Exception $quotation_error) {
    1714                         // Log quotation error but don't fail the whole submission
    1715                         shift8_gravitysap_debug_log('⚠️ SAP QUOTATION FAILED (BP was created successfully)', array(
    1716                             'error' => $quotation_error->getMessage(),
    1717                             'CardCode' => $result['CardCode'],
    1718                             'EntryID' => $entry['id']
    1719                         ));
    1720                        
     2001                        // Add quotation note
    17212002                        GFFormsModel::add_note(
    17222003                            $entry['id'],
     
    17242005                            'Shift8 SAP Integration',
    17252006                            sprintf(
    1726                                 /* translators: %s: Error message */
    1727                                 esc_html__('⚠️ WARNING: Sales Quotation creation failed: %s (Business Partner was created successfully)', 'shift8-gravity-forms-sap-b1-integration'),
    1728                                 esc_html($quotation_error->getMessage())
     2007                                /* translators: 1: DocNum, 2: DocEntry */
     2008                                esc_html__('✅ SUCCESS: Sales Quotation created in SAP B1. Doc #%1$s (Entry: %2$s)', 'shift8-gravity-forms-sap-b1-integration'),
     2009                                esc_html($quotation_result['DocNum'] ?? 'N/A'),
     2010                                esc_html($quotation_result['DocEntry'])
    17292011                            )
    17302012                        );
     2013                       
     2014                        shift8_gravitysap_debug_log('✅ SAP QUOTATION: Sales Quotation created successfully', array(
     2015                            'DocEntry' => $quotation_result['DocEntry'],
     2016                            'DocNum' => $quotation_result['DocNum'] ?? 'N/A',
     2017                            'CardCode' => $card_code,
     2018                            'EntryID' => $entry['id']
     2019                        ));
    17312020                    }
     2021                } catch (Exception $quotation_error) {
     2022                    // Log quotation error but don't fail the whole submission
     2023                    shift8_gravitysap_debug_log('⚠️ SAP QUOTATION FAILED (BP was created/matched successfully)', array(
     2024                        'error' => $quotation_error->getMessage(),
     2025                        'CardCode' => $card_code,
     2026                        'EntryID' => $entry['id']
     2027                    ));
     2028                   
     2029                    GFFormsModel::add_note(
     2030                        $entry['id'],
     2031                        0,
     2032                        'Shift8 SAP Integration',
     2033                        sprintf(
     2034                            /* translators: %s: Error message */
     2035                            esc_html__('⚠️ WARNING: Sales Quotation creation failed: %s (Business Partner was created/matched successfully)', 'shift8-gravity-forms-sap-b1-integration'),
     2036                            esc_html($quotation_error->getMessage())
     2037                        )
     2038                    );
    17322039                }
    1733             } else {
    1734                 throw new Exception('SAP returned success but no CardCode - check SAP logs');
    17352040            }
    17362041           
     
    17582063            ));
    17592064        }
     2065    }
     2066   
     2067    /**
     2068     * Check for existing Business Partner in SAP
     2069     *
     2070     * Uses the centralized lookup function in SAP Service to find matching BPs.
     2071     *
     2072     * @since 1.4.0
     2073     * @param Shift8_GravitySAP_SAP_Service $sap_service           SAP Service instance
     2074     * @param array                         $business_partner_data BP data with lookup fields
     2075     * @return string|null CardCode if found, null otherwise
     2076     */
     2077    private function check_for_existing_business_partner($sap_service, $business_partner_data) {
     2078        // Extract lookup fields from business partner data
     2079        $name = rgar($business_partner_data, 'CardName', '');
     2080        $country = '';
     2081        $postal = '';
     2082       
     2083        // Get country and postal from BPAddresses if present
     2084        $bp_addresses = rgar($business_partner_data, 'BPAddresses', array());
     2085        if (!empty($bp_addresses) && isset($bp_addresses[0])) {
     2086            $country = rgar($bp_addresses[0], 'Country', '');
     2087            $postal = rgar($bp_addresses[0], 'ZipCode', '');
     2088        }
     2089       
     2090        // If no address data in BPAddresses, check root level (for test data)
     2091        if (empty($country)) {
     2092            $country = rgar($business_partner_data, 'Country', '');
     2093        }
     2094        if (empty($postal)) {
     2095            $postal = rgar($business_partner_data, 'ZipCode', '');
     2096        }
     2097       
     2098        // Need at least name and country for lookup
     2099        if (empty($name) || empty($country)) {
     2100            shift8_gravitysap_debug_log('⚠️ Cannot check for existing BP: Missing required fields', array(
     2101                'name' => $name,
     2102                'country' => $country,
     2103                'postal' => $postal
     2104            ));
     2105            return null;
     2106        }
     2107       
     2108        shift8_gravitysap_debug_log('🔍 Checking for existing Business Partner', array(
     2109            'name' => $name,
     2110            'country' => $country,
     2111            'postal' => $postal
     2112        ));
     2113       
     2114        // Use centralized lookup function from SAP Service
     2115        $result = $sap_service->find_existing_business_partner($name, $country, $postal);
     2116       
     2117        if ($result['found'] && !empty($result['card_code'])) {
     2118            shift8_gravitysap_debug_log('✅ Found existing Business Partner', array(
     2119                'card_code' => $result['card_code'],
     2120                'card_name' => $result['card_name'],
     2121                'match_type' => $result['match_type']
     2122            ));
     2123            return $result['card_code'];
     2124        }
     2125       
     2126        shift8_gravitysap_debug_log('ℹ️ No existing Business Partner found');
     2127        return null;
    17602128    }
    17612129
     
    20402408     *
    20412409     * @since 1.2.2
    2042      * @param array  $entry       Entry data
    2043      * @param array  $form        Form data
    2044      * @param array  $settings    Form settings
    2045      * @param string $card_code   Business Partner CardCode
    2046      * @param object $sap_service SAP Service instance
     2410     * @param array    $entry               Entry data
     2411     * @param array    $form                Form data
     2412     * @param array    $settings            Form settings
     2413     * @param string   $card_code           Business Partner CardCode
     2414     * @param object   $sap_service         SAP Service instance
     2415     * @param int|null $contact_person_code Contact Person InternalCode to link (optional)
    20472416     * @return array Created quotation data
    20482417     * @throws Exception If creation fails
    20492418     */
    2050     private function create_sales_quotation_from_entry($entry, $form, $settings, $card_code, $sap_service) {
     2419    private function create_sales_quotation_from_entry($entry, $form, $settings, $card_code, $sap_service, $contact_person_code = null) {
    20512420        shift8_gravitysap_debug_log('=== STARTING SALES QUOTATION CREATION ===');
    20522421       
     
    20572426            'quotation_field_mapping' => $quotation_mapping,
    20582427            'itemcode_mapping' => $itemcode_mapping,
    2059             'entry_id' => $entry['id']
     2428            'entry_id' => $entry['id'],
     2429            'contact_person_code' => $contact_person_code
    20602430        ));
    20612431       
     
    20692439            'DocumentLines' => array()
    20702440        );
     2441       
     2442        // Link Contact Person to the quotation if provided
     2443        // SAP B1 uses ContactPersonCode which is the InternalCode (numeric) of the contact
     2444        if (!empty($contact_person_code)) {
     2445            $quotation_data['ContactPersonCode'] = intval($contact_person_code);
     2446           
     2447            shift8_gravitysap_debug_log('Linking Contact Person to Sales Quotation', array(
     2448                'ContactPersonCode' => $contact_person_code,
     2449                'CardCode' => $card_code
     2450            ));
     2451        }
    20712452       
    20722453        // Add Comments if mapped
     
    28813262            $sap_service = new Shift8_GravitySAP_SAP_Service($plugin_settings);
    28823263           
    2883             // Create Business Partner in SAP
     3264            // Check for existing Business Partner if enabled
     3265            if (rgar($settings, 'check_existing_bp') === '1') {
     3266                $existing_card_code = $this->check_for_existing_business_partner($sap_service, $business_partner_data);
     3267               
     3268                if ($existing_card_code) {
     3269                    return array(
     3270                        'success' => true,
     3271                        'message' => '🔗 MATCHED: Found existing Business Partner with Card Code: ' . esc_html($existing_card_code)
     3272                    );
     3273                }
     3274            }
     3275           
     3276            // Create Business Partner in SAP (no match found or check disabled)
    28843277            $result = $sap_service->create_business_partner($business_partner_data);
    28853278           
     
    28873280                return array(
    28883281                    'success' => true,
    2889                     'message' => 'Card Code: ' . esc_html($result['CardCode'])
     3282                    'message' => '✅ CREATED: New Business Partner with Card Code: ' . esc_html($result['CardCode'])
    28903283                );
    28913284            } else {
Note: See TracChangeset for help on using the changeset viewer.