analytics
title: OneBalance Analytics Service
description: Usage analytics, cost tracking, profit calculation, and transaction logs for OneBalance credit systemtitle: OneBalance Analytics Service
tags: [onebalance, analytics, credits, reporting, profit]
---description: Usage analytics, profit tracking, and log retrieval for OneBalance consumption with 3-month comparisons and linked document details## Overview
๐ Overviewtags: [onebalance, analytics, logs, profit, reseller, reporting]โ
internal/api/v1/onebalance/services/analytics.js provides analytics and reporting for OneBalance usage. Tracks spending by event type (SMS, email, InstaSites, etc.), calculates profit margins for parent accounts with sub-accounts, and generates transaction logs with currency conversion.---The Analytics Controller provides comprehensive usage analytics and detailed logging for all OneBalance events. It tracks spending trends, generates usage reports, provides transaction logs, and calculates profit for resellers (main accounts with sub-accounts).
File Path: internal/api/v1/onebalance/services/analytics.js
๐๏ธ Collections Used## ๐ OverviewController Path: internal/api/v1/onebalance/controllers/analytics.jsโ
๐ Full Schema: See Database Collections DocumentationService Path:
internal/api/v1/onebalance/services/analytics.js
onebalance.usage_logs``internal/api/v1/onebalance/services/analytics.js provides analytics, logging, and profit tracking for OneBalance consumption events. Generates monthly usage reports with 3-month comparisons, paginated log retrieval with filtering, detailed log views with linked documents, and reseller profit calculations.โ
-
Operations: Read for analytics aggregation
-
Model:
shared/models/onebalance-usage_logs.js--- -
Usage Context: All OneBalance transactions (debit/credit)
File Path: internal/api/v1/onebalance/services/analytics.js
communicationsโ
-
Operations: Read for SMS/email details## API Endpoints
-
Model:
shared/models/communication.js -
Usage Context: Link logs to sent messages## ๐๏ธ Collections Used
instasites### 1. GET /v1/onebalance/analytics - Usage Analyticsโ
-
Operations: Read for InstaSite details
-
Model:
shared/models/instasites.js> ๐ Full Schema: See Database Collections Documentation -
Usage Context: Link logs to generated sites
Purpose: Retrieves usage analytics with month-over-month comparison for all OneBalance events.
instareportsโ
-
Operations: Read for InstaReport details###
onebalance.usage_logs -
Model:
shared/models/instareports.js -
Usage Context: Link logs to generated reportsAuthentication: Required (JWT)
_accounts- Operations: Read (aggregation pipelines) for analytics and log retrievalโ
-
Operations: Read for account details
-
Model:
shared/models/account.js- Model:shared/models/onebalance-usage_logs.jsRequest Example: -
Usage Context: Currency conversion and validation
-
Usage Context: Primary source for all analytics - stores event type, cost, status, timestamps
๐ฏ API Endpointsโ
### `GET /v1/onebalance/analytics`
### `_accounts`GET /v1/onebalance/analytics HTTP/1.1
**Purpose**: Get monthly analytics with current/previous month comparisons
Authorization: Bearer <token>
**Response**:
```javascript- **Operations**: Read for sub-account discovery and filtering```
\{
success: true,- **Model**: `shared/models/account.js`
message: "SUCCESS",
data: \{- **Usage Context**: Find all sub-accounts for main account analytics aggregation**Response**:
this_month: \{
sms: \{ total: 150.50, used: 1200 \},
email: \{ total: 45.30, used: 850 \},
instasite: \{ total: 200.00, used: 10 \}### `communications````javascript
\},
last_month: \{{
sms: \{ total: 130.25, used: 1000 \},
email: \{ total: 40.10, used: 750 \}- **Operations**: Read for SMS/email log details "success": true,
\},
previous_month: \{- **Model**: `shared/models/communication.js` "message": "SUCCESS",
sms: \{ total: 125.00, used: 950 \}
\}- **Usage Context**: Linked document retrieval for communication events "data": {
\}
\} "this_month": {
```
### `instasites` "sms": 45.30, // Total spent on SMS this month
---
"email": 12.50, // Total spent on Email this month
### `GET /v1/onebalance/analytics/logs`
- **Operations**: Read for website generation details "instasite": 0, // Instasites (credit-based, not shown for sub-accounts)
**Purpose**: Retrieve paginated transaction logs with filtering
- **Model**: `shared/models/instasite.js` "instareport": 75.00, // Total spent on Instareports
**Query Parameters**:
```javascript- **Usage Context**: Linked document retrieval for instasite events "listing": 100.00, // Total spent on Listings
\{
page: Number, // Page number "phone": 23.00 // Total spent on Phone numbers
limit: Number, // Items per page (default: 15)
sortField: String, // "created_at", "updated_at", "event", "cost"### `instareports` },
sortOrder: String, // "asc" or "desc"
filters: \{ "last_month": {
startDate: Date, // Filter start date
endDate: Date, // Filter end date- **Operations**: Read for report generation details "sms": 38.20,
account_id: String, // Filter by account
event: [String] // Filter by event types- **Model**: `shared/models/instareport.js` "email": 10.30,
\}
\}- **Usage Context**: Linked document retrieval for instareport events "instasite": 0,
```
"instareport": 62.50,
**Response**:
````javascript## ๐ Data Flow "listing": 85.00,
\{
success: true, "phone": 23.00
message: "SUCCESS",
data: [\{```mermaid },
_id: ObjectId,
account_id: ObjectId,sequenceDiagram "previous_month": { // Month before last month
event: "sms", // Event type
cost: 0.05, // Cost in dollars participant Client "sms": 42.10,
status: "success", // "success" | "failed" | "on_hold"
type: "debit", // "debit" | "credit" participant Service "email": 11.00,
additional_info: \{\}, // Event-specific metadata
created_at: Date, participant DB as Usage Logs "instasite": 0,
updated_at: Date
\}], participant Accounts as Account DB "instareport": 58.00,
pagination: \{
total: 1500, participant Currency as Currency Util "listing": 90.00,
page: 1,
pages: 100, "phone": 23.00
limit: 15
\} Client->>Service: analytics(account_id) }
\}
``` Service->>Accounts: Get sub-accounts }
--- Service->>DB: Aggregate logs (3 months)}
### `GET /v1/onebalance/analytics/logs/:id` ```
**Purpose**: Get detailed information for a single transaction log Note over DB: $facet pipeline<br/>this_month, last_month, previous_month
**Response**: **Response Structure**:
```javascript
\{ DB-->>Service: Raw event counts & costs
success: true,
message: "SUCCESS", Service->>Service: Convert cents โ dollars| Period | Description | Date Range |
data: \{
_id: ObjectId, Service->>Currency: Convert USD โ account currency| ---------------- | ----------------------- | -------------------------------------------------- |
account_id: ObjectId,
event: "sms", Service-->>Client: { this_month, last_month, previous_month }| `this_month` | Current calendar month | 1st of current month โ today |
cost: 0.05,
status: "success", | `last_month` | Previous calendar month | 1st of last month โ last day of last month |
type: "debit",
created_at: Date, Note over Client,Service: Pagination Flow| `previous_month` | Month before last month | 1st of previous month โ last day of previous month |
communication: \{ // Populated if SMS/email
_id: ObjectId, Client->>Service: logs(page, limit, filters)
to: "+15551234567",
from: "+15559876543", Service->>Accounts: Get sub-accounts (if main)**Event Types** (if used):
message: "SMS content",
status: "delivered" Service->>DB: Find logs with filters
\}
// OR Service->>Currency: Convert amounts| Event | Description | Shown For |
instasite: \{ // Populated if InstaSite
_id: ObjectId, Service-->>Client: { data: [...], pagination: {...} }| ------------- | ------------------ | ------------------ |
name: "Site Name",
url: "https://..."```| `sms` | SMS messaging | All accounts |
\}
\}| `email` | Email sending | All accounts |
\}
```## ๐ง Business Logic & Functions| `instasite` | Website generation | Main accounts only |
---| `instareport` | Report generation | Main accounts only |
### `GET /v1/onebalance/analytics/profit`### Analytics & Reporting| `listing` | Directory listings | All accounts |
**Purpose**: Calculate profit margins for parent accounts (main accounts with sub-accounts)| `phone` | Phone numbers | All accounts |
**Query Parameters**:---
```javascript
\{**Analytics Aggregation**:
account_id: String // Optional: specific sub-account ID
\}#### `analytics({ account_id, currency, main })`
````
````mermaid
**Response**:
```javascript**Purpose**: Generates monthly usage analytics with 3-month comparison (this month, last month, previous month). Aggregates by event type with total costs and usage counts.flowchart TD
\{
success: true, A[Get Date Ranges] --> B[Query Usage Logs]
message: "SUCCESS",
data: \{**Parameters**: B --> C{Group by Event & Month}
sms: \{
cost: 150.00, // What main account paid C --> D[This Month Stats]
sub_account_cost: 180.00, // What sub-accounts were charged
profit: 30.00 // Markup profit- `account_id` (ObjectId) - Account identifier C --> E[Last Month Stats]
\},
email: \{- `currency` (String, optional) - Target currency for display C --> F[Previous Month Stats]
cost: 45.00,
sub_account_cost: 54.00,- `main` (Boolean) - Whether account is main account (includes sub-account data)
profit: 9.00
\}, D --> G[Sum Costs per Event]
total_profit: 39.00
\}**Returns**: `Promise<Object>` - Three-month analytics: E --> G
\}
``` F --> G
---```javascript
## ๐ง Service Methods{ G --> H{Main Account?}
### `analytics({ account_id, currency, main })` this_month: { H -->|Yes| I[Include All Events]
**Purpose**: Generate monthly analytics with comparisons sms: { used: 150, cost: 1.125 }, H -->|No| J[Exclude Instasite/Instareport]
**Logic**: email: { used: 300, cost: 3.00 },
1. Calculate date ranges for this/last/previous months
2. Aggregate logs by event type for each period instasite: { used: 12, cost: 60.00 }, I --> K[Apply Currency Conversion]
3. Convert costs to requested currency
4. Format results with totals and usage counts instareport: { used: 25, cost: 125.00 }, J --> K
**Aggregation Pipeline**: listing: { used: 5, cost: 250.00 },
```javascript
[\{ lighting_domain: { used: 3, cost: 30.00 }, K --> L[Fill Missing Events with 0]
$facet: \{
this_month: [\{ phone: { used: 2, cost: 2.00 }, L --> M[Sort by Total Descending]
$match: \{ created_at: \{ $gte: first_day, $lte: last_day \} \}
\}, \{ inbound_call: { used: 45, cost: 4.50 }, M --> N[Return Analytics]
$group: \{
_id: '$event', outbound_call: { used: 30, cost: 9.00 }```
total: \{ $sum: '$cost' \},
used: \{ $sum: 1 \} },
\}
\}], last_month: { /* same structure */ },**Data Calculation**:
last_month: [...],
previous_month: [...] previous_month: { /* same structure */ }
\}
\}]}```javascript
````
````// Date ranges
---
const firstDayOfMonth = new Date(2024, 9, 1); // Oct 1, 2024
### `logs({ account_id, filters, sortField, sortOrder, page, limit, currency, main })`
**Business Logic Flow**:const firstDayOfLastMonth = new Date(2024, 8, 1); // Sep 1, 2024
**Purpose**: Retrieve paginated and filtered transaction logs
const lastDayOfLastMonth = new Date(2024, 8, 30, 23, 59, 59, 999); // Sep 30, 2024 11:59:59 PM
**Filtering**:
- Date range filtering1. **Calculate Date Ranges**
- Event type filtering (SMS, email, etc.)
- Status filtering - `first_day_of_month`: Start of current month// Aggregation pipeline
- Account-specific filtering
- `last_day_of_last_month`: End of last month[
**Sorting**:
- Default: `created_at` descending - `first_day_of_last_month`: Start of last month {
- Supports: `created_at`, `updated_at`, `event`, `cost`
- `last_day_of_previous_month`: End of month before last $match: {
**Currency Conversion**:
All costs converted from USD (stored) to requested currency - `first_day_of_previous_month`: Start of month before last account_id: ObjectId("..."),
--- event: { $ne: 'refill' }, // Exclude refills (not usage)
### `log(id, currency)`2. **Build Account Filter** (if main account) status: 'success' // Only successful transactions
**Purpose**: Get detailed single log with related entities - Query all accounts where `parent_account = account_id` }
**Population Logic**: - Extract sub-account IDs },
```javascript
if (log.event === 'sms' || log.event === 'email') \{ - Match: `account_id IN [main_account_id, ...sub_account_ids]` {
const communication = await Communication.findById(log.additional_info.communication_id);
log.communication = communication; $facet: {
\}
3. **Build Sub-Account Filter** this_month: [
if (log.event === 'instasite') \{
const instasite = await Instasite.findById(log.additional_info.site_id); - Sub-accounts excluded from 'instasite' and 'instareport' events { $match: { created_at: { $gte: firstDayOfMonth, $lte: now } } },
log.instasite = instasite;
\} - Match: `event NOT IN ['instasite', 'instareport'] OR account_id = main_account_id` { $group: { _id: '$event', total: { $sum: '$cost' } } }
````
],
---
4. **Execute Aggregation Pipeline** last_month: [
### `profit({ account_id, sub_account_id, currency })`
- Base match: `event != 'refill' AND status = 'success'` { $match: { created_at: { $gte: firstDayOfLastMonth, $lte: lastDayOfLastMonth } } },
**Purpose**: Calculate profit from sub-account markup
- Use `$facet` for parallel queries: { $group: { _id: '$event', total: { $sum: '$cost' } } }
**Requirements**:
- Only available for main accounts - **this_month**: `created_at >= first_day_of_month` ],
- Requires sub-accounts with OneBalance transactions
- **last_month**: `created_at between first_day_of_last_month and last_day_of_last_month` previous_month: [...]
**Aggregation**:
```javascript - **previous_month**: `created_at between first_day_of_previous_month and last_day_of_previous_month` }
const match = \{
account_id: mainAccountId, - Each facet groups by `event` and sums: `used: $sum: 1`, `cost: $sum: '$cost'` }
sub_account_id: \{ $exists: true \},
status: 'success']
\};
5. **Process Results**```
[\{
$match: match - For each month (this_month, last_month, previous_month):
\}, \{
$group: \{ - Call `getObj()` to format results**Currency Conversion**:
_id: '$event',
cost: \{ $sum: '$cost' \}, // Main account cost - Convert costs: cents โ dollars โ target currency
sub_account_cost: \{ $sum: '$sub_account_cost' \} // Sub charged
\} - Fill missing events with `{ used: 0, cost: 0 }`All costs converted from cents to dollars and from USD to account currency:
\}, \{
$set: \{
profit: \{ $subtract: ['$sub_account_cost', '$cost'] \}
\}6. **Return Analytics Object**```javascript
\}]
``````// Stored in DB: 4530 cents = $45.30
---**Key Business Rules**:// If account currency is EUR, convert $45.30 โ โฌ41.20
## ๐ Data Flow`````
```mermaid- Only successful events counted (`status: 'success'`)
sequenceDiagram
participant Client- Refills excluded from analytics (`event != 'refill'`)**Null Event Handling**:
participant Analytics
participant OnebalanceLogs- Main accounts see aggregated data (own + sub-accounts)
participant Currency
participant Related- Sub-accounts only see own dataIf an event has no usage in a period, it's added with `0` value:
Client->>Analytics: GET /analytics/logs- Sub-accounts excluded from InstaSite/InstantReport consumption (main feature)
Analytics->>OnebalanceLogs: Aggregate with filters
OnebalanceLogs-->>Analytics: Raw logs (cents)- All costs converted to account's display currency
- Missing events filled with zero values
loop For each log
Analytics->>Currency: Convert USD to requested currency**Currency Conversion Flow**:
Currency-->>Analytics: Converted amount
end```javascript
// If no SMS usage this month
opt If detailed view{
Analytics->>Related: Fetch communication/site/report "sms": 0, // Explicitly set to 0
Related-->>Analytics: Related entity "email": 12.50,
end ...
}
Analytics-->>Client: \{ data, pagination \}
```// Stored: 750 cents
// Step 1: Convert to dollars: 750 / 100 = $7.50
---// Step 2: Convert to target currency: $7.50 USD โ โฌ7.00 EUR
๐ฐ Currency Conversionโ
Main vs Sub-Account Filtering:
All costs stored in cents (USD) in database.
Example Usage:```javascript
Conversion Process:
const currUtil = new CurrencyUtil();
const converted = await currUtil.convert(````javascriptconst EVENTS = ['sms', 'email', 'instasite', 'instareport', 'listing', 'phone'];
account_id,
costInCents / 100, // Convert cents to dollarsconst analytics = await analyticsService.analytics({
'USD', // Base currency
targetCurrency // e.g., 'EUR', 'GBP' account_id: req.account_id,// Sub-accounts: Exclude instasite/instareport
);
currency: 'USD',const EVENTS = ['sms', 'email', 'listing', 'phone'];
log.cost = parseFloat(converted.amount);
``` main: true```
**Supported Currencies**: All currencies defined in account business settings});
---// Returns: { this_month: {...}, last_month: {...}, previous_month: {...} }**Use Cases**:
## ๐ Analytics Calculations````
### Event Types Tracked- Monthly spend tracking
```javascript**Side Effects**:- Trend analysis (increasing/decreasing usage)
const EVENTS = [
'sms', // SMS messages- Budget forecasting
'email', // Email messages
'instasite', // InstaSite generations- โ ๏ธ Queries accounts collection for sub-account discovery- Service comparison (which services cost most)
'instareport', // InstaReport generations
'listing', // Business listing- โ ๏ธ Executes complex aggregation pipeline (performance consideration)
'phone', // Phone number
'inbound_call', // Inbound calls- โ ๏ธ Multiple currency conversions per event type---
'outbound_call', // Outbound calls
'seo_keyword', // SEO keyword tracking---### 2. `GET /v1/onebalance/logs` - Usage Logs
'lighting_domain', // Lightning domains
'a2p_registration' // A2P brand registration### Log Management**Purpose**: Retrieves paginated list of all OneBalance usage logs with filtering and sorting.
];
```---**Authentication**: Required (JWT)
### Monthly Comparison Logic#### `logs({ account_id, filters, sortField, sortOrder, page, limit, currency, main })`**Query Parameters**:
```javascript**Purpose**: Retrieves paginated OneBalance usage logs with filtering and sorting. Main accounts see all sub-account logs. Excludes listing and phone events from log view.| Parameter | Type | Required | Description |
const growth = (this_month.total - last_month.total) / last_month.total * 100;
const trend = growth > 0 ? 'up' : 'down';| --------------------- | -------- | -------- | --------------------------------------------------- |
Parameters:| page | Number | No | Page number (default: 1) |
| limit | Number | No | Items per page (default: 10) |
๐ก๏ธ Security & Access Controlโ
account_id(ObjectId) - Account identifier|sortField| String | No | Sort field (default:created_at) |
Authenticationโ
-
Bearer token required-
filters(Object, optional) - Filter criteria:|sortOrder| String | No |ascordesc(default:desc) | -
JWT validation
event(String) - Filter by event type|filters[startDate]| Date | No | Filter from this date |
Authorizationโ
-
Account-specific data only -
status(String) - Filter by status|filters[endDate]| Date | No | Filter to this date | -
Main accounts can view sub-account analytics
-
Sub-accounts cannot view other sub-accounts -
start_date(Date) - Filter by start date|filters[event]| Array | No | Filter by event types |
Data Access Rules - end_date (Date) - Filter by end date| filters[account_id] | ObjectId | No | Filter by specific sub-account (main accounts only) |โ
const match = \{- `sortField` (String, optional) - Field to sort by (default: 'created_at')
account_id: req.auth.account_id
\};- `sortOrder` (String, optional) - 'asc' or 'desc' (default: 'desc')**Request Example**:
if (req.auth.account.main && sub_account_id) \{- `page` (Number, optional) - Page number (default: 1)
match.account_id = parent_account_id;
match.sub_account_id = sub_account_id;- `limit` (Number, optional) - Items per page (default: 10)```http
\}
```- `currency` (String, optional) - Target currency for displayGET /v1/onebalance/logs?page=1&limit=20&sortField=created_at&sortOrder=desc&filters[startDate]=2024-10-01&filters[endDate]=2024-10-08&filters[event][]=sms&filters[event][]=email HTTP/1.1
---- `main` (Boolean) - Whether account is main accountAuthorization: Bearer <token>
## ๐ Related Documentation````
- [OneBalance Service](./onebalance.md) - Main OneBalance operations**Returns**: `Promise<Object>` - Paginated logs with metadata:
- Currency Utility *(documentation unavailable)* - Currency conversion**Response**:
- [Communications](../../communications/sms.md) - SMS/Email tracking
```javascript
---
{```javascript
**Last Updated**: October 2025
**Status**: Production Active data: [{
**Complexity**: HIGH
**Lines of Code**: ~450 { "success": true,
_id: ObjectId, "message": "SUCCESS",
account_id: ObjectId, "data": [
event: 'sms', {
status: 'success', "id": "670512a5e4b0f8a5c9d3e1b2",
cost: 0.0075, // Converted to currency "business": {
sub_account_cost: 0.00975, // If sub-account "id": "60a7f8d5e4b0d8f3a4c5e1b1",
created_at: Date, "name": "DashClicks",
details: { // Event-specific data "image": "https://...",
to: '+1234567890', "images": [...],
message_sid: 'SM123...' "profile_logo": "https://..."
} },
} "cost": 0.0075, // Cost in account currency (converted)
], "base_price": 0.0065, // Base cost (main accounts only)
pagination: { "credits": null, // For credit-based events (Instasite)
total: 1234, "account_id": "60a7f8d5e4b0d8f3a4c5e1b1",
page: 1, "event": "sms",
limit: 10, "status": "success",
pages: 124 "status_reason": null,
} "type": "deduction",
} "created_at": "2024-10-08T10:30:00.000Z",
``` "updated_at": "2024-10-08T10:30:00.000Z"
},
**Business Logic Flow**: {
"id": "670512b2e4b0f8a5c9d3e1b3",
1. **Build Base Query** "business": {
- Exclude: `event NOT IN ['listing', 'phone']` (not shown in logs UI) "id": "60a7f8d5e4b0d8f3a4c5e1b1",
- Main accounts: `account_id IN [main, ...sub_accounts]` "name": "DashClicks",
- Sub-accounts: `account_id = account_id` "image": "https://..."
},
2. **Apply Filters** (if provided) "cost": 0.001,
- `filters.event`: Match specific event type "credits": null,
- `filters.status`: Match status (success/failed) "account_id": "60a7f8d5e4b0d8f3a4c5e1b1",
- `filters.start_date` & `filters.end_date`: Date range filter "event": "email",
"status": "success",
3. **Count Total Records** "status_reason": null,
- `countDocuments()` with query "type": "deduction",
"created_at": "2024-10-08T10:25:00.000Z",
4. **Execute Paginated Query** "updated_at": "2024-10-08T10:25:00.000Z"
- Skip: `(page - 1) ร limit` },
- Limit: `limit` {
- Sort: `{ [sortField]: sortOrder === 'asc' ? 1 : -1 }` "id": "670512c1e4b0f8a5c9d3e1b4",
"business": {...},
5. **Convert Costs to Currency** "cost": 2.50,
- For each log: "credits": null,
- Convert `cost / 100` to dollars "account_id": "60a7f8d5e4b0d8f3a4c5e1b1",
- Convert `sub_account_cost / 100` to dollars (if present) "event": "instareport",
- Apply currency conversion "status": "success",
"status_reason": null,
6. **Calculate Pagination** "type": "deduction",
- `total`: Total record count "created_at": "2024-10-08T09:15:00.000Z",
- `pages`: `Math.ceil(total / limit)` "updated_at": "2024-10-08T09:15:00.000Z"
- `page`: Current page number },
- `limit`: Records per page {
"id": "670512d0e4b0f8a5c9d3e1b5",
7. **Return Logs with Pagination** "business": {...},
"cost": null,
**Filtering Logic**: "credits": 1, // Instasite used 1 credit
"account_id": "60a7f8d5e4b0d8f3a4c5e1b1",
```javascript "event": "instasite",
// Date range filter "status": "success",
{ "status_reason": null,
created_at: { "type": "deduction",
$gte: filters.start_date, "created_at": "2024-10-08T08:00:00.000Z",
$lte: filters.end_date "updated_at": "2024-10-08T08:00:00.000Z"
} }
} ],
"pagination": {
// Event filter "total": 1547,
{ "current_page": 1,
event: filters.event // 'sms', 'email', etc. "total_pages": 78,
} "per_page": 20,
"has_next": true,
// Status filter "has_prev": false
{ }
status: filters.status // 'success', 'failed'}
}```
Response Fields:
Key Business Rules:
| Field | Type | Description |
-
Listing and phone events excluded from log view (business decision)| --------------- | -------- | ------------------------------------------------ |
-
Main accounts see all sub-account logs in one view|
id| ObjectId | Log ID | -
Sub-accounts only see own logs|
business| Object | Account business info (populated) | -
Default sort: Most recent first (
created_at: desc)|cost| Number | Cost in account currency (null for credit-based) | -
Cost and sub_account_cost converted to account currency|
base_price| Number | Base cost before markup (main accounts only) | -
Empty filters return all logs (with exclusions)|
credits| Number | Credits used (for Instasite) |
| account_id | ObjectId | Account that incurred the cost |
| event | String | Event type (sms, email, instasite, etc.) |
| status | String | success or failed |
| status_reason| String | Failure reason (if status isfailed) |
| type | String | deduction or refill |
| created_at | Date | When event occurred |
| updated_at | Date | When log was updated |
Example Usage:
const logs = await analyticsService.logs(\{
account_id: req.account_id,
filters: \{
event: 'sms',
start_date: new Date('2024-10-01'),**Filtering Logic**:
end_date: new Date('2024-10-31')
},```javascript
page: 1,const match = {
limit: 50, $and: [],
currency: 'USD', event: { $nin: ['listing', 'phone'] }, // Exclude listing/phone from logs view
main: false};
});
// Returns: { data: [...50 logs], pagination: {...} }// Date range
````if (filters.startDate) {
match.$and.push({ created_at: { $gte: new Date(filters.startDate) } });
**Side Effects**:}
if (filters.endDate) {
- โ ๏ธ Queries accounts collection for sub-account discovery (if main) match.$and.push({ created_at: { $lte: new Date(filters.endDate) } });
- โ ๏ธ Two database queries: count + find (pagination pattern)}
- โ ๏ธ Currency conversion for each log record
// Event types
---if (filters.event && filters.event.length) {
match.event = { $in: filters.event }; // ['sms', 'email']
#### `log(id, currency)`}
**Purpose**: Retrieves single usage log with full details and linked documents (Communication, Instasite, Instareport). Provides deep insight into specific consumption event.// Account filtering (main accounts)
if (filters.account_id) {
**Parameters**: match.account_id = ObjectId(filters.account_id); // Specific sub-account
} else if (main) {
- `id` (ObjectId) - Log identifier // Main account sees all sub-accounts
- `currency` (String, optional) - Target currency for display const subAccounts = await Account.find({ parent_account: account_id });
const ids = [...subAccounts.map(a => a._id), account_id];
**Returns**: `Promise<Object>` - Log with linked document: match.account_id = { $in: ids };
} else {
```javascript // Sub-account sees only own logs
{ match.account_id = account_id;
_id: ObjectId,}
account_id: ObjectId,```
event: 'sms',
status: 'success',**Aggregation Pipeline**:
cost: 0.0075,
sub_account_cost: 0.00975,```javascript
created_at: Date,[
details: { // Original log details { $match: match },
to: '+1234567890', { $sort: { [sortField]: sortOrder === 'asc' ? 1 : -1 } },
message_sid: 'SM123...' {
}, $facet: {
linked_document: { // Populated from linked collection data: [
_id: ObjectId, { $limit: limit + skip },
from: '+10987654321', { $skip: skip },
to: '+1234567890', // Lookup account business info
body: 'Your verification code is...', {
status: 'delivered', $lookup: {
sent_at: Date from: '_accounts',
} localField: 'account_id',
} foreignField: '_id',
``` as: 'account',
},
**Business Logic Flow**: },
{ $unwind: '$account' },
1. **Find Log by ID** // Lookup business contact
- Query: `findById(id)` {
- Throw `notFound` if not exists $lookup: {
from: 'crm.contacts',
2. **Convert Costs to Currency** localField: 'account.business',
- Convert `cost / 100` to dollars โ target currency foreignField: '_id',
- Convert `sub_account_cost / 100` to dollars โ target currency (if present) as: 'business',
},
3. **Retrieve Linked Document** (if `details` field present) },
- **SMS/Email**: Query `Communication` by `_id = details._id` OR `_id = details.communication_id` { $unwind: '$business' },
- **Instasite**: Query `Instasite` by `_id = details._id` // Project fields
- **Instareport**: Query `Instareport` by `_id = details._id` {
- If found: Populate `log.details` with full document $project: {
id: '$_id',
4. **Return Log with Details** _id: 0,
business: { id: '$business._id', name: 1, image: 1, images: 1, profile_logo: 1 },
**Linked Document Mapping**: cost: 1,
base_price: 1, // Only for main accounts
| Event Type | Source Collection | Linked Field | Document Contains | credits: 1,
| ---------------- | ----------------- | ----------------------- | ------------------------- | account_id: 1,
| `sms` | `communications` | `details._id` | SMS message, status | event: 1,
| `email` | `communications` | `details.communication` | Email content, recipients | status: 1,
| `instasite` | `instasites` | `details._id` | Website generation data | status_reason: 1,
| `instareport` | `instareports` | `details._id` | Report generation data | type: 1,
| `listing` | N/A | N/A | No linked document | created_at: 1,
| `lighting_domain`| N/A | N/A | No linked document | updated_at: 1,
},
**Key Business Rules**: },
],
- Only events with `details` field have linked documents total: [{ $count: 'total' }],
- Communication lookup tries two field names (`_id` and `communication_id`) },
- Missing linked documents don't cause errors (details unchanged) },
- Costs converted to account currency];
- Full document details provided for troubleshooting```
**Error Handling**:**Use Cases**:
- `notFound(404)`: Log ID not found- Audit trail for all OneBalance transactions
- Troubleshooting failed transactions
**Example Usage**:- Cost analysis by event type
- Sub-account usage monitoring (main accounts)
```javascript- Date range reports for accounting
const logDetail = await analyticsService.log(
'60a7f8d5e4b0d8f3a4c5e1b2',---
'USD'
);### 3. `GET /v1/onebalance/logs/:id` - Log Details
// Returns log with populated details from Communication/Instasite/Instareport
```**Purpose**: Retrieves detailed information for a specific usage log including linked documents (Communication, Instasite, Instareport).
**Side Effects**:**Authentication**: Required (JWT)
- โ ๏ธ Queries linked collection (Communication/Instasite/Instareport)**Request Example**:
- โ ๏ธ Currency conversion performed
- โ ๏ธ No error if linked document not found (silently skipped)```http
GET /v1/onebalance/logs/670512a5e4b0f8a5c9d3e1b2 HTTP/1.1
---Authorization: Bearer <token>
Profit Analysisโ
Response (SMS/Email Event):
#### `profit({ account_id, sub_account_id, currency })`{
"success": true,
**Purpose**: Calculates reseller profit for main accounts by comparing parent cost vs sub-account cost. Used for white-label reseller profit tracking. "message": "SUCCESS",
"data": {
**Parameters**: "id": "670512a5e4b0f8a5c9d3e1b2",
"account_id": "60a7f8d5e4b0d8f3a4c5e1b1",
- `account_id` (ObjectId) - Main account (parent) identifier "event": "sms",
- `sub_account_id` (ObjectId, optional) - Filter by specific sub-account "cost": 0.0075,
- `currency` (String, optional) - Target currency for display "credits": null,
"status": "success",
**Returns**: `Promise<Object>` - Profit breakdown by event type: "status_reason": null,
"type": "deduction",
```javascript "additional_info": {
{ "communication_id": "670512a0e4b0f8a5c9d3e1b0"
sms: { },
cost: 7.50, // Main account's cost (base pricing) "created_at": "2024-10-08T10:30:00.000Z",
sub_account_cost: 9.75, // Sub-account's cost (with markup) "updated_at": "2024-10-08T10:30:00.000Z",
profit: 2.25 // Profit earned (difference)
}, // Linked Communication details
email: { "details": {
cost: 15.00, "id": "670512a0e4b0f8a5c9d3e1b0",
sub_account_cost: 18.00, "type": "SMS",
profit: 3.00 "from": "+14155551234",
}, "recipients": [
instasite: { {
cost: 150.00, "phone": "+14155555678",
sub_account_cost: 195.00, "status": "delivered"
profit: 45.00 }
} ],
// ... other events "content": "Your InstaReport is ready!",
} "origin": "instareport",
``` "created_at": "2024-10-08T10:30:00.000Z"
}
**Business Logic Flow**: }
}
1. **Build Query**```
- Base: `account_id = main_account_id`
- Optional: `sub_account_id = sub_account_id` (filter by specific sub)**Response (Instasite Event)**:
- Filter: `event != 'refill'` AND `sub_account_cost exists`
```javascript
2. **Execute Aggregation**{
- Group by `event` "success": true,
- Sum `cost` โ Total parent cost "message": "SUCCESS",
- Sum `sub_account_cost` โ Total sub-account cost "data": {
"id": "670512d0e4b0f8a5c9d3e1b5",
3. **Calculate Profit** "account_id": "60a7f8d5e4b0d8f3a4c5e1b1",
- For each event: "event": "instasite",
- `profit = sub_account_cost - cost` "cost": null, // Credit-based
- Convert all values: cents โ dollars โ target currency "credits": 1, // Used 1 credit
"status": "success",
4. **Return Profit Map** "type": "deduction",
"additional_info": {
**Aggregation Pipeline**: "instasite_id": "670512cfe4b0f8a5c9d3e1b4"
},
```javascript "created_at": "2024-10-08T08:00:00.000Z",
[
{ // Linked Instasite details
$match: { "details": {
account_id: ObjectId(account_id), "id": "670512cfe4b0f8a5c9d3e1b4",
sub_account_id: ObjectId(sub_account_id), // Optional "business_id": "60a7f8d5e4b0d8f3a4c5e1b3",
event: { $ne: 'refill' }, "business_name": "Acme Corp",
sub_account_cost: { $exists: true } "status": "GENERATED",
} "url": "https://acmecorp.instasites.ai",
}, "template_id": "template_123",
{ "created_at": "2024-10-08T07:55:00.000Z",
$group: { "generated_at": "2024-10-08T08:00:00.000Z"
_id: '$event', }
cost: { $sum: '$cost' }, }
sub_account_cost: { $sum: '$sub_account_cost' }}
}```
}
]**Response (Instareport Event)**:
**Profit Calculation Example**:{
"success": true,
```javascript "message": "SUCCESS",
// SMS Event "data": {
cost (parent): 750 cents = $7.50 "id": "670512c1e4b0f8a5c9d3e1b4",
sub_account_cost: 975 cents = $9.75 "account_id": "60a7f8d5e4b0d8f3a4c5e1b1",
profit: $9.75 - $7.50 = $2.25 "event": "instareport",
"cost": 2.50,
// Markup: 30% ($7.50 ร 1.3 = $9.75) "credits": null,
``` "status": "success",
"type": "deduction",
**Key Business Rules**: "additional_info": {
"instareport_id": "670512c0e4b0f8a5c9d3e1b3"
- Only logs with `sub_account_cost` field counted (sub-account transactions) },
- Refills excluded (not profit-generating) "created_at": "2024-10-08T09:15:00.000Z",
- Profit = revenue (sub_account_cost) - cost (parent_account_cost)
- Can filter by specific sub-account or aggregate all // Linked Instareport details
- All values converted to account currency "details": {
"id": "670512c0e4b0f8a5c9d3e1b3",
**Use Cases**: "business_id": "60a7f8d5e4b0d8f3a4c5e1b3",
"status": "GENERATED",
- **Reseller Dashboard**: Show total profit across all sub-accounts "scores": {
- **Sub-Account Analysis**: Filter by specific sub to see per-client profit "yext": 75,
- **Event Breakdown**: Identify most profitable services (SMS vs email vs sites) "seo": 82,
"social": 68,
**Example Usage**: "reviews": 90,
"pageSpeed": 85,
```javascript "googleAds": 70
// All sub-accounts profit },
const totalProfit = await analyticsService.profit({ "created_at": "2024-10-08T09:00:00.000Z",
account_id: main_account_id, "generated_at": "2024-10-08T09:15:00.000Z"
currency: 'USD' }
}); }
}
// Specific sub-account profit```
const clientProfit = await analyticsService.profit({
account_id: main_account_id,**Linked Document Retrieval**:
sub_account_id: sub_account_id,
currency: 'USD'```javascript
});if (log.status === 'success') {
``` // For SMS/Email events
if (['sms', 'email'].includes(log.event)) {
**Side Effects**: const communication = await Communication.findById(log.additional_info?.communication_id);
log.details = communication;
- โ ๏ธ Aggregation pipeline query (performance consideration) }
- โ ๏ธ Currency conversion for all events
// For Instasite events
--- if (log.event === 'instasite') {
const instasite = await Instasite.findById(log.additional_info?.instasite_id);
### Utility Functions log.details = instasite;
}
---
// For Instareport events
#### `getObj({ arr, currency, account_id, main })` (Internal) if (log.event === 'instareport') {
const instareport = await Instareport.findById(log.additional_info?.instareport_id);
**Purpose**: Formats aggregation results into standardized analytics object. Fills missing events with zero values and converts currency. log.details = instareport;
}
**Parameters**:}
-
arr(Array) - Aggregation results[{ _id: 'sms', used: 10, cost: 100 }] -
currency(String) - Target currencyError Scenarios: -
account_id(ObjectId) - Account for currency conversion -
main(Boolean) - Whether account is main| Scenario | Response | HTTP Status |
| ------------------------- | --------------------------- | ----------- |
Returns: Promise<Object> - Formatted analytics:| Log not found | Log does not exist | 404 |
| Linked document not found | {Document} does not exist | 404 |
{**Use Cases**:
sms: { used: 10, cost: 1.00 },
email: { used: 0, cost: 0 },- Detailed transaction investigation
instasite: { used: 5, cost: 25.00 },- Linking usage to specific communications/sites/reports
// ... all EVENTS filled- Troubleshooting failed events
}- Audit compliance
Business Logic Flow:
4. GET /v1/onebalance/profit/:account_id - Profit Reportโ
-
Initialize Empty Object
-
Create object with all
EVENTS(sms, email, instasite, etc.)Purpose: Calculates profit from sub-account usage for main accounts (resellers). Shows cost difference between what main account pays and what sub-account is charged. -
Default values:
{ used: 0, cost: 0 }
-
Authentication: Required (JWT)
-
Populate from Aggregation Results
-
For each item in
arr:Access: Main accounts only-
If
_id(event name) is null: skip -
Set
obj[_id].used = usedURL Parameters: -
Convert cost:
cost / 100โ dollars -
Apply currency conversion: USD โ target currency| Parameter | Type | Required | Description |
-
Set
obj[_id].cost = converted_cost| ------------ | -------- | -------- | ------------------------------------------------ |
-
-
| account_id | ObjectId | No | Specific sub-account (optional, defaults to all) |
- Return Formatted Object
Request Example:
Null Event Handling:
```javascriptGET /v1/onebalance/profit HTTP/1.1
// Aggregation may return null events (edge case)Authorization: Bearer <token>
[{ _id: null, used: 5, cost: 500 }]
// Or for specific sub-account
// Skipped from output (not assigned to any event)GET /v1/onebalance/profit/60a7f8d5e4b0d8f3a4c5e1b9 HTTP/1.1
```Authorization: Bearer <token>
Example Usage (Internal):
Response:
// Aggregation result```javascript
const arr = [{
{ _id: 'sms', used: 10, cost: 750 }, // 750 cents "success": true,
{ _id: 'email', used: 5, cost: 500 } // 500 cents "message": "SUCCESS",
]; "data": {
"sms": {
const result = await getObj({ arr, currency: 'USD', account_id, main: true }); "cost": 45.30, // What main account paid to DashClicks
// Returns: "sub_account_cost": 58.89, // What sub-account was charged
// { "profit": 13.59 // Profit = 58.89 - 45.30
// sms: { used: 10, cost: 7.50 }, },
// email: { used: 5, cost: 5.00 }, "email": {
// instasite: { used: 0, cost: 0 }, "cost": 12.50,
// ... all other events with 0 "sub_account_cost": 13.75,
// } "profit": 1.25
``` },
"instareport": {
**Side Effects**: "cost": 75.00,
"sub_account_cost": 97.50,
- โ ๏ธ Currency conversion for each event in array "profit": 22.50
},
--- "listing": {
"cost": 100.00,
## ๐ Integration Points "sub_account_cost": 150.00,
"profit": 50.00
### External Services },
"phone": {
#### Currency Utility "cost": 23.00,
"sub_account_cost": 28.75,
- **Purpose**: Convert USD base costs to account currency "profit": 5.75
- **Class**: `CurrencyUtil` from `shared/utilities/currency.js` }
- **Method**: `convert(account_id, amount, from_currency, to_currency)` }
- **Usage**: All cost values converted before return}
Internal Dependenciesโ
Response Fields:
-
shared/utilities/constants.js-EVENTSarray (list of all event types) -
shared/models/account.js- Sub-account discovery| Field | Description | -
shared/models/communication.js- SMS/email linked documents| -------------------------- | ----------------------------------------------- | -
shared/models/instasite.js- Website generation linked documents|{event}.cost| Total cost paid by main account (to DashClicks) | -
shared/models/instareport.js- Report generation linked documents|{event}.sub_account_cost| Total amount charged to sub-account |
| {event}.profit | Profit = sub_account_cost - cost |
Shared Modelsโ
Profit Calculation:
-
OnebalanceLogs- Primary analytics data source -
Account- Sub-account relationships```mermaid -
Communication- SMS/email message detailsflowchart LR -
Instasite- Website generation records A[DashClicks] -->|Base Price| B[Main Account] -
Instareport- Report generation records B -->|Base Price + Markup| C[Sub-Account]C -.->|Profit| B
style A fill:#f9f,stroke:#333
๐งช Edge Cases & Special Handling style B fill:#9ff,stroke:#333โ
style C fill:#ff9,stroke:#333
Case: Main Account with No Sub-Accounts```โ
Condition: Main account flag set but no sub-accounts exist Example Calculation:
Handling: Query includes only main account ID
Result: Returns main account's own usage only```javascript
// SMS Event
Case: Sub-Account Excluded from InstaSite AnalyticsBase DashClicks Price: $0.0075โ
Main Account Pays: $0.0075
Condition: Sub-account queries analytics
Handling: Aggregation filter excludes 'instasite' and 'instareport' events Sub-Account Rebill Multiplier: 1.3
Result: Sub-accounts never see website/report consumption (main account feature)Sub-Account Charged: $0.0075 ร 1.3 = $0.00975
Case: Missing Linked Document// For 1000 SMS messagesโ
Main Account Cost: $7.50
Condition: Log has details._id but document deleted Sub-Account Cost: $9.75
Handling: Linked document query returns null, details unchanged Profit: $2.25
Result: Original details field returned (no error thrown)
Case: Null Event in Aggregationโ
Condition: Aggregation returns \{ _id: null, used: 5 \}
Handling: Null events skipped in getObj()
Result: No assignment to any event type (data integrity)
Case: Zero Results for Date Rangeโ
Aggregation Query:
const match = \{
account_id: mainAccountId, // Main account ID
sub_account_id: \{ $exists: true \}, // Only transactions with sub-account
status: 'success', // Only successful transactions
\};
**Condition**: No logs match filter criteria
**Handling**: All events filled with `{ used: 0, cost: 0 }` // Filter by specific sub-account (optional)
**Result**: Consistent data structure, no errorsif (sub_account_id) {
match.sub_account_id = ObjectId(sub_account_id);
### Case: Profit Without Sub-Account Cost}
**Condition**: Logs missing `sub_account_cost` field const query = [
**Handling**: Aggregation filters out these logs (`$exists: true`) { $match: match },
**Result**: Only transactions with markup included in profit calculation {
$group: {
--- \_id: '$event', // Group by event type
cost: { $sum: '$cost' }, // Sum main account cost
## โ ๏ธ Important Notes sub_account_cost: { $sum: '$sub_account_cost' }, // Sum sub-account charges
},
- ๐ **3-Month Comparison**: Analytics provides historical context for trend analysis },
- ๐ **Main Account Visibility**: Main accounts see aggregated data across all sub-accounts {
- ๐ซ **Log Exclusions**: Listing and phone events excluded from log view (business decision) $set: {
- ๐ **Profit Tracking**: Only for main accounts with sub-accounts (reseller model) profit: { $subtract: ['$sub_account_cost', '$cost'] }, // Calculate profit
- ๐ฐ **Cent-Based Storage**: All costs stored in cents, converted to dollars for display },
- ๐ **Currency Conversion**: All monetary values converted to account currency },
- ๐ **Linked Documents**: Deep details available for SMS, email, InstaSite, InstantReport];
- โก **Performance**: Aggregation pipelines may be slow with large datasets```
- ๐
**Date Ranges**: Month boundaries calculated dynamically (not calendar-based)
- โ ๏ธ **No Caching**: All queries hit database (consider Redis for high-traffic)**Currency Conversion**:
---All values converted from cents to dollars and from USD to main account's currency:
## ๐ Related Documentation```javascript
// Stored in DB: cost = 4530 cents, sub_account_cost = 5889 cents
- **Parent Module**: [OneBalance Module](./index.md)// Display: cost = $45.30, sub_account_cost = $58.89, profit = $13.59
- **Related Service**: [OneBalance Service](./onebalance.md)// If account in EUR: cost = โฌ41.20, sub_account_cost = โฌ53.56, profit = โฌ12.36
- **Controller**: `internal/api/v1/onebalance/controllers/analytics.js````
- **Routes**: `internal/api/v1/onebalance/routes/index.js`
- **Utilities**:**Use Cases**:
- Currency Conversion *(documentation unavailable)*
- **Models**:- Service-level profitability analysis
## Data Models
### OnebalanceLogs Schema
```javascript
{
_id: ObjectId,
account_id: ObjectId, // Account that incurred the cost
sub_account_id: ObjectId, // If triggered by sub-account (for profit tracking)
event: String, // 'sms', 'email', 'instasite', 'instareport', 'listing', 'phone'
type: String, // 'deduction' or 'refill'
cost: Number, // Cost in cents (main account pays this)
sub_account_cost: Number, // Cost charged to sub-account (if applicable)
base_price: Number, // Original base price (for reference)
credits: Number, // For credit-based events (Instasite)
status: String, // 'success' or 'failed'
status_reason: String, // Failure reason
additional_info: {
communication_id: ObjectId, // For sms/email events
instasite_id: ObjectId, // For instasite events
instareport_id: ObjectId, // For instareport events
listing_id: ObjectId, // For listing events
phone_id: ObjectId // For phone events
},
created_at: Date,
updated_at: Date
}
Service Layer Functionsโ
analytics({ account_id, currency, main })โ
Generates monthly analytics with comparison.
Returns:
{
this_month: { sms: 45.30, email: 12.50, ... },
last_month: { sms: 38.20, email: 10.30, ... },
previous_month: { sms: 42.10, email: 11.00, ... }
}
logs({ account_id, filters, sortField, sortOrder, page, limit, currency, main })โ
Retrieves paginated usage logs with filtering.
Returns:
{
data: [ { id, cost, event, status, ... }, ... ],
pagination: { total, current_page, total_pages, ... }
}
log(id, currency)โ
Retrieves single log with linked document details.
Returns: Log object with details field containing linked document
profit({ account_id, sub_account_id, currency })โ
Calculates profit from sub-account usage.
Returns:
{
sms: { cost, sub_account_cost, profit },
email: { cost, sub_account_cost, profit },
...
}
Business Rulesโ
Analyticsโ
- Refills Excluded:
refillevents not counted in analytics (not usage) - Successful Only: Only
status: 'success'logs counted - Sub-Account Filtering: Sub-accounts don't see Instasite/Instareport
- Null Events: Events with no usage shown as
0
Logsโ
- Listing/Phone Excluded: Not shown in general logs view (configurable)
- Main Account Visibility: Main accounts see all sub-account logs
- Sub-Account Isolation: Sub-accounts only see own logs
- Linked Documents: Only available for successful transactions
Profitโ
- Main Accounts Only: Only main accounts have profit reports
- Sub-Account Transactions: Only logs with
sub_account_idcounted - Successful Only: Failed transactions not included in profit
Performance Considerationsโ
- Indexes Required:
{ account_id: 1, created_at: -1 }{ account_id: 1, event: 1, status: 1, created_at: -1 }{ account_id: 1, sub_account_id: 1, status: 1 }
- Aggregation Optimization: Use
$facetfor pagination + count in single query - Currency Conversion: Cached conversion rates (update hourly)
Last Updated: October 2025
Status: Production Critical
Complexity: MEDIUM
Lines of Code: ~400 controller + service