---
title: Customer/contact combined search
summary: Validates a combined search request and merges contact and customer matches within a shared, configurable result quota.
status: draft
owner: SAIF
last_reviewed: 2026-07-30
library: SAIF.Business.CustomerContact
reference_type: package
operation_id: OP-CC-01
trigger: Caller invokes clsCustomerContactSearch.SearchForCustomerContacts with a populated SearchRequest
---

# Customer/contact combined search

`clsCustomerContactSearch.SearchForCustomerContacts` is the single entry point callers use to search across both contacts (`PERSON`) and customers (`COMPANY`) with one request. The request object itself enforces population and result-size rules, then the dispatcher runs the contact search first, reduces the remaining quota by however many contacts were found, and runs the customer search only if quota remains.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-CC-01 |
| **Trigger** | Caller constructs a `clsCustomerContactSearch.SearchRequest`, sets search criteria, and calls the shared function `SearchForCustomerContacts(inDPI, inRequest)` |
| **Inputs** | `SearchRequest`: name/first/last name strings, reference ID, customer type, relationship type, search-format code, search type (`Contact`, `Customer`, `CustomerContact`), split ratio, result limit |
| **Outputs** | `SearchResult`: merged list of `ICustomerContact` matches, the formatted search strings actually used, and an informational message when results were truncated or the request was invalid |
| **Primary components** | `clsCustomerContactSearch.SearchForCustomerContacts`, `clsCustomerContactSearch.SearchRequest`, `clsContactCollection.SearchForContacts`, `clsCustomerCollection.SearchForCustomers` |
| **Entry point** | `Business\SAIF.Business.CustomerContact\clsCustomerContactSearch.vb` |

## Steps

1. **Normalize the request.** `PrepForSearch()` fills `NameString` from `LastNameString`/`FirstNameString` when no combined name was supplied (for `Customer` or `CustomerContact` search types) and calls `CalcLimits()` to compute the initial contact/customer quotas from `ResultLimit` and the configured split ratio.
2. **Validate the request** ([BR-001](../business-rules.md#br-001-search-requires-at-least-one-populated-criterion)). `IsValidRequest` re-runs `CalcLimits()` and returns `IsCustomerSearchable Or IsContactSearchable`; each of those properties requires the relevant criteria strings to be non-blank for the current `SearchType`.
   - If invalid: `SearchForCustomerContacts` skips both search paths and returns a `SearchResult` with the message `"Invalid Search Request."` and no rows.
3. **Enforce and split the result cap** ([BR-002](../business-rules.md#br-002-search-results-are-capped-at-300)). `CalcLimits()` clamps `ResultLimit` to the `ResultMax` constant (300) and, for a combined `CustomerContact` search, allocates `_ContactLimit` and `_CustomerLimit` from the configured split ratio (default `50/50`, validated by `ValidateSplitRatio` which requires two numeric parts summing to 100).
4. **Search contacts, if applicable.** When `IsContactSearchable` is true and `SearchType` is `Contact` or `CustomerContact`, `clsContactCollection.SearchForContacts(inDPI, inRequest)` runs against `PERSON` (optionally joined to `COMPANY_PERSON` for a relationship-type filter) using the formatted, wildcarded search strings built by `BuildSearchString` ([SQL-031](../sql/SQL-031.md), [PERSON](../database-tables.md#person), [COMPANY_PERSON](../database-tables.md#company-person)). Every returned contact is added to the merged result list.
   - After the contact search: if the contact result count is less than the remaining `ResultLimit`, the limit is reduced by that count for the customer search; otherwise the limit is forced to `0` and the response message is set to `"There are more results than can be returned."`
5. **Search customers, if applicable and quota remains.** When `IsCustomerSearchable` is true, `ResultLimit > 0`, and `SearchType` is `Customer` or `CustomerContact`, `clsCustomerCollection.SearchForCustomers(inDPI, inRequest)` runs against `COMPANY` (optionally joined to `COMPANY_PERSON` for a relationship-type filter) using the same formatted search-string pattern ([SQL-037](../sql/SQL-037.md), [COMPANY](../database-tables.md#company), [COMPANY_PERSON](../database-tables.md#company-person)). Every returned customer is appended to the merged result list.
6. **Build the search-string patterns.** For each populated criterion (`NameString`, `FirstNameString`, `LastNameString`, `ReferenceID`), `BuildSearchString` applies the selected `CustomerContactSearchFormat` (contains-default, contains-specific consonant matching, contains-exact, starts-with, ends-with, and their "exactly" variants) to build the wildcarded (`%`) SQL comparison string used by the contact and customer search SQL.
7. **Return the merged result.** `SearchForCustomerContacts` returns a `SearchResult` carrying the accumulated message, the formatted name/first-name/last-name strings actually applied, and the combined `Results` list.

## Process flow

The following diagram visualizes the combined search dispatch and quota handling.

```mermaid
flowchart TD
	Start([Caller calls SearchForCustomerContacts]) --> Prep[PrepForSearch:<br>normalize NameString, CalcLimits]
	Prep --> Valid{IsValidRequest?}
	Valid -->|No| Invalid([Return: Invalid Search Request])
	Valid -->|Yes| ContactCheck{IsContactSearchable<br>and SearchType includes Contact?}
	ContactCheck -->|Yes| ContactSearch[SearchForContacts against PERSON]
	ContactSearch --> Reduce{Contact count < ResultLimit?}
	Reduce -->|Yes| ReduceLimit[ResultLimit -= contact count]
	Reduce -->|No| ZeroLimit[ResultLimit = 0;<br>set truncation message]
	ContactCheck -->|No| CustomerCheck
	ReduceLimit --> CustomerCheck{IsCustomerSearchable<br>and ResultLimit > 0<br>and SearchType includes Customer?}
	ZeroLimit --> CustomerCheck
	CustomerCheck -->|Yes| CustomerSearch[SearchForCustomers against COMPANY]
	CustomerCheck -->|No| Merge[Merge results]
	CustomerSearch --> Merge
	Merge --> Done([Return SearchResult])
```

## Failure and edge-case behavior
- An invalid request (no populated criteria for the selected `SearchType`) short-circuits both search paths entirely; no SQL executes ([BR-001](../business-rules.md#br-001-search-requires-at-least-one-populated-criterion)).
- The 300-row cap and split ratio mean a contact-heavy result set can consume the entire quota before the customer search runs, which is reported to the caller through the truncation message rather than as an error ([BR-002](../business-rules.md#br-002-search-results-are-capped-at-300)).
- `ValidateSplitRatio` silently ignores a malformed `SplitRatio` value (non-numeric parts, or parts that do not sum to 100) and leaves the previous ratio in effect rather than raising an error.*
- `SearchType.Customer` or `SearchType.Contact` disables the other path entirely regardless of which criteria are populated.

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [SQL catalog](../sql/index.md)
- [Database tables](../database-tables.md)

## Sources
- `Business\SAIF.Business.CustomerContact\clsCustomerContactSearch.vb`
- `Business\SAIF.Business.CustomerContact\Contact\clsContact.vb`
- `Business\SAIF.Business.CustomerContact\Customer\clsCustomer.vb`
