Customers Module
The Customers module maintains unified customer profiles that merge data from multiple sources—store platforms, communication channels, and manual entries.
Overview
Customer profiles in Auto Commerce:
- Unified: Merge customer data from Shopify, WooCommerce, WhatsApp, etc.
- Multi-channel: Track interactions across stores, support, and marketing
- Analytics-rich: Lifetime value, purchase patterns, engagement metrics
- Privacy-focused: Each tenant's customer data is completely isolated
Data Model
Core Customer Fields
id: UUID
external_ids: JSON ({"shopify": "123", "whatsapp": "+91..."})
email: string
phone: string
first_name: string
last_name: string
accepts_marketing: boolean
total_orders: integer
total_spent: decimal
avg_order_value: decimal
first_order_at: timestamp
last_order_at: timestamp
tags: array
Customer Identity Resolution
Auto Commerce resolves customer identity through a priority-based matching system. When a new order arrives (from any source), the OrderIngestService.resolveCustomer() method finds or creates the correct customer profile:
- Explicit ID — If a
customer.idis provided, load that customer directly - Phone match — If
customer.phoneis provided, find-or-create by phone (Customer::findOrCreateByPhone) - Email match — If
customer.emailis provided, find-or-create by email (Customer::findOrCreateByEmail) - Fallback — If neither phone nor email is available, create a new "Guest" customer record
The find-or-create methods use firstOrCreate with the identifier column as the match key. If an existing record is found, the profile is returned as-is (attributes in the second argument are only applied on creation).
// How the Orders module resolves a customer during ingest:
// OrderIngestService::resolveCustomer($data)
if (!empty($data['phone'])) {
return Customer::findOrCreateByPhone($data['phone'], [
'first_name' => $data['first_name'] ?? 'Guest',
'last_name' => $data['last_name'] ?? '',
'email' => $data['email'] ?? null,
]);
}
if (!empty($data['email'])) {
return Customer::findOrCreateByEmail($data['email'], [
'first_name' => $data['first_name'] ?? 'Guest',
'last_name' => $data['last_name'] ?? '',
]);
}
This pattern ensures that repeated orders from the same phone/email are always attributed to a single customer profile regardless of which channel they originate from.
External ID Mapping
Each customer can carry external IDs from multiple platforms via the external_ids JSONB column:
{
"id": "uuid-123",
"email": "customer@example.com",
"external_ids": {
"shopify": "cust_shopify_456",
"woocommerce": "789",
"whatsapp": "+919876543210"
}
}
Use $customer->setExternalId('shopify', '456') and $customer->getExternalId('shopify') to read/write external IDs from any module.
Merging Duplicate Profiles
When duplicates slip through (e.g. a customer used different phone numbers), admins can merge two profiles via the API:
POST /api/v1/customers/merge
{
"primary_id": "uuid-keep",
"secondary_id": "uuid-remove"
}
The merge operation (in CustomerService::merge) runs inside a transaction and:
- Combines
external_idsandtagsfrom both profiles - Fills missing fields on the primary from the secondary (email, phone, name)
- Transfers all orders, addresses, conversations, and metadata to the primary
- Appends the secondary's notes with a merge marker
- Recalculates order statistics (
updateOrderStats) - Hard-deletes the secondary profile
Requires the customers.merge permission (sensitive).
Customer Addresses
id: UUID
customer_id: UUID
type: 'billing' | 'shipping'
is_default: boolean
first_name: string
last_name: string
company: string
address1: string
address2: string
city: string
state: string
pincode: string
country: string
phone: string
Customer Metadata
Modules can store additional data:
id: UUID
customer_id: UUID
key: string
value: string
module_source: string
API Endpoints
Customers
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/customers |
List customers (paginated, filterable) |
POST |
/api/v1/customers |
Create customer |
GET |
/api/v1/customers/search?q= |
Search by name, email, or phone |
GET |
/api/v1/customers/vip |
List VIP customers |
GET |
/api/v1/customers/segments |
Segment counts |
GET |
/api/v1/customers/export |
Export customers |
GET |
/api/v1/customers/{id} |
Get customer detail (with addresses, recent orders) |
PATCH |
/api/v1/customers/{id} |
Update customer |
DELETE |
/api/v1/customers/{id} |
Delete customer (soft delete) |
GET |
/api/v1/customers/{id}/orders |
Customer order history |
Addresses
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/customers/{id}/addresses |
List addresses |
POST |
/api/v1/customers/{id}/addresses |
Add address |
PATCH |
/api/v1/customers/{id}/addresses/{addr} |
Update address |
DELETE |
/api/v1/customers/{id}/addresses/{addr} |
Delete address |
POST |
/api/v1/customers/{id}/addresses/{addr}/default |
Set as default |
Bulk Operations
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/customers/bulk/tags |
Add tags to multiple customers |
DELETE |
/api/v1/customers/bulk/tags |
Remove tags from multiple customers |
POST |
/api/v1/customers/merge |
Merge two customer profiles |
Query Parameters for List:
| Parameter | Type | Description |
|---|---|---|
search |
string | Search name, email, or phone |
tags |
string/array | Filter by tags (comma-separated or array) |
accepts_marketing |
boolean | Marketing opt-in filter |
vip |
boolean | VIP customers only |
min_orders |
integer | Minimum order count |
min_spent |
decimal | Minimum total spend |
created_from / created_to |
date | Date range filter |
sort_by |
string | Sort field (created_at, first_name, total_orders, total_spent, last_order_at) |
sort_order |
string | asc or desc |
page / per_page |
integer | Pagination (max 100 per page) |
Customer Segmentation
Tags
Tags are free-form string labels stored in a JSONB array on each customer. Apply them manually or in bulk for ad-hoc grouping:
vip— High-value customersrepeat-customer— Multiple purchasesat-risk— Haven't ordered recentlyabandoned-cart— Left items in cart- Custom tags — any string up to 50 characters
Bulk tag operations:
POST /api/v1/customers/bulk/tags
{
"customer_ids": ["uuid-1", "uuid-2"],
"tags": ["vip", "wholesale"]
}
DELETE /api/v1/customers/bulk/tags
{
"customer_ids": ["uuid-1", "uuid-2"],
"tags": ["at-risk"]
}
Tags are additive on bulk-add (merged with existing tags, deduplicated) and subtractive on bulk-remove.
Smart Segments
The module defines five built-in segments in config/customers.php. Each segment is a named query condition evaluated at request time via CustomerService::getSegmentCounts().
| Segment | Condition | Description |
|---|---|---|
new |
total_orders = 0 |
Registered but never purchased |
returning |
total_orders > 0 AND < 5 |
Repeat buyers, not yet loyal |
loyal |
total_orders >= 5 |
Consistent repeat purchasers |
vip |
total_orders >= 10 OR total_spent >= ₹50,000 |
Highest-value customers |
at_risk |
last_order_at < 90 days ago |
Previously active, now dormant |
marketing |
accepts_marketing = true |
Opted in to marketing communications |
VIP thresholds are configurable per tenant via settings_schema:
| Setting | Default | Description |
|---|---|---|
vip_order_threshold |
10 | Minimum orders to qualify as VIP |
vip_spend_threshold |
50000 | Minimum total spend (INR) to qualify as VIP |
Segments API (GET /api/v1/customers/segments) returns all segment labels with live counts. VIP endpoint (GET /api/v1/customers/vip) returns paginated VIP customers sorted by total spend descending, using the scopeVip query scope.
Customer Analytics
Lifetime Value (LTV)
LTV = Average Order Value × Purchase Frequency × Customer Lifespan
Tracked automatically for each customer.
Purchase Patterns
- Average days between orders
- Preferred product categories
- Typical order value range
- Best communication channel
Engagement Metrics
- Email open rate
- WhatsApp response rate
- Support ticket count
- Review/feedback submissions
Customer Lifecycle
Order Statistics
Every customer carries denormalized order metrics that are recalculated when orders are created, updated, or merged:
| Field | Type | Description |
|---|---|---|
total_orders |
integer | Count of non-cancelled/non-failed orders |
total_spent |
decimal | Sum of order totals |
avg_order_value |
decimal | Average order total |
first_order_at |
timestamp | Date of first order |
last_order_at |
timestamp | Date of most recent order |
Recalculation is triggered by Customer::updateOrderStats(), which runs a single aggregate query across the customer's orders (excluding cancelled and failed).
Order History
Retrieve a customer's full order history with line items:
GET /api/v1/customers/{id}/orders
Returns paginated orders (most recent first) with items eager-loaded. Supports per_page parameter.
Activity Tracking
Customer interactions span multiple models:
- Orders — Full purchase history via
customer.ordersrelationship - Conversations — Communication threads via
customer.conversationsrelationship (WhatsApp, SMS, support) - Metadata — Module-scoped key-value pairs via
customer.metadatarelationship - Addresses — Billing and shipping addresses via
customer.addressesrelationship
Soft Deletion
Customer records use Laravel's SoftDeletes trait. When a customer is deleted via the API, the record is soft-deleted and can be restored if needed. Use the hard-delete option for GDPR erasure requests (see Privacy section below).
Privacy & Compliance
Data Export
Customers can request their data:
GET /api/v1/customers/{id}/export
Returns all data in JSON format.
Data Deletion
GDPR-compliant deletion:
DELETE /api/v1/customers/{id}
Options:
- Hard delete: Completely remove customer
- Anonymize: Keep order history but remove personal info
Communication Integration
Customer data is the backbone of all outbound communication. When an order event fires (e.g. order.status_changed), the notification system resolves the customer's contact details to deliver messages across channels.
Notification Flow
- An order event fires (e.g.
order.status_changed) OrderNotificationServicefinds active notification rules matching the event alias and conditions- Each rule's template is rendered with customer placeholders:
{{customer_name}},{{customer_phone}},{{customer_email}},{{order_number}},{{total}} - A
SendOrderNotificationJobis dispatched per rule, which resolves the recipient from the customer relationship on the order and calls the Notifications module bus:- SMS / WhatsApp —
Bus::call('Notifications', 'notifications.send', { channel, to: customer.phone, message }) - Email —
Bus::call('Notifications', 'notifications.sendEmail', { to: customer.email, subject, body }) - In-App —
Bus::call('Notifications', 'notifications.sendInApp', { user_id, title, body, data })
- SMS / WhatsApp —
If the customer lacks the required contact field (e.g. no phone for SMS), the notification is skipped with a warning log.
Marketing Consent
The accepts_marketing boolean on each customer controls whether they receive promotional communications. Transactional notifications (order confirmations, shipping updates) are sent regardless of this flag. Filter marketing-eligible customers:
GET /api/v1/customers?accepts_marketing=true
Module Integration
Customer Events
Event::listen(CustomerCreated::class, function ($event) {
// Send welcome message via Notifications bus
// Add to marketing list
// Sync to external CRM
});
Event::listen(CustomerUpdated::class, function ($event) {
// Sync changes to external CRM
// Update communication preferences
});
Adding Customer Metadata
Any module can store scoped key-value data on a customer profile:
// Store metadata (scoped by module_source to avoid key conflicts)
$customer->setMeta('loyalty_points', '1250', 'loyalty-module');
$customer->setMeta('referral_code', 'REF-ABC', 'referrals-module');
// Read metadata
$points = $customer->getMeta('loyalty_points'); // '1250'
Conversations Relationship
The Customer model has a conversations() relationship that links to communication threads. This allows the WhatsApp and other messaging modules to associate conversation history with a customer profile. When customers are merged, conversations are transferred to the primary profile.
Module API Bus Integration
The Customers module does not currently register formal bus endpoints, but its services are consumed by other modules via direct service resolution and model methods. The Orders module is the primary consumer.
Consumed by Other Modules
| Consumer | Method | Description |
|---|---|---|
Orders (OrderIngestService) |
Customer::findOrCreateByPhone() |
Resolve customer during order ingest |
Orders (OrderIngestService) |
Customer::findOrCreateByEmail() |
Resolve customer during order ingest |
Orders (SendOrderNotificationJob) |
order.customer relationship |
Resolve recipient for notifications |
| Any module | $customer->setMeta() / getMeta() |
Store/read module-scoped metadata |
| Any module | $customer->setExternalId() / getExternalId() |
Map external platform IDs |
Published Events
| Event | Trigger |
|---|---|
customer.created |
New customer record created |
customer.updated |
Customer profile updated |
Permissions
| Permission | Description | Sensitivity |
|---|---|---|
customers.view |
View customer list and profiles | Normal |
customers.create |
Create new customer records | Normal |
customers.edit |
Edit customer information | Normal |
customers.delete |
Delete customer records | Sensitive |
customers.export |
Export customer data | Normal |
customers.merge |
Merge duplicate customer records | Sensitive |
customers.view_sensitive |
View sensitive data (phone, address) | Sensitive |
Best Practices
- Always capture email or phone: Required for customer identification
- Enable marketing opt-in: Respect customer preferences
- Use tags effectively: Segment customers for targeted campaigns
- Monitor customer health: Track at-risk and high-value customers
- Regular data hygiene: Merge duplicates and clean invalid data
Next Steps
- Orders Module - Process customer orders
- Products Module - Manage product catalog
- Creating Modules - Build custom integrations