Complete Product Lifecycle Guide
This guide provides an end-to-end walkthrough of the product lifecycle in OmniCRM, from creating a product definition through provisioning services, adding addons, and deprovisioning. We'll cover pricing strategy, Ansible integration, and provide real-world examples throughout.
Overview: The Product-to-Service Journey
The lifecycle of a product in OmniCRM follows these stages:
- Product Definition - Administrator creates product template with pricing and provisioning rules
- Service Creation - Customer orders product, system provisions service instance
- Service Lifecycle - Customer uses service, adds addons/topups, modifies service
- Deprovisioning - Service is terminated, resources are released
Understanding Pricing: Wholesale vs Retail
Every product and service in OmniCRM has two pricing dimensions: wholesale and retail.
Wholesale Cost
The wholesale cost represents the actual cost to deliver the service:
- Infrastructure and bandwidth costs
- Licensing fees
- Equipment costs
- Operational expenses
Retail Cost
The retail cost is the amount charged to the customer.
Setup Costs
Both wholesale and retail have setup cost variants for one-time provisioning charges:
wholesale_setup_cost- Your cost to provisionretail_setup_cost- Amount charged to customer for activation
Example:
{
"retail_cost": 15.00,
"wholesale_cost": 5.00,
"retail_setup_cost": 0.00,
"wholesale_setup_cost": 1.00
}
Stage 1: Creating a Product Definition
Products are templates that define what gets provisioned and how customers are charged.
Creating a Mobile SIM Product
Let's create a prepaid mobile SIM product with 20GB data per month.
Step 1: Navigate to Product Management
From the admin UI, go to Products → Create Product.
Step 2: Define Basic Information
{
"product_name": "Prepaid Mobile 20GB",
"product_slug": "prepaid-mobile-20gb",
"category": "standalone",
"service_type": "mobile",
"enabled": true,
"icon": "fa-solid fa-sim-card",
"comment": "Prepaid mobile SIM with 20GB data, unlimited calls & texts"
}
Field Explanations:
product_name- Customer-facing name shown in catalogproduct_slug- URL-safe identifier used in API calls and linkscategory- "standalone" means this creates a new service (vs addon/bundle)service_type- Groups related products, used for addon filteringenabled- Must be true for product to be orderableicon- FontAwesome icon displayed in UIcomment- Internal notes for staff reference
Step 3: Set Pricing
{
"retail_cost": 15.00,
"wholesale_cost": 5.00,
"retail_setup_cost": 0.00,
"wholesale_setup_cost": 1.00,
"contract_days": 30
}
Pricing Breakdown:
- Monthly revenue per customer: £15.00
- Monthly cost to deliver: £5.00
- Monthly profit margin: £10.00 (200% markup, 67% margin)
- Setup profit: -£1.00 (subsidized to attract customers)
- Contract length: 30 days (monthly renewal)
Step 4: Define Customer Eligibility
{
"residential": true,
"business": false,
"customer_can_purchase": true,
"available_from": "2025-01-01T00:00:00Z",
"available_until": null
}
- Residential customers can order
- Business customers cannot (different product line)
- Self-service purchase enabled
- Available from Jan 1, 2025 onwards
- No end date (ongoing offer)
Step 5: Configure Auto-Renewal
{
"auto_renew": "prompt",
"allow_auto_renew": true
}
"prompt"- Ask customer if they want auto-renewal at purchase"true"- Automatically renew without asking"false"- Never auto-renew (manual top-up only)allow_auto_renew: true- Customer can enable/disable auto-renewal later
Step 6: Specify Inventory Requirements
Inventory requirements define which physical or virtual resources must be allocated when provisioning this product. This is a critical step that connects your product catalog to your Inventory Management System .
{
"inventory_items_list": "['SIM Card', 'Mobile Number']"
}
What Are Inventory Items?
Inventory items are trackable resources stored in the OmniCRM inventory system. Each item has:
- Type - Defined by the Inventory Template (e.g., "SIM Card", "Mobile Number", "Modem")
- Unique attributes - Serial numbers, MAC addresses, phone numbers, etc.
- State - In Stock, Assigned, Decommissioned, etc.
- Location - Physical or logical location
How Inventory Requirements Work:
The inventory_items_list is a Python list (as a string) containing
inventory type names. Each name must exactly match an existing
Inventory Template name.
Example Inventory Requirements:
# Mobile SIM product
inventory_items_list: "['SIM Card', 'Mobile Number']"
# Fixed internet service
inventory_items_list: "['Modem Router', 'Static IP Address']"
# Digital service (no physical items)
inventory_items_list: "[]"
# Fixed wireless with CPE
inventory_items_list: "['Fixed Wireless CPE', 'IPv4 Address', 'IPv6 Prefix']"
The Inventory Picker Process
When a user provisions a product with inventory requirements, the system enforces a mandatory selection process:
1. Provision Button Clicked
After selecting the product, the user clicks "Provision". Instead of
immediately provisioning, the system checks inventory_items_list.
2. Inventory Picker Modal Appears
If inventory is required, a modal dialog appears with a separate dropdown for each inventory type:
3. Filtering Available Inventory
The dropdown for each inventory type only shows items that are:
- Correct Type - Matches the inventory template name exactly
- Available Status -
item_stateis "New" or "In Stock" (not "Assigned" or "Damaged") - Not Assigned -
service_idandcustomer_idare NULL - In Stock at Location - Optionally filtered by warehouse/store location
Example Dropdown Options:
For "SIM Card" inventory type, the dropdown might show:
Each option displays:
- Inventory ID or reference number
- Primary identifier (
itemtext1- e.g., ICCID for SIM, number for phone) - Current location (
item_location)
4. Selection Required to Proceed
Critical Rule: Provisioning CANNOT proceed without selecting all required inventory items.
- "Continue" button is disabled until all dropdowns have selections
- User must select one item for each inventory type
- System validates selections before proceeding
5. Selected Inventory Passed to Ansible
Once user clicks "Continue", the selected inventory IDs are passed to the Ansible playbook as variables:
# User selected:
# - SIM Card inventory_id: 5001
# - Mobile Number inventory_id: 5002
# Variables passed to Ansible:
{
"product_id": 42,
"customer_id": 123,
"SIM Card": 5001, # Inventory ID
"Mobile Number": 5002, # Inventory ID
"access_token": "eyJ..."
}
Note: The variable name matches the inventory type exactly. The
playbook uses hostvars[inventory_hostname]['SIM Card'] to access the
inventory ID.
6. Playbook Fetches Full Inventory Details
The Ansible playbook uses the inventory ID to fetch complete details:
- name: Get SIM Card details from inventory
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ hostvars[inventory_hostname]['SIM Card'] }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
register: api_response_sim
- name: Extract ICCID and IMSI
set_fact:
iccid: "{{ api_response_sim.json.itemtext1 }}"
imsi: "{{ api_response_sim.json.itemtext2 }}"
Now the playbook has all SIM details (ICCID, IMSI, etc.) to provision the subscriber in the HSS.
7. Inventory State Changed to "Assigned"
After the service record is created, the playbook updates inventory to link it to the service:
- name: Assign SIM Card to Service
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ hostvars[inventory_hostname]['SIM Card'] }}"
method: PATCH
body:
{
"service_id": "{{ service_creation_response.json.service_id }}",
"customer_id": "{{ customer_id }}",
"item_state": "Assigned"
}
Important: Inventory assignment happens during playbook execution as a specific task, NOT when the provision button is clicked. This means:
- Risk of Double-Allocation: Between clicking "Provision" and inventory being assigned, another user could theoretically select the same inventory item
- Best Practice: For high-volume operations, implement inventory locking or use database transactions
- Rollback on Failure: If the playbook fails before inventory assignment, inventory remains unassigned and available for reuse
Why Not Assign Earlier?
Inventory isn't assigned when "Provision" is clicked because:
- Service ID Needed: The
service_iddoesn't exist until the service is created in the playbook - Rollback Simplicity: If provisioning fails early (e.g., OCS account creation fails), inventory doesn't need cleanup
- Flexibility: Playbook can decide not to assign inventory based on conditional logic
Handling Failed Provisions:
When a provision fails after inventory is assigned, the rescue block should release inventory:
rescue:
- name: Release inventory on failure
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ hostvars[inventory_hostname]['SIM Card'] }}"
method: PATCH
body:
{
"service_id": null,
"customer_id": null,
"item_state": "In Stock"
}
when: service_id is defined # Only if service was created
This ensures inventory isn't left in an "Assigned" state for a non-existent or failed service.
When Inventory List is Empty
If inventory_items_list: "[]" (empty list), the inventory picker is
skipped entirely and provisioning proceeds immediately. This is common
for:
- Digital products - Software licenses, VPN accounts
- Service addons - Data top-ups that don't need new hardware
- Virtual services - That don't consume trackable resources
Example: A "5GB Data Boost" addon has inventory_items_list: "[]"
because it just adds balance to an existing service without needing new
hardware.
Inventory Template Setup
Before using an inventory type in inventory_items_list, you must
create the Inventory Template:
- Navigate to Administration → Inventory → Templates
- Create template with exact name (e.g., "SIM Card")
- Define fields:
itemtext1_label: "ICCID"itemtext2_label: "IMSI"itemtext3_label: "PUK Code"
- Add inventory items of this type to stock
For complete details on creating and managing inventory templates, see Inventory Management .
Multiple Items of Same Type
While the inventory_items_list is an array, having duplicate types
(e.g., "['SIM Card', 'SIM Card']") is not recommended as it may
cause confusion in the UI and playbook variable naming.
For scenarios requiring multiple similar items:
Option 1: Create distinct inventory template names
# Dual-SIM phone service
inventory_items_list: "['Primary SIM Card', 'Secondary SIM Card', 'Mobile Number']"
Create separate templates: "Primary SIM Card" and "Secondary SIM Card" with same fields but different names.
Option 2: Use a single bundled inventory item
# Dual-SIM kit
inventory_items_list: "['Dual SIM Kit', 'Mobile Number']"
Where "Dual SIM Kit" inventory template has fields for both SIMs
(itemtext1: Primary ICCID, itemtext2: Secondary ICCID, etc.).
Common Inventory Scenarios
Mobile Service:
inventory_items_list: "['SIM Card', 'Mobile Number']"
- SIM Card: Physical or eSIM with ICCID/IMSI
- Mobile Number: Phone number (MSISDN)
Fixed Internet:
inventory_items_list: "['Modem Router', 'Static IP Address']"
- Modem Router: CPE device with MAC address
- Static IP Address: IPv4 from address pool
Fixed Wireless:
inventory_items_list: "['Fixed Wireless CPE', 'IPv4 Address', 'IPv6 Prefix']"
- CPE: Customer premises equipment (antenna, modem)
- IPv4: Public IP address
- IPv6 Prefix: /56 or /64 prefix
Note: Appointments and scheduling are not inventory items. Use separate scheduling/calendar systems for installation appointments.
VoIP Service:
inventory_items_list: "['DID Number']"
- DID Number: Direct Inward Dialing phone number
Note: SIP usernames, passwords, and account configurations are generated programmatically by the provisioning playbook, not selected from inventory.
GPON/Fiber:
inventory_items_list: "['ONT Device', 'GPON Port', 'IPv4 Address', 'Fiber Drop Cable']"
- ONT Device: Optical Network Terminal with serial number
- GPON Port: Specific port on OLT with fiber connection
- IPv4 Address: Public or private IP
- Fiber Drop Cable: Physical fiber cable from street to premises (tracked for asset management)
Equipment Rental:
inventory_items_list: "['Rental Modem']"
- Tracks which modem is with which customer
- Important for recovering equipment on cancellation
Why Inventory Requirements Matter
1. Prevent Double-Allocation
Without inventory tracking, you could accidentally:
- Assign same SIM card to two customers
- Allocate same IP address to multiple services
- Ship same equipment serial to different locations
Inventory picker ensures each item is assigned to exactly one service.
2. Audit Trail
Inventory assignment creates complete audit trail:
- Which SIM card is with which customer
- When was it assigned
- Which service is using which phone number
- Equipment history (who had it, when, for what service)
3. Resource Planning
Track inventory levels:
- Alert when SIM cards running low
- Reorder before stockout
- Plan technician schedules based on CPE availability
- Manage IP address space allocation
4. Cost Tracking
Link wholesale cost to specific item:
- Track cost of each SIM card
- Calculate equipment depreciation
- Identify lost or stolen items
- Accurate COGS (Cost of Goods Sold)
5. Deprovisioning
When service is cancelled, inventory can be:
- Released back to stock (SIM cards, modems)
- Retired (damaged equipment)
- Returned to vendor (rental equipment)
- Kept for grace period (phone numbers before release)
Troubleshooting Inventory Picker Issues
Problem: "No inventory available" message appears
Causes:
- No inventory items of required type exist in database
- All items are already "Assigned" to other services
- Items are marked as "Damaged" or "Out Of Service"
- Inventory template name doesn't match exactly (case-sensitive)
Solution:
- Verify inventory template exists: Administration → Inventory → Templates
- Check template name matches exactly (including spaces, case)
- Add inventory items of this type: Administration → Inventory → Add Item
- Verify items are in "New" or "In Stock" state
- Check items aren't already assigned (
service_idshould be NULL)
Problem: Inventory picker doesn't appear
Causes:
inventory_items_listis empty:"[]"inventory_items_listis NULL or not set- Product category is "addon" and inherits parent service inventory
Solution:
- If inventory is needed, set
inventory_items_list: "['Type1', 'Type2']" - Verify product definition saved correctly
- Check API response for product includes inventory_items_list
Problem: Playbook fails with "inventory not found"
Causes:
- Playbook references wrong variable name
- Inventory ID not passed correctly
- Inventory was deleted between selection and provisioning
Solution:
- Verify playbook uses correct variable:
hostvars[inventory_hostname]['SIM Card'] - Check variable is integer:
{{ hostvars[inventory_hostname]['SIM Card'] | int }} - Add error handling in playbook for missing inventory
See Inventory Management for complete details on creating templates, adding items, and managing stock levels.
Step 7: Define Features and Terms
Features and terms are customer-facing marketing and legal content that helps customers understand what they're buying and the obligations involved.
{
"features_list": "20GB High-Speed Data. Unlimited Calls & Texts. EU Roaming Included. No Contract. 30-Day Expiry",
"terms": "Credit expires after 30 days. Data, calls, and texts valid only within expiry period. Fair use policy applies. See website for full terms."
}
Purpose and Business Value
Features List - Marketing & Sales:
The features list serves multiple critical business functions:
- Product Differentiation - Helps customers quickly compare
products and choose the right one
- "Prepaid Mobile 20GB" vs "Prepaid Mobile 50GB" - features clearly show the difference
- Without features, customers only see price, missing value proposition
- Marketing Communication - Key selling points prominently
displayed
- "EU Roaming Included" attracts international travelers
- "No Contract" appeals to commitment-averse customers
- Features drive purchase decisions
- Customer Expectations - Sets clear expectations about what's
included
- Reduces support calls ("Does this include calls?" → clearly listed)
- Prevents misunderstandings and refund requests
- Builds trust through transparency
- Self-Service - Enables customers to self-select appropriate
products
- Customer reads features, understands offering, makes informed choice
- Reduces need for sales staff explanation
- Speeds up purchase process
- SEO and Discoverability - Features can be indexed for search
- Customer searches "unlimited calls mobile plan" → product appears
- Improves product catalog searchability
Terms and Conditions - Legal & Compliance:
Terms serve legal and operational purposes:
- Legal Protection - Protects business from disputes and liability
- "Credit expires after 30 days" - customer cannot demand refund at 31 days
- "Fair use policy applies" - prevents abuse (tethering entire office on mobile plan)
- Creates binding agreement
- Expectation Management - Prevents customer dissatisfaction
- "Valid only within expiry period" - customer knows usage deadline
- "Cannot be refunded" (for addons) - prevents fraudulent purchases
- Reduces chargebacks and complaints
- Regulatory Compliance - Meets legal requirements
- Consumer protection laws require clear terms
- Telecommunications regulations mandate disclosure
- GDPR/privacy terms can be referenced
- Operational Boundaries - Defines service scope and limitations
- "Subject to network coverage" - not liable for dead zones
- "Speed may vary" - manages expectations on "up to" speeds
- "Equipment must be returned" - ensures rental equipment recovery
- Audit Trail - Proves customer was informed
- Customer accepted terms at purchase
- System logs acceptance timestamp
- Defensible in disputes or legal proceedings
Real-World Example:
Customer buys "Unlimited Calls & Texts" plan, then uses it for telemarketing (10,000 calls/day). Without terms:
- Customer: "You said unlimited!"
- Provider: "We meant personal use..."
- Customer: "That's not what you advertised!"
- Result: Dispute, potential regulator complaint, brand damage
With terms: "Fair use policy applies. Service is for personal use only. Commercial use prohibited."
- Provider: Points to terms customer accepted
- Customer cannot claim ignorance
- Legal basis to suspend service
- Dispute resolved in provider's favor
Features List Format:
Understanding the correct format is critical because improper formatting breaks the UI display. Features might appear as one long string instead of bullet points, or not display at all.
The features_list field can be formatted in two ways:
Option 1: Period-Separated String (Recommended)
Features are separated by a period and space (". "). The UI splits on this delimiter and renders each feature as a bullet point.
Why this format?
- Simple to edit - just type features with periods between them
- No special characters to escape
- Works reliably across all UI components
- Easy to update without breaking JSON syntax
Correct vs Incorrect:
Option 2: JSON Array String
"['20GB High-Speed Data', 'Unlimited Calls & Texts', 'EU Roaming Included']"
The UI can also parse JSON arrays. Note this is a string containing JSON, not an actual JSON array in the database.
Why this format exists?
- Allows features with periods in them (e.g., "Up to 100Mbps. Subject to availability.")
- Programmatic generation from scripts/API is easier
- Imported from external product catalogs that use arrays
Important: This must be valid Python list syntax as a string. Single quotes around each item, double quotes around the whole string.
Which Format to Use?
- Period-separated - For manual product creation in UI (simpler, less error-prone)
- JSON array - For API/script-based product creation (more robust for complex features)
Both formats produce identical output in the UI - they just affect how you input the data.
Where Features Appear in the UI:
1. Product Catalog (Customer View)
When customers browse available products, features are displayed as bullet points on each product card:
2. Product Details Page
Clicking "View Details" shows full product information including:
- Product name and icon
- Pricing (monthly cost, setup cost)
- Full features list (bullet points)
- Terms and conditions (see below)
- Availability and eligibility
3. Provisioning Confirmation
During provisioning, features are shown for user to review before confirming:
Features: • 20GB High-Speed Data • Unlimited Calls & Texts • EU Roaming Included • No Contract • 30-Day Expiry
Cost: £15.00/month Setup: £0.00
[Cancel] [Confirm & Provision]
4. Service Details (After Provisioning)
After service is active, features are displayed on the service detail page for customer reference.
Terms and Conditions Format:
The terms field is plain text that can include newlines:
Where Terms Appear in the UI:
1. Product Details Page
Terms are displayed in a collapsed section that expands when clicked:
2. Order Confirmation
During provisioning, a checkbox requires user to accept terms:
[Provision] button disabled until checked
3. Invoices
Service terms may be included on invoices as footnotes for clarity.
Best Practices:
- Features: Keep concise (under 50 characters each), focus on key benefits
- Terms: Include critical legal requirements, expiration policies, fair use policies
- Both: Update when product changes to keep customers informed
Step 8: Link Ansible Provisioning Playbook
{
"provisioning_play": "play_local_mobile_sim",
"provisioning_json_vars": "{
\"days\": 30,
\"data_gb\": 20,
\"voice_minutes\": \"unlimited\",
\"sms_count\": \"unlimited\"
}"
}
provisioning_play- Name of Ansible playbook (without .yaml extension)provisioning_json_vars- Default variables passed to playbook- Playbook must exist at:
OmniCRM-API/Provisioners/plays/play_local_mobile_sim.yaml
Complete Product Definition
{
"product_name": "Prepaid Mobile 20GB",
"product_slug": "prepaid-mobile-20gb",
"category": "standalone",
"service_type": "mobile",
"enabled": true,
"icon": "fa-solid fa-sim-card",
"comment": "Prepaid mobile SIM with 20GB data, unlimited calls & texts",
"retail_cost": 15.00,
"wholesale_cost": 5.00,
"retail_setup_cost": 0.00,
"wholesale_setup_cost": 1.00,
"contract_days": 30,
"residential": true,
"business": false,
"customer_can_purchase": true,
"available_from": "2025-01-01T00:00:00Z",
"available_until": null,
"auto_renew": "prompt",
"allow_auto_renew": true,
"inventory_items_list": "['SIM Card', 'Mobile Number']",
"features_list": "[
'20GB High-Speed Data',
'Unlimited Calls & Texts',
'EU Roaming Included',
'No Contract',
'30-Day Expiry'
]",
"terms": "Credit expires after 30 days. Data, calls, and texts valid only within expiry period. Fair use policy applies.",
"provisioning_play": "play_local_mobile_sim",
"provisioning_json_vars": "{
\"days\": 30,
\"data_gb\": 20,
\"voice_minutes\": \"unlimited\",
\"sms_count\": \"unlimited\"
}"
}
Creating an Addon Product
Addons enhance or modify existing services. They come in two types: virtual addons (no physical resources) and hardware addons (require inventory).
Example 1: Virtual Addon (5GB Data Boost)
A digital addon that adds data to an existing mobile service:
{
"product_name": "5GB Data Boost",
"product_slug": "5gb-data-boost",
"category": "addon",
"service_type": "mobile",
"enabled": true,
"icon": "fa-solid fa-plus",
"comment": "Add 5GB extra data to existing mobile service",
"retail_cost": 5.00,
"wholesale_cost": 1.50,
"retail_setup_cost": 0.00,
"wholesale_setup_cost": 0.00,
"contract_days": 0,
"residential": true,
"business": true,
"customer_can_purchase": true,
"auto_renew": "false",
"allow_auto_renew": false,
"inventory_items_list": "[]",
"relies_on_list": "",
"features_list": "5GB High-Speed Data. Valid for 7 Days",
"terms": "Data expires after 7 days or when exhausted. Cannot be refunded.",
"provisioning_play": "play_topup_charge_then_action",
"provisioning_json_vars": "{
\"data_gb\": 5,
\"days\": 7
}"
}
Example 2: Hardware Addon (Modem Rental)
An addon that provides physical equipment for an existing fiber service:
{
"product_name": "WiFi 6 Modem Rental",
"product_slug": "wifi6-modem-rental",
"category": "addon",
"service_type": "internet",
"enabled": true,
"icon": "fa-solid fa-router",
"comment": "Add WiFi 6 modem to fiber service - rental",
"retail_cost": 10.00,
"wholesale_cost": 3.00,
"retail_setup_cost": 0.00,
"wholesale_setup_cost": 45.00,
"contract_days": 30,
"residential": true,
"business": true,
"customer_can_purchase": true,
"auto_renew": "true",
"allow_auto_renew": true,
"inventory_items_list": "['Rental Modem']",
"relies_on_list": "",
"features_list": "WiFi 6 (802.11ax). Dual-band 2.4GHz + 5GHz. Up to 40 devices. Parental controls",
"terms": "Equipment rental. Must be returned on service cancellation or £150 replacement fee applies. Equipment remains property of provider.",
"provisioning_play": "play_addon_assign_modem",
"provisioning_json_vars": "{
\"device_type\": \"modem_router\",
\"requires_configuration\": true
}"
}
Key Differences for Addons:
category: "addon"- Applied to existing service, not standalonecontract_days: 0(virtual) or30(recurring rental) - Billing frequencyinventory_items_list: "[]"(virtual) or"['Rental Modem']"(hardware) - Physical resourcesauto_renew: "false"(one-time) or"true"(rental) - Recurring behaviorrelies_on_list: ""- Empty means applies to any service of matchingservice_type
Why Hardware Addons Need Inventory:
Hardware addons require inventory_items_list because:
- Track Equipment - Know which modem is with which customer
- Prevent Stockouts - Can't provision addon if no modems in stock
- Recovery - When customer cancels, know which equipment to recover
- Cost Tracking - Link wholesale cost to specific serial number
- Depreciation - Track equipment value over rental period
- Warranty - Identify defective units by serial number
Addon Provisioning Flow with Inventory:
When a customer adds "WiFi 6 Modem Rental" to their fiber service:
- Addon Selected - Customer clicks "Add to Service"
- Inventory Picker Appears - Just like standalone services:
- Payment Processed - £10.00 monthly rental charged
- Modem Assigned - Inventory updated:
service_id: Linked to fiber servicecustomer_id: Linked to customeritem_state: "Assigned"
- Shipping Triggered - Fulfillment system notified to ship modem
- Installation - Customer receives modem, plugs into ONT
- Recurring Billing - £10/month charged until addon cancelled
Deprovisioning Hardware Addons:
When customer cancels modem rental:
- Cancellation Initiated - Customer clicks "Remove Addon"
- Return Process Started:
- Email sent with return instructions
- Prepaid shipping label generated
- 14-day grace period before penalty
- Equipment Returned:
- Inventory updated:
item_state= "In Stock" (after refurbishment) - Or
item_state= "Damaged" (if defective) - Linked to next customer once refurbished
- Inventory updated:
- No Return:
- After 14 days, £150 replacement fee charged
- Inventory marked:
item_state= "Lost" - Wholesale cost (£45) + replacement value recovered
Pricing for Addons:
Addons can be priced differently from standalone services:
- Virtual addons typically have no setup costs
- Hardware addons may have wholesale setup costs for equipment
- Recurring rental addons use
contract_daysfor billing frequency
Stage 2: The Provisioning Process
When a customer orders the "Prepaid Mobile 20GB" product, OmniCRM orchestrates provisioning through Ansible.
Provisioning Flow Diagram
Customer Orders → Inventory Selection → Provisioning Job Created ↓ ↓ Payment Authorized ← Variables Assembled ← Ansible Playbook Executed ↓ ↓ Service Record Created → OCS Account Setup → Inventory Assigned → Service Active
Step-by-Step Provisioning Flow
1. Customer Initiates Order
From customer page:
- Staff clicks "Add Service"
- Selects "Prepaid Mobile 20GB" from product carousel
- Product details and pricing displayed
2. Inventory Selection
System prompts for required inventory:
- SIM Card - Dropdown shows available SIM cards in stock
- Example: "SIM-00123 - ICCID: 8944..."
- Mobile Number - Dropdown shows available phone numbers
- Example: "+44 7700 900123"
Staff or customer selects items from available inventory.
3. Pricing Confirmation
System displays final pricing:
- Setup cost: £0.00 (free activation)
- Monthly cost: £15.00
- Due today: £15.00 (first month)
- Renewal date: 30 days from today
If auto-renew prompting enabled, customer chooses:
- Automatically renew this service every 30 days
4. Provision Button Clicked
When "Provision" is clicked, the API:
- Creates
Provisionrecord with status "Running" (status=1) - Merges variables from product + request + inventory selections
- Spawns background thread to execute Ansible playbook
- Returns provision_id to UI for status tracking
5. Variables Assembled
System merges variables from multiple sources:
From Product:
{
"days": 30,
"data_gb": 20,
"voice_minutes": "unlimited",
"sms_count": "unlimited"
}
From Request:
{
"product_id": 42,
"customer_id": 123,
"SIM Card": 5001,
"Mobile Number": 5002
}
System-Added:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"initiating_user": 7
}
Final Variables Passed to Ansible:
{
"product_id": 42,
"customer_id": 123,
"SIM Card": 5001,
"Mobile Number": 5002,
"days": 30,
"data_gb": 20,
"voice_minutes": "unlimited",
"sms_count": "unlimited",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"initiating_user": 7
}
6. Ansible Playbook Execution
The playbook play_local_mobile_sim.yaml executes with these variables.
Understanding the Ansible Provisioning Playbook
Let's examine a real provisioning playbook to understand what happens behind the scenes.
Mobile SIM Provisioning Playbook Example
Location:
OmniCRM-API/Provisioners/plays/play_local_mobile_sim.yaml
High-Level Structure:
- name: Mobile SIM Provisioning
hosts: localhost
gather_facts: no
become: False
tasks:
- name: Main block
block:
# 1. Load configuration
# 2. Fetch product details from API
# 3. Fetch customer details from API
# 4. Fetch inventory details from API
# 5. Create account in OCS (CGRateS)
# 6. Add balances and allowances to OCS
# 7. Create service record in CRM
# 8. Assign inventory to service
# 9. Record transactions
# 10. Send welcome notifications
rescue:
# Rollback on failure
# - Remove OCS account
# - Release inventory
# - Log error
Detailed Playbook Walkthrough:
Task 1: Load Configuration
- name: Include vars of crm_config
ansible.builtin.include_vars:
file: "../../crm_config.yaml"
name: crm_config
Loads system configuration including:
- OCS/CGRateS URL and credentials
- CRM base URL
- Tenant configuration
Task 2: Fetch Product Details
- name: Get Product information from CRM API
uri:
url: "{{ crm_config.crm.base_url }}/crm/product/product_id/{{ product_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_product
What This Does:
- Calls
GET /crm/product/product_id/42 - Retrieves complete product definition
- Stores in
api_response_productvariable
Why: Even though we have provisioning_json_vars from the product,
we fetch the full product to get:
- Latest pricing (may have changed since order started)
- Product name for service naming
- Features list for documentation
- Wholesale costs for margin tracking
Task 3: Set Package Facts
- name: Set package facts
set_fact:
package_name: "{{ api_response_product.json.product_name }}"
monthly_cost: "{{ api_response_product.json.retail_cost }}"
setup_cost: "{{ api_response_product.json.retail_setup_cost }}"
Extracts commonly-used values into simple variables for readability.
Task 4: Fetch Inventory Details
- name: Get SIM information from CRM API
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ hostvars[inventory_hostname]['SIM Card'] }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
register: api_response_sim
- name: Set IMSI from Inventory response
set_fact:
imsi: "{{ api_response_sim.json.itemtext2 }}"
iccid: "{{ api_response_sim.json.itemtext1 }}"
What This Does:
- Looks up SIM Card inventory ID 5001
- Retrieves SIM details:
itemtext1= ICCID (SIM card number)itemtext2= IMSI (subscriber identity)
- Does same for Mobile Number inventory (retrieves phone number)
Why This Matters:
- IMSI is needed to provision subscriber in HSS (Home Subscriber Server)
- ICCID is recorded in service notes for troubleshooting
- Phone number (MSISDN) is displayed to customer and used for routing
Task 5: Generate Service UUID
- name: Generate UUID Fact
set_fact:
uuid: "{{ 99999999 | random | to_uuid }}"
- name: Set Service UUID
set_fact:
service_uuid: "Local_Mobile_SIM_{{ uuid[0:8] }}"
What This Does:
- Generates random UUID
- Creates service_uuid like
Local_Mobile_SIM_a3f2c1d8
Why:
- Service UUID is the unique identifier in OCS/CGRateS
- Used for all charging operations
- Must be globally unique across all services
Task 6: Create OCS Account
- name: Create account in OCS
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
headers:
Content-Type: "application/json"
body:
{
"method": "ApierV2.SetAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionPlanIds": [],
"ExtraOptions": {
"AllowNegative": false,
"Disabled": false
},
"ReloadScheduler": true
}]
}
register: ocs_create_response
What This Does:
- Calls CGRateS JSON-RPC API
- Creates new account with service_uuid
- Sets account to active (not disabled)
- Prevents negative balance (prepaid mode)
Why:
- OCS account is where all charging happens
- Balances (data, voice, SMS, money) are stored here
- Usage is tracked and rated in real-time
Task 7: Add Data Balance
- name: Add 20GB Data Balance
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Balance": {
"ID": "DATA_20GB_Monthly",
"Value": 21474836480,
"ExpiryTime": "+720h",
"Weight": 10,
"DestinationIDs": "*any"
}
}]
}
What This Does:
- Adds 20GB data balance to account
- Value: 21474836480 bytes (20 * 1024 * 1024 * 1024)
- Expires in 720 hours (30 days)
- Weight 10 (higher weight consumed first)
Task 8: Add Unlimited Voice & SMS
- name: Add Unlimited Voice
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Account": "{{ service_uuid }}",
"BalanceType": "*voice",
"Balance": {
"ID": "VOICE_Unlimited",
"Value": 999999999,
"ExpiryTime": "+720h"
}
}]
}
- Adds 999,999,999 seconds of voice (essentially unlimited)
- Expires in 30 days
Task 9: Create Service Record in CRM
- name: Add Service via API
uri:
url: "{{ crm_config.crm.base_url }}/crm/service/"
method: PUT
body_format: json
headers:
Authorization: "Bearer {{ access_token }}"
body:
{
"customer_id": "{{ customer_id }}",
"product_id": "{{ product_id }}",
"service_name": "Mobile - {{ phone_number }}",
"service_type": "mobile",
"service_uuid": "{{ service_uuid }}",
"service_status": "Active",
"service_provisioned_date": "{{ provision_datetime }}",
"retail_cost": "{{ monthly_cost }}",
"wholesale_cost": "{{ api_response_product.json.wholesale_cost }}",
"icon": "fa-solid fa-sim-card"
}
register: service_creation_response
What This Creates:
- Service record linked to customer
- Links to OCS via
service_uuid - Stores retail and wholesale costs
- Sets status to "Active"
- Returns
service_idfor subsequent operations
Task 10: Assign Inventory to Service
- name: Assign SIM Card to Service
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ hostvars[inventory_hostname]['SIM Card'] }}"
method: PATCH
body_format: json
headers:
Authorization: "Bearer {{ access_token }}"
body:
{
"service_id": "{{ service_creation_response.json.service_id }}",
"customer_id": "{{ customer_id }}",
"item_state": "Assigned"
}
What This Does:
- Updates SIM Card inventory record
- Sets
service_idto link SIM to service - Changes state from "In Stock" to "Assigned"
- Repeats for Mobile Number inventory
Why:
- Tracks which SIM is assigned to which customer
- Prevents double-allocation of inventory
- Enables inventory reporting and auditing
Task 11: Record Setup Cost Transaction
- name: Add Setup Cost Transaction
uri:
url: "{{ crm_config.crm.base_url }}/crm/transaction/"
method: PUT
body_format: json
headers:
Authorization: "Bearer {{ access_token }}"
body:
{
"customer_id": "{{ customer_id }}",
"service_id": "{{ service_creation_response.json.service_id }}",
"title": "{{ package_name }} - Setup",
"description": "Activation fee",
"retail_cost": "{{ setup_cost }}",
"wholesale_cost": "{{ api_response_product.json.wholesale_setup_cost }}"
}
What This Does:
- Records £0.00 setup charge to customer (retail)
- Records £1.00 wholesale cost
- Creates transaction record for invoicing
Task 12: Rescue Block (Error Handling)
rescue:
- name: Remove account in OCS on failure
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body:
{
"method": "ApierV2.RemoveAccount",
"params": [{
"Account": "{{ service_uuid }}"
}]
}
- name: Fail the provisioning
fail:
msg: "Provisioning failed, rolled back OCS account"
What This Does:
- If any task fails, rescue block executes
- Deletes OCS account that was partially created
- Releases inventory back to "In Stock"
- Fails the provision job with error message
Why:
- Prevents orphaned accounts in OCS
- Ensures clean rollback on errors
- Maintains data consistency
Provisioning Complete: What Was Created
After successful provisioning, the system has:
1. OCS Account (CGRateS):
- Account ID:
Local_Mobile_SIM_a3f2c1d8 - Balances:
- 20GB data (expires in 30 days)
- Unlimited voice (999M seconds, expires in 30 days)
- Unlimited SMS (999M messages, expires in 30 days)
2. CRM Service Record:
- Service ID: 1234
- Customer: John Doe (customer_id: 123)
- Product: Prepaid Mobile 20GB (product_id: 42)
- Service Name: "Mobile - +44 7700 900123"
- Service UUID:
Local_Mobile_SIM_a3f2c1d8 - Status: Active
- Monthly Cost: £15.00 (retail), £5.00 (wholesale)
- Profit: £10.00/month
3. Inventory Assignments:
- SIM Card 5001: Assigned to service 1234, customer 123
- Mobile Number 5002: Assigned to service 1234, customer 123
4. Transaction Records:
- Setup cost transaction created
- First month charge recorded
5. Customer Can Now:
- View service in self-care portal
- See 20GB data balance
- Make calls and send SMS
- Top up or add addons
- View usage in real-time
Stage 3: Adding Addons and Topups
After a service is active, customers can purchase addons to enhance their service.
Addon Provisioning Flow
Let's say customer has used 18GB of their 20GB allowance and wants to buy the "5GB Data Boost" addon.
1. Customer Navigates to Service
- Opens "Mobile - +44 7700 900123" service page
- Sees current usage: 18GB of 20GB used (90%)
- Clicks "Add Addon" or "Top Up"
2. System Filters Available Addons
Only shows addons where:
category = "addon"service_type = "mobile"(matches service type)residential = true(if customer is residential)enabled = true
Customer sees: "5GB Data Boost - £5.00"
3. Customer Selects Addon
- Clicks "5GB Data Boost"
- Confirms purchase for £5.00
- System captures payment authorization
4. Addon Provisioning Initiated
System calls play_topup_charge_then_action.yaml with variables:
{
"product_id": 43, # 5GB Data Boost product
"customer_id": 123,
"service_id": 1234, # Existing service
"access_token": "eyJ...",
"data_gb": 5, # From provisioning_json_vars
"days": 7 # From provisioning_json_vars
}
Key Difference from Standalone:
service_idis included (existing service to modify)- No inventory required
- No service creation (modifies existing)
Addon Provisioning Playbook Walkthrough
Task 1: Fetch Service Details
- name: Get Service information from CRM API
uri:
url: "http://localhost:5000/crm/service/service_id/{{ service_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
register: api_response_service
- name: Set service facts
set_fact:
service_uuid: "{{ api_response_service.json.service_uuid }}"
customer_id: "{{ api_response_service.json.customer_id }}"
Why:
- Need
service_uuidto add balance to correct OCS account - Verifies service exists and is active
- Ensures service belongs to the customer
Task 2: Charge Customer
- name: Get Customer's Default Payment Method
uri:
url: "http://localhost:5000/crm/stripe/customer_id/{{ customer_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
register: api_response_stripe
- name: Get default card ID
set_fact:
customer_stripe_id: "{{ api_response_stripe.json | json_query(query) }}"
vars:
query: "data[?default_payment_method==`true`].customer_stripe_id | [0]"
- name: Charge card
uri:
url: "http://localhost:5000/crm/stripe/charge_card/{{ customer_stripe_id }}"
method: POST
body_format: json
headers:
Authorization: "Bearer {{ access_token }}"
body:
{
"retail_cost": 5.00,
"description": "5GB Data Boost",
"customer_id": "{{ customer_id }}",
"service_id": "{{ service_id }}",
"product_id": "{{ product_id }}",
"wholesale_cost": 1.50,
"invoice": true
}
register: charge_response
- name: Assert payment successful
assert:
that:
- charge_response.status == 200
What This Does:
- Finds customer's default Stripe payment method
- Charges £5.00 to the card
- Records wholesale cost £1.50 for margin tracking
- Creates transaction linked to service
- Adds to next invoice
- Fails provision if payment fails
Why Charge First:
- No credit delivered until payment confirmed
- Prevents fraud
- Matches payment to addon provision
Task 3: Add Data Balance to OCS
- name: Add 5GB Data Balance
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Balance": {
"ID": "DATA_5GB_Boost_{{ uuid }}",
"Value": 5368709120,
"ExpiryTime": "+168h",
"Weight": 20
}
}]
}
What This Does:
- Adds 5GB (5368709120 bytes) to account
- Expires in 168 hours (7 days)
- Weight 20 (higher weight consumed first - boost before monthly allowance)
Customer Balance After Addon:
- Original monthly: 2GB remaining (expires in 25 days)
- New boost: 5GB (expires in 7 days)
- Total available: 7GB
- Usage order: Boost consumed first, then monthly
Task 4: Record Transaction
- name: Add Addon Transaction
uri:
url: "http://localhost:5000/crm/transaction/"
method: PUT
body_format: json
headers:
Authorization: "Bearer {{ access_token }}"
body:
{
"customer_id": "{{ customer_id }}",
"service_id": "{{ service_id }}",
"title": "5GB Data Boost",
"description": "Additional 5GB data valid for 7 days",
"retail_cost": 5.00,
"wholesale_cost": 1.50
}
What This Does:
- Records £5.00 charge to customer
- Records £1.50 wholesale cost
- Links transaction to service for reporting
Complete Addon Flow Summary
- Customer selects addon from filtered list
- Payment authorized and charged
- Data balance added to OCS account
- Transaction recorded in CRM
- Customer immediately sees updated balance: 7GB available
Financial Tracking:
- Service monthly charge: £15 retail, £5 wholesale
- Addon purchase: £5 retail, £1.50 wholesale
Auto-Renewal: Recurring Addons
Some addons can be set to auto-renew (monthly data plans, subscriptions, etc).
Product Configuration:
{
"product_name": "Monthly 10GB Data Plan",
"category": "addon",
"retail_cost": 10.00,
"contract_days": 30,
"auto_renew": "true",
"provisioning_play": "play_topup_charge_then_action"
}
Provisioning Creates ActionPlan:
- name: Create ActionPlan for Auto-Renewal
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body:
{
"method": "ApierV1.SetActionPlan",
"params": [{
"Id": "ServiceID_{{ service_uuid }}__ProductID_{{ product_id }}__MonthlyRenewal",
"ActionPlan": [{
"ActionsId": "Action_{{ product_slug }}",
"Years": "*any",
"Months": "*any",
"MonthDays": "*any",
"WeekDays": "*any",
"Time": "00:00:00",
"Weight": 10
}],
"Overwrite": false,
"ReloadScheduler": true
}]
}
What This Does:
- Creates scheduled task in OCS
- Executes
Action_{{ product_slug }}every 30 days - Action charges customer and re-applies data balance
- Continues until customer cancels
Customer Management:
- Customer sees "Next Renewal: Feb 1, 2025 - £10.00" in service view
- Can click "Cancel Auto-Renewal" to stop future charges
- Can click "Renew Now" to immediately apply next month's allowance
Stage 4: Deprovisioning Services
When a customer cancels service, the system must cleanly remove all resources.
Deprovisioning Triggers
Deprovisioning can be triggered by:
- Customer cancellation - Customer clicks "Cancel Service"
- Administrative action - Staff marks service for deactivation
- Non-payment - Service expires due to lack of renewal
- Contract end - Fixed-term contract reaches end date
Deprovisioning Flow
1. Customer Initiates Cancellation
- Navigates to service
- Clicks "Cancel Service"
- System prompts: "Are you sure? Any remaining balance will be forfeited."
- Customer confirms
2. Grace Period (Optional)
Some operators implement grace period:
- Service marked "Pending Cancellation"
- Remains active for 7-30 days
- Customer can reverse cancellation during grace period
- Automatic deprovisioning after grace period
3. Deprovisioning Job Created
System creates provision job with:
{
"action": "deprovision",
"service_id": 1234,
"customer_id": 123,
"service_uuid": "Local_Mobile_SIM_a3f2c1d8"
}
Calls playbook specified in service.deprovisioning_play or rescue
block of original playbook.
4. Ansible Deprovision Playbook
- name: Deprovision Mobile Service
hosts: localhost
tasks:
- name: Disable OCS Account
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body:
{
"method": "ApierV2.SetAccount",
"params": [{
"Account": "{{ service_uuid }}",
"ExtraOptions": { "Disabled": true }
}]
}
- name: Remove ActionPlans (stop auto-renewals)
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body:
{
"method": "ApierV1.RemoveActionPlan",
"params": [{
"Id": "ServiceID_{{ service_uuid }}__*"
}]
}
- name: Update Service Status in CRM
uri:
url: "http://localhost:5000/crm/service/{{ service_id }}"
method: PATCH
body:
{
"service_status": "Deactivated",
"service_deactivate_date": "{{ current_datetime }}"
}
- name: Release Inventory to Stock
uri:
url: "http://localhost:5000/crm/inventory/inventory_id/{{ sim_card_id }}"
method: PATCH
body:
{
"service_id": null,
"customer_id": null,
"item_state": "Decommissioned"
}
What This Does:
- Disables OCS account - Stops all charging, usage blocked
- Removes ActionPlans - Cancels auto-renewals
- Updates CRM service - Status "Deactivated", date recorded
- Releases inventory - SIM marked "Decommissioned", available for reuse (after refurbishment)
5. Post-Deprovisioning
System performs cleanup:
- Customer no longer sees service in self-care portal
- Service remains in CRM for historical reporting
- Transactions and invoices preserved for accounting
- Inventory can be refurbished and reused
- OCS account can be archived after retention period
Partial vs Full Deprovisioning
Partial Deprovisioning (Suspension):
- Used for non-payment or temporary suspension
- OCS account disabled but not deleted
- Balances preserved
- Can be re-enabled when payment received
- name: Suspend Service
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body:
{
"method": "ApierV2.SetAccount",
"params": [{
"Account": "{{ service_uuid }}",
"ExtraOptions": { "Disabled": true }
}]
}
Full Deprovisioning (Permanent Cancellation):
- Used for permanent cancellation
- OCS account deleted entirely
- Balances forfeit
- Cannot be re-enabled
- name: Remove OCS Account
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body:
{
"method": "ApierV2.RemoveAccount",
"params": [{
"Account": "{{ service_uuid }}"
}]
}
Best Practices for Product Management
Product Lifecycle Management
Product States:
enabled: true- Product available for new ordersenabled: false- Product disabled, existing services continue
Disabling Products:
- Mark product as
enabled: falseto prevent new orders - Existing services remain active
- Customers can still renew/modify existing services
- Useful for sunsetting old products
Inventory Management
Inventory States:
New- Fresh stock, ready to assignIn Stock- Available for provisioningAssigned- Linked to customer serviceDecommissioned- Can be refurbished and reusedDamaged- Needs repair or disposal
Reusing Inventory:
After deprovisioning:
- SIM cards: Refurbish and mark "In Stock"
- Phone numbers: Release after porting period (30 days)
- Equipment: Test, refurbish, mark "Used"
Provisioning Metrics
Monitor:
- Provisioning success rate
- Average provisioning time
- Common failure points
- Inventory turnover