$npx -y skills add Lonsdale201/wp-agent-skills --skill fluentcrm-companies-modelWork with FluentCRM 3.x Companies / account records from companion plugins. Covers the experimental company_module flag, FluentCrmApi('companies'), Company model fields, createOrUpdate, owner_id as Subscriber ID, custom company fields in meta.custom_values, primary company vs man
| 1 | # FluentCRM: Companies model and contact-company relations |
| 2 | |
| 3 | Use this skill when a plugin needs to sync B2B account/company records into FluentCRM or attach contacts to existing companies. Companies are core FluentCRM 3.1.8 code, but the UI/automation surface is behind the experimental `company_module` setting. |
| 4 | |
| 5 | ## Guard the feature |
| 6 | |
| 7 | For UI, automation actions, or user-visible sync flows, check the module flag: |
| 8 | |
| 9 | ```php |
| 10 | use FluentCrm\App\Services\Helper; |
| 11 | |
| 12 | if (!function_exists('FluentCrmApi') || !Helper::isCompanyEnabled()) { |
| 13 | return; |
| 14 | } |
| 15 | ``` |
| 16 | |
| 17 | `Helper::isCompanyEnabled()` reads `_fluentcrm_experimental_settings['company_module'] === 'yes'`. The low-level API does not hard-block writes when the flag is off, so do not create surprise company data from a hidden integration unless the admin opted in. |
| 18 | |
| 19 | ## API entry points |
| 20 | |
| 21 | `app/Api/config.php` registers the API key as `companies`: |
| 22 | |
| 23 | ```php |
| 24 | $companiesApi = FluentCrmApi('companies'); |
| 25 | ``` |
| 26 | |
| 27 | Create or update a company: |
| 28 | |
| 29 | ```php |
| 30 | $company = FluentCrmApi('companies')->createOrUpdate([ |
| 31 | 'name' => sanitize_text_field($accountName), |
| 32 | 'email' => sanitize_email($billingEmail), |
| 33 | 'website' => esc_url_raw($website), |
| 34 | 'type' => 'Customer', |
| 35 | 'industry' => sanitize_text_field($industry), |
| 36 | 'owner_id' => (int) $fluentContactId, // Subscriber ID, not WP user ID |
| 37 | 'custom_values' => [ |
| 38 | 'external_account_id' => sanitize_text_field($externalId), |
| 39 | ], |
| 40 | ]); |
| 41 | ``` |
| 42 | |
| 43 | Important behavior from `Companies::createOrUpdate()`: |
| 44 | |
| 45 | - Existing company lookup is by `id` when provided, otherwise by exact `name`. |
| 46 | - `owner_id` is a FluentCRM `Subscriber` ID. It is not a WordPress user ID. |
| 47 | - Setting `owner_id` attaches that contact to the company and sets the contact's primary `company_id` if empty. |
| 48 | - `custom_values` are formatted by `CustomCompanyField` and stored under serialized `Company.meta['custom_values']`. |
| 49 | - Create fires `fluent_crm/company_created`; update fires `fluent_crm/company_updated`. |
| 50 | |
| 51 | Do not call `FluentCrmApi('companies')->getCompany($name)` for name lookup. The method name says `$idOrName`, but the 3.1.8 source checks `id` for numeric values and `email` for strings. For name lookup use: |
| 52 | |
| 53 | ```php |
| 54 | use FluentCrm\App\Models\Company; |
| 55 | |
| 56 | $company = Company::where('name', $name)->first(); |
| 57 | ``` |
| 58 | |
| 59 | ## Data model |
| 60 | |
| 61 | `Company` maps `fc_companies`. Core fields include: |
| 62 | |
| 63 | ```php |
| 64 | name, owner_id, industry, type, email, phone, website, |
| 65 | address_line_1, address_line_2, postal_code, city, state, country, |
| 66 | timezone, employees_number, description, logo, |
| 67 | linkedin_url, facebook_url, twitter_url, date_of_start, meta |
| 68 | ``` |
| 69 | |
| 70 | Relations: |
| 71 | |
| 72 | - `Company::subscribers()` uses `fc_subscriber_pivot` with `object_type = FluentCrm\App\Models\Company`. |
| 73 | - `Company::owner()` belongs to a `Subscriber` through `owner_id`. |
| 74 | - `Company::notes()` stores company notes in `fc_subscriber_notes` with status `_company_note_`. |
| 75 | - `Subscriber::company()` points to the primary company via `fc_subscribers.company_id`. |
| 76 | - `Subscriber::companies()` is the many-to-many relation through the pivot table. |
| 77 | |
| 78 | Treat `company_id` as the primary/display company only. A contact can belong to multiple companies through `Subscriber::companies()`. |
| 79 | |
| 80 | ## Attach and detach contacts |
| 81 | |
| 82 | Prefer the API wrapper for bulk changes: |
| 83 | |
| 84 | ```php |
| 85 | $result = FluentCrmApi('companies')->attachContactsByIds( |
| 86 | [(int) $contactId], |
| 87 | [(int) $companyId] |
| 88 | ); |
| 89 | |
| 90 | if (!$result) { |
| 91 | // At least one company ID was invalid, no subscribers were found, or input was empty. |
| 92 | } |
| 93 | ``` |
| 94 | |
| 95 | `attachContactsByIds()` validates that every requested company ID exists. It attaches all valid companies to each subscriber and sets the first company as primary only when `company_id` is empty. |
| 96 | |
| 97 | Detach: |
| 98 | |
| 99 | ```php |
| 100 | FluentCrmApi('companies')->detachContactsByIds([$contactId], [$companyId]); |
| 101 | ``` |
| 102 | |
| 103 | Detach behavior: |
| 104 | |
| 105 | - If a detached contact was the company owner, `owner_id` is cleared. |
| 106 | - If the detached company was the contact's primary `company_id`, FluentCRM promotes the first remaining related company or sets `company_id = n |