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:

  1. Explicit ID — If a customer.id is provided, load that customer directly
  2. Phone match — If customer.phone is provided, find-or-create by phone (Customer::findOrCreateByPhone)
  3. Email match — If customer.email is provided, find-or-create by email (Customer::findOrCreateByEmail)
  4. 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:

  1. Combines external_ids and tags from both profiles
  2. Fills missing fields on the primary from the secondary (email, phone, name)
  3. Transfers all orders, addresses, conversations, and metadata to the primary
  4. Appends the secondary's notes with a merge marker
  5. Recalculates order statistics (updateOrderStats)
  6. 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 customers
  • repeat-customer — Multiple purchases
  • at-risk — Haven't ordered recently
  • abandoned-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.orders relationship
  • Conversations — Communication threads via customer.conversations relationship (WhatsApp, SMS, support)
  • Metadata — Module-scoped key-value pairs via customer.metadata relationship
  • Addresses — Billing and shipping addresses via customer.addresses relationship

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

  1. An order event fires (e.g. order.status_changed)
  2. OrderNotificationService finds active notification rules matching the event alias and conditions
  3. Each rule's template is rendered with customer placeholders: {{customer_name}}, {{customer_phone}}, {{customer_email}}, {{order_number}}, {{total}}
  4. A SendOrderNotificationJob is dispatched per rule, which resolves the recipient from the customer relationship on the order and calls the Notifications module bus:
    • SMS / WhatsAppBus::call('Notifications', 'notifications.send', { channel, to: customer.phone, message })
    • EmailBus::call('Notifications', 'notifications.sendEmail', { to: customer.email, subject, body })
    • In-AppBus::call('Notifications', 'notifications.sendInApp', { user_id, title, body, data })

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

  1. Always capture email or phone: Required for customer identification
  2. Enable marketing opt-in: Respect customer preferences
  3. Use tags effectively: Segment customers for targeted campaigns
  4. Monitor customer health: Track at-risk and high-value customers
  5. Regular data hygiene: Merge duplicates and clean invalid data

Next Steps