@memberjunction/actions-bizapps-crm
Version:
CRM system integration actions for MemberJunction
409 lines (327 loc) • 17.4 kB
Markdown
# /actions-bizapps-crm
Customer Relationship Management (CRM) integration actions for MemberJunction. This package provides 22 HubSpot CRM actions covering contact, company, deal, and activity management, built on a provider-extensible architecture that supports multi-tenant credential management.
This package is part of the [BizApps actions family](../README.md) within the [MemberJunction Actions Framework](../../README.md). See those documents for framework-level patterns, the action design philosophy, and shared conventions across all BizApps integrations.
## Architecture
The package uses a three-tier class hierarchy: a domain-level base class for all CRM providers, a provider-specific base class for HubSpot, and individual action classes for each operation.
```mermaid
graph TD
subgraph Framework["MemberJunction Actions Framework"]
BA["BaseAction<br/>@memberjunction/actions"]
end
subgraph CRM["CRM Domain Layer"]
BCRM["BaseCRMAction<br/>Credential management, validation helpers,<br/>date/phone formatting, status mapping"]
end
subgraph HubSpot["HubSpot Provider Layer"]
HSB["HubSpotBaseAction<br/>API auth, pagination,<br/>search, batch ops, associations"]
end
subgraph Actions["HubSpot Actions (22 total)"]
direction LR
CC["Contact Actions<br/>Create, Update, Get,<br/>Search, Delete, Merge"]
CO["Company Actions<br/>Create, Update, Get,<br/>Search, Associate"]
DA["Deal Actions<br/>Create, Update, Get,<br/>Search, By Contact, By Company"]
AA["Activity Actions<br/>Log Activity, Create Task,<br/>Update Task, Get Activities,<br/>Get Upcoming Tasks"]
end
BA --> BCRM
BCRM --> HSB
HSB --> CC
HSB --> CO
HSB --> DA
HSB --> AA
style Framework fill:#64748b,stroke:#475569,color:#fff
style CRM fill:#2d6a9f,stroke:#1a4971,color:#fff
style HubSpot fill:#b8762f,stroke:#8a5722,color:#fff
style Actions fill:#2d8659,stroke:#1a5c3a,color:#fff
```
### Credential Resolution Flow
All actions resolve credentials through a two-step lookup: environment variables first, database fallback second. This supports both development and production deployment patterns.
```mermaid
flowchart LR
A["Action receives<br/>CompanyID param"] --> B["Look up<br/>CompanyIntegration<br/>entity"]
B --> C{"Env var<br/>exists?"}
C -->|Yes| D["Use env var<br/>BIZAPPS_HUBSPOT_<br/>{ID}_ACCESS_TOKEN"]
C -->|No| E{"DB credentials<br/>exist?"}
E -->|Yes| F["Use AccessToken /<br/>APIKey from entity"]
E -->|No| G["Throw error:<br/>no credentials"]
style A fill:#64748b,stroke:#475569,color:#fff
style B fill:#2d6a9f,stroke:#1a4971,color:#fff
style C fill:#b8762f,stroke:#8a5722,color:#fff
style D fill:#2d8659,stroke:#1a5c3a,color:#fff
style E fill:#b8762f,stroke:#8a5722,color:#fff
style F fill:#2d8659,stroke:#1a5c3a,color:#fff
style G fill:#7c5295,stroke:#563a6b,color:#fff
```
## Installation
```bash
npm install /actions-bizapps-crm
```
This package is server-side only. It is typically consumed by MJAPI or other server-side hosts that register actions via the MemberJunction class factory.
## Configuration
### 1. Create Integration Record
Register HubSpot as an integration in your MemberJunction database:
```sql
INSERT INTO Integration (Name, Description, NavigationBaseURL, ClassName)
VALUES ('HubSpot', 'HubSpot CRM Integration',
'https://api.hubapi.com', 'HubSpotIntegration');
```
### 2. Link to Company
Create a CompanyIntegration record for each tenant/company that uses HubSpot:
```sql
INSERT INTO CompanyIntegration (CompanyID, IntegrationID, ExternalSystemID, IsActive)
VALUES (, , , 1);
```
### 3. Set Credentials
Credentials are resolved from environment variables first, then from database fields.
**Environment variables (recommended):**
```bash
# OAuth access token (preferred)
BIZAPPS_HUBSPOT_{COMPANY_ID}_ACCESS_TOKEN=your_access_token
# Or private app API key
BIZAPPS_HUBSPOT_{COMPANY_ID}_API_KEY=your_api_key
# Optional OAuth fields
BIZAPPS_HUBSPOT_{COMPANY_ID}_REFRESH_TOKEN=your_refresh_token
BIZAPPS_HUBSPOT_{COMPANY_ID}_CLIENT_ID=your_client_id
BIZAPPS_HUBSPOT_{COMPANY_ID}_CLIENT_SECRET=your_client_secret
```
**Database fallback:** store credentials in the `AccessToken`, `APIKey`, or `RefreshToken` columns of the CompanyIntegration entity.
## Available Actions
### Contact Management
| Action | Class | Description |
|--------|-------|-------------|
| Create Contact | `CreateContactAction` | Creates a new contact with email validation and optional company association |
| Update Contact | `UpdateContactAction` | Updates an existing contact's properties |
| Get Contact | `GetContactAction` | Retrieves a single contact by ID with optional associations |
| Search Contacts | `SearchContactsAction` | Searches contacts with filters, sorting, pagination, and date ranges |
| Delete Contact | `DeleteContactAction` | Deletes (archives) a contact |
| Merge Contacts | `MergeContactsAction` | Merges duplicate contacts into a single record |
### Company Management
| Action | Class | Description |
|--------|-------|-------------|
| Create Company | `CreateCompanyAction` | Creates a new company record |
| Update Company | `UpdateCompanyAction` | Updates an existing company's properties |
| Get Company | `GetCompanyAction` | Retrieves a single company by ID |
| Search Companies | `SearchCompaniesAction` | Searches companies with flexible filter criteria |
| Associate Contact to Company | `AssociateContactToCompanyAction` | Links contacts to companies with batch support and primary designation |
### Deal Management
| Action | Class | Description |
|--------|-------|-------------|
| Create Deal | `CreateDealAction` | Creates a new deal/opportunity |
| Update Deal | `UpdateDealAction` | Updates deal properties and pipeline stage |
| Get Deal | `GetDealAction` | Retrieves deal details with associations, timeline, and metrics |
| Search Deals | `SearchDealsAction` | Searches deals with flexible filter criteria |
| Get Deals by Contact | `GetDealsByContactAction` | Retrieves all deals associated with a contact |
| Get Deals by Company | `GetDealsByCompanyAction` | Retrieves all deals associated with a company |
### Activity Management
| Action | Class | Description |
|--------|-------|-------------|
| Log Activity | `LogActivityAction` | Logs calls, emails, meetings, or notes with contact/company/deal associations |
| Create Task | `CreateTaskAction` | Creates a task with subject, priority, due date, and owner |
| Update Task | `UpdateTaskAction` | Updates an existing task's properties |
| Get Activities by Contact | `GetActivitiesByContactAction` | Retrieves all activities for a specific contact |
| Get Upcoming Tasks | `GetUpcomingTasksAction` | Gets upcoming/overdue tasks filtered by owner, status, and priority |
## Usage Examples
### Creating a Contact
```typescript
import { ActionEngineServer } from '/actions';
const engine = ActionEngineServer.Instance;
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'CreateContactAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'Email', Type: 'Input', Value: 'jane.doe.com' },
{ Name: 'FirstName', Type: 'Input', Value: 'Jane' },
{ Name: 'LastName', Type: 'Input', Value: 'Doe' },
{ Name: 'Company', Type: 'Input', Value: 'Acme Corp' },
{ Name: 'JobTitle', Type: 'Input', Value: 'VP of Sales' },
{ Name: 'LifecycleStage', Type: 'Input', Value: 'lead' },
{ Name: 'AssociateWithCompanyId', Type: 'Input', Value: 'hubspot-company-id' }
],
ContextUser: contextUser
});
if (result.Success) {
const summary = result.Params.find(p => p.Name === 'Summary')?.Value;
console.log(`Created contact ${summary.contactId} at ${summary.portalUrl}`);
}
```
### Searching Contacts
```typescript
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'SearchContactsAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'LifecycleStage', Type: 'Input', Value: 'lead' },
{ Name: 'CreatedAfter', Type: 'Input', Value: '2025-01-01' },
{ Name: 'SortBy', Type: 'Input', Value: 'createdate' },
{ Name: 'SortOrder', Type: 'Input', Value: 'DESC' },
{ Name: 'Limit', Type: 'Input', Value: 50 }
],
ContextUser: contextUser
});
if (result.Success) {
const contacts = result.Params.find(p => p.Name === 'Contacts')?.Value;
const summary = result.Params.find(p => p.Name === 'Summary')?.Value;
console.log(`Found ${summary.totalResults} contacts`);
console.log('By lifecycle stage:', summary.resultStats.byLifecycleStage);
}
```
### Logging an Activity
```typescript
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'LogActivityAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'ActivityType', Type: 'Input', Value: 'CALL' },
{ Name: 'Subject', Type: 'Input', Value: 'Follow-up call' },
{ Name: 'Body', Type: 'Input', Value: 'Discussed renewal terms' },
{ Name: 'Status', Type: 'Input', Value: 'COMPLETED' },
{ Name: 'DurationMilliseconds', Type: 'Input', Value: 900000 },
{ Name: 'ContactIds', Type: 'Input', Value: ['contact-id-1'] },
{ Name: 'DealIds', Type: 'Input', Value: ['deal-id-1'] }
],
ContextUser: contextUser
});
```
### Retrieving a Deal with Associations
```typescript
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'GetDealAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'DealId', Type: 'Input', Value: 'hubspot-deal-id' },
{ Name: 'IncludeAssociations', Type: 'Input', Value: true },
{ Name: 'IncludeTimeline', Type: 'Input', Value: true }
],
ContextUser: contextUser
});
if (result.Success) {
const summary = result.Params.find(p => p.Name === 'Summary')?.Value;
console.log(`Deal: ${summary.dealName}, Stage: ${summary.dealStage}`);
console.log(`Amount: ${summary.amount}, Days open: ${summary.metrics.daysOpen}`);
console.log(`Overdue: ${summary.metrics.isOverdue}`);
}
```
### Getting Upcoming Tasks
```typescript
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'GetUpcomingTasksAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'OwnerId', Type: 'Input', Value: 'owner-id' },
{ Name: 'DaysAhead', Type: 'Input', Value: 14 },
{ Name: 'IncludeOverdue', Type: 'Input', Value: true },
{ Name: 'Statuses', Type: 'Input', Value: ['NOT_STARTED', 'IN_PROGRESS'] }
],
ContextUser: contextUser
});
if (result.Success) {
const summary = result.Params.find(p => p.Name === 'Summary')?.Value;
console.log(`Total tasks: ${summary.totalTasks}`);
console.log(`Overdue: ${summary.overdueCount}, Due today: ${summary.dueTodayCount}`);
console.log('By due date:', summary.byDueDate);
}
```
## API Reference
### Base Classes
#### BaseCRMAction
Abstract base class for all CRM actions. Provides:
| Method | Description |
|--------|-------------|
| `getCompanyIntegration(companyId, contextUser)` | Looks up and caches the CompanyIntegration entity for a given company |
| `getCredentialFromEnv(companyId, credentialType)` | Resolves credentials from environment variables using `BIZAPPS_{PROVIDER}_{ID}_{TYPE}` format |
| `getAPICredentials(integration)` | Resolves API credentials with env-var-first, database-fallback strategy |
| `getAPIBaseURL(integration)` | Returns the base URL for API calls (from `CustomAttribute1` or provider default) |
| `formatCRMDate(date)` | Formats a Date to ISO 8601 string |
| `parseCRMDate(dateString)` | Parses an ISO 8601 string to Date |
| `formatPhoneNumber(phone)` | Normalizes phone numbers toward E.164 format |
| `isValidEmail(email)` | Validates email format with a regex check |
| `mapDealStatus(stage)` | Maps CRM-specific deal stage names to `'open' \| 'won' \| 'lost' \| 'unknown'` |
| `mapActivityType(type)` | Maps activity type strings to `'call' \| 'email' \| 'meeting' \| 'task' \| 'note' \| 'other'` |
#### HubSpotBaseAction
Provider-specific base class extending `BaseCRMAction`. Adds HubSpot v3 API support:
| Method | Description |
|--------|-------------|
| `makeHubSpotRequest<T>(endpoint, method, body, contextUser)` | Makes an authenticated request to the HubSpot CRM v3 API |
| `makeHubSpotPaginatedRequest<T>(endpoint, params, contextUser)` | Handles cursor-based pagination with configurable `MaxResults` limit |
| `searchHubSpotObjects<T>(objectType, filters, properties, contextUser)` | Executes a HubSpot search API call with filter groups |
| `batchCreateHubSpotObjects<T>(objectType, objects, contextUser)` | Batch-creates objects in HubSpot |
| `batchUpdateHubSpotObjects<T>(objectType, updates, contextUser)` | Batch-updates objects in HubSpot |
| `associateObjects(fromType, fromId, toType, toId, assocType, contextUser)` | Creates an association between two HubSpot objects |
| `getAssociationTypeId(fromType, toType)` | Returns the default HubSpot association type ID for a given pair |
| `mapHubSpotProperties(hubspotObject)` | Flattens a HubSpot object's `properties` map alongside `id`, `createdAt`, `updatedAt`, `archived` |
### Common Parameters
Every action accepts at minimum:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `CompanyID` | Input | Yes | The MemberJunction CompanyIntegration company identifier |
Most actions also produce:
| Parameter | Type | Description |
|-----------|------|-------------|
| `Summary` | Output | Structured summary of the operation result |
### Result Codes
Actions return standardized result codes:
| Code | Meaning |
|------|---------|
| `SUCCESS` | Operation completed successfully |
| `PARTIAL_SUCCESS` | Batch operation with some failures (e.g., associating multiple contacts) |
| `VALIDATION_ERROR` | Missing or invalid input parameters |
| `DUPLICATE_CONTACT` | Contact with the given email already exists |
| `DEAL_NOT_FOUND` | Requested deal ID does not exist |
| `NOT_FOUND` | Requested resource does not exist |
| `ERROR` | General operation failure |
## Source Structure
```
src/
index.ts # Package entry point
base/
base-crm.action.ts # BaseCRMAction - domain base class
providers/
hubspot/
index.ts # HubSpot provider exports
hubspot-base.action.ts # HubSpotBaseAction - provider base class
actions/
index.ts # Action barrel exports
create-contact.action.ts # Contact CRUD
update-contact.action.ts
get-contact.action.ts
search-contacts.action.ts
delete-contact.action.ts
merge-contacts.action.ts
create-company.action.ts # Company management
update-company.action.ts
get-company.action.ts
search-companies.action.ts
associate-contact-to-company.action.ts
create-deal.action.ts # Deal management
update-deal.action.ts
get-deal.action.ts
search-deals.action.ts
get-deals-by-contact.action.ts
get-deals-by-company.action.ts
log-activity.action.ts # Activity management
create-task.action.ts
update-task.action.ts
get-activities-by-contact.action.ts
get-upcoming-tasks.action.ts
```
## Dependencies
| Package | Purpose |
|---------|---------|
| `/actions` | `BaseAction` class and `ActionEngineServer` |
| `/actions-base` | `ActionParam`, `RunActionParams`, `ActionResultSimple` types |
| `/core` | `UserInfo`, `Metadata`, `RunView` |
| `/core-entities` | `CompanyIntegrationEntity` for credential lookup |
| `/global` | `` decorator for class factory registration |
## Related Packages
- [/actions-bizapps-accounting](../Accounting/README.md) - Accounting system integrations (QuickBooks, etc.)
- [/actions-bizapps-lms](../LMS/README.md) - Learning management system integrations (LearnWorlds, etc.)
- [/actions](../../Engine/readme.md) - Actions execution engine
- [/actions-base](../../Base/README.md) - Base classes and type definitions
## Adding New Providers
To add a new CRM provider (e.g., Salesforce):
1. Create a provider directory: `src/providers/salesforce/`
2. Create a provider base class extending `BaseCRMAction`:
- Set `crmProvider` and `integrationName`
- Implement provider-specific authentication and request methods
3. Add individual action classes in `src/providers/salesforce/actions/`
4. Register each action with `(BaseAction, 'ActionName')`
5. Export from provider `index.ts` and the package root `index.ts`
6. Configure Integration and CompanyIntegration records for the new provider