OneBalance Main Service
๐ Overviewโ
The OneBalance service manages the unified credit balance system for all DashClicks consumption-based services. It handles:
- Balance retrieval and updates (cent-based storage with dollar display)
- Auto-reload configuration with Stripe payment methods
- Per-service pricing with tier-based logic
- Rebilling configuration for main accounts with sub-account markup
- InstaSite credit tracking tied to billing cycles
- Multi-currency pricing conversion
๐๏ธ Database Collectionsโ
๐ Full Schema: See Database Collections Documentation
onebalanceโ
- Operations: Read/Write for balance and configuration management
- Model:
shared/models/onebalance.js - Usage: Stores account credit balance (cents), auto-reload settings, and rebilling configuration
onebalance.usage_logsโ
- Operations: Read for InstaSite usage calculation
- Model:
shared/models/onebalance-usage_logs.js - Usage: Count InstaSite usage within billing cycle for credit tracking
configsโ
- Operations: Read for base pricing by account tier
- Model:
shared/models/config.js - Usage: Retrieve service pricing by
account_pricing_type(pro, plus, platinum)
_store.subscriptionsโ
- Operations: Read for billing cycle dates
- Model:
shared/models/store-subscription.js - Usage: Determine current billing period for InstaSite monthly credit resets
_accountsโ
- Operations: Read for account tier, parent relationships, pricing type
- Model:
shared/models/account.js - Usage: Determine pricing tier and rebilling relationships
๐ Data Flowโ
sequenceDiagram
participant Client
participant Service
participant DB as Database
participant Currency as Currency Util
Client->>Service: get(account_id)
Service->>DB: Find OneBalance
alt OneBalance not found
Service->>Service: initOnebalance()
Service->>DB: Create default OneBalance
end
Service->>Service: Convert cents โ dollars (รท100)
alt Main Account
Service->>DB: Get Store Subscription
Service->>DB: Count InstaSite usage
Service->>Service: Calculate remaining credits
end
Service->>Currency: Convert USD โ account currency
Service-->>Client: OneBalance with converted values
๐ฏ API Endpointsโ
GET /v1/onebalanceโ
Purpose: Retrieve OneBalance configuration and current balance
Authentication: Required (JWT)
Response:
{
"success": true,
"message": "SUCCESS",
"data": {
"_id": "60a7f8d5e4b0d8f3a4c5e1b2",
"account_id": "60a7f8d5e4b0d8f3a4c5e1b1",
"balance": 150.5,
"reload": {
"enabled": true,
"threshold": 20.0,
"amount": 100.0,
"payment_method": "pm_xxx"
},
"rebill": {
"sms": {
"enabled": true,
"multiplier": 1.2
},
"email": {
"enabled": true,
"multiplier": 1.1
},
"listing": {
"enabled": true,
"value": 5.0,
"multiplier": null
},
"lighting_domain": {
"enabled": true,
"value": null,
"multiplier": 1.5
},
"instasite": {
"enabled": true,
"credits": 850
}
},
"created_at": "2024-10-01T10:00:00.000Z",
"updated_at": "2024-10-08T14:30:00.000Z"
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
balance | Number | Current balance in dollars (converted from cents) |
reload.enabled | Boolean | Whether auto-reload is enabled |
reload.threshold | Number | Balance threshold for auto-reload (dollars) |
reload.amount | Number | Amount to reload (dollars) |
reload.payment_method | String | Stripe payment method ID |
rebill | Object | Rebilling configuration (main accounts only) |
rebill.{service}.enabled | Boolean | Whether rebilling is enabled for service |
rebill.{service}.value | Number | Fixed rebill price (if set) |
rebill.{service}.multiplier | Number | Multiplier on base price (if set) |
rebill.instasite.credits | Number | Remaining credits this billing cycle |
PUT /v1/onebalanceโ
Purpose: Update OneBalance configuration (reload settings, rebilling)
Authentication: Required (JWT)
Request Body:
{
"reload": {
"enabled": true,
"threshold": 25.0,
"amount": 200.0,
"payment_method": "pm_newmethod123"
},
"rebill": {
"listing": {
"enabled": true,
"value": 6.0
},
"sms": {
"enabled": true,
"multiplier": 1.3
},
"instasite": {
"enabled": true,
"credits": 1500
}
}
}
Validation:
- Threshold must be > 0
- Amount must be > 0
- Multiplier must be >= 1
- Sub-accounts cannot update
rebillsettings
Response: Same structure as GET endpoint with updated values
GET /v1/onebalance/cost-per-itemโ
Purpose: Get current pricing for all OneBalance services
Authentication: Required (JWT)
Response:
{
"success": true,
"message": "SUCCESS",
"data": {
"sms": {
"type": "dynamic",
"base_us_price": 0.0075,
"a2p_registration_pending": false
},
"email": {
"type": "fixed",
"amount": 0.001
},
"phone": {
"type": "dynamic",
"base_us_price": 1.0
},
"listing": {
"type": "fixed",
"amount": 50.0
},
"lighting_domain": {
"type": "fixed",
"amount": 10.0
},
"instasite": {
"type": "credit",
"credits": 850,
"credit_details": {
"total": 1000,
"used": 150,
"remaining": 850
},
"billing_cycle": {
"start": "2024-10-01T00:00:00.000Z",
"end": "2024-11-01T00:00:00.000Z"
}
},
"instareport": {
"type": "fixed",
"amount": 5.0
}
}
}
Pricing Types:
| Type | Description | Examples |
|---|---|---|
dynamic | Price fetched from external API | SMS, Phone, Calls |
fixed | Static price per item | Email, Listing, Domain |
credit | Monthly credit allocation | InstaSite |
๐ง Service Methodsโ
get({ account_id, account, currency, main, account_pricing_type })โ
Purpose: Retrieves OneBalance configuration and current balance for an account. Auto-initializes if not exists. Converts cent-based storage to dollar display with currency conversion.
Parameters:
account_id(ObjectId) - Account identifieraccount(Object) - Full account documentcurrency(String, optional) - Target currency for display (default: USD)main(Boolean) - Whether account is main accountaccount_pricing_type(String, optional) - Pricing tier override
Returns: Promise<Object> - OneBalance document with converted values
Business Logic Flow:
-
Query OneBalance
- Find by
account_id - If not found: call
initOnebalance()to create default
- Find by
-
Validate Record
- Throw
notFoundif initialization fails
- Throw
-
Convert Cent Storage to Dollars
balance รท 100reload.threshold รท 100reload.amount รท 100
-
Main Account Processing
- Convert rebill values:
listing.value รท 100,lighting_domain.value รท 100 - Calculate InstaSite credits: call
getInstasiteCreditInfo() - Set
rebill.instasite.creditsto remaining credits
- Convert rebill values:
-
Sub-Account Processing
- Delete
rebillobject entirely (not applicable)
- Delete
-
Apply Currency Conversion (if not USD)
- Convert all monetary values to target currency
-
Return OneBalance
Key Business Rules:
- All monetary values stored in cents (prevents floating-point errors)
- Auto-initialization ensures record always exists
- Main accounts receive rebilling configuration
- Sub-accounts never see rebill settings
- InstaSite credits calculated per billing cycle
Error Handling:
notFound(404): OneBalance initialization failed
Example Usage:
const balance = await onebalanceService.get({
account_id: req.account_id,
account: req.account,
currency: 'USD',
main: true,
});
// Returns: { balance: 125.50, reload: {...}, rebill: {...} }
Side Effects:
- โ ๏ธ May create OneBalance record if not exists
- โ ๏ธ Queries store subscription for billing cycle
- โ ๏ธ Counts usage logs for credit calculation
update(account_id, data, currency, main)โ
Purpose: Updates OneBalance configuration including reload settings and rebilling multipliers/values. Converts dollar inputs to cent storage.
Parameters:
account_id(ObjectId) - Account identifierdata(Object) - Update fields (nested structure)currency(String, optional) - Display currencymain(Boolean) - Whether account is main account
Returns: Promise<Object> - Updated OneBalance with converted values
Business Logic Flow:
-
Convert Dollars to Cents (if present)
reload.amount ร 100reload.threshold ร 100rebill.listing.value ร 100rebill.lighting_domain.value ร 100
-
Flatten Nested Object
- Convert
{ reload: { amount: 10000 } }โ{ 'reload.amount': 10000 } - Uses
flattenObject()utility
- Convert
-
Update Database
findOneAndUpdate({ account_id }, flattened, { new: true })
-
Convert Response Back to Dollars
balance รท 100reload.threshold รท 100reload.amount รท 100- Main only:
rebill.listing.value รท 100,rebill.lighting_domain.value รท 100
-
Filter by Account Type
- Main: Return all fields including rebill
- Sub: Delete rebill object
-
Return Updated Record
Updatable Fields:
{
reload: {
enabled: Boolean,
threshold: Number, // Dollars (will be ร 100)
amount: Number, // Dollars (will be ร 100)
payment_method: String
},
rebill: { // Main only
listing: { value: Number }, // Dollars (will be ร 100)
lighting_domain: { value: Number },
instasite: { credits: Number }, // Raw number, not converted
sms: { multiplier: Number },
email: { multiplier: Number }
}
}
Key Business Rules:
- Only monetary values converted (not multipliers or credits)
- Partial updates supported (only send changed fields)
- Sub-accounts cannot update rebill settings
- Nested updates flattened for MongoDB dot notation
Example Usage:
const updated = await onebalanceService.update(
account_id,
{
reload: { enabled: true, threshold: 25.0, amount: 200.0 },
},
'USD',
true,
);
costPerItem({ account_id, account, currency, user_id })โ
Purpose: Calculates per-service pricing for account, applying tier-based pricing, parent rebilling markup, and InstaSite credit information.
Parameters:
account_id(ObjectId) - Account identifieraccount(Object) - Full account document withpricing_type,main,parent_accountcurrency(String, optional) - Target currencyuser_id(ObjectId) - User ID for dynamic pricing requests
Returns: Promise<Object> - Service pricing map
Business Logic Flow:
-
Fetch Base Pricing Config
- Query
configscollection - Match:
type: 'onebalance-base-prices'ANDaccount_pricing_type: pricing_type - Throw
notFoundif config missing
- Query
-
Handle InstaSite Credits
- Call
getInstasiteCreditInfo({ account_id, account }) - Replace
prices['instasite']with credit details - Skip conversion (credit-based, not monetary)
- Call
-
Convert Base Prices to Account Currency
- For each service (skip instasite):
- Dynamic pricing: Convert
base_us_price / 100from USD to currency - Fixed pricing: Convert
amount / 100from USD to currency
- Dynamic pricing: Convert
- Uses
CurrencyUtil.convert()
- For each service (skip instasite):
-
Apply Sub-Account Rebilling (if not main)
- Find parent OneBalance
- Throw
notFoundif no rebill config - For each service (skip instasite):
- Dynamic:
base_us_price ร rebill[service].multiplier - Fixed with value override: Use
rebill[service].value / 100(converted) - Fixed with multiplier:
amount ร rebill[service].multiplier
- Dynamic:
-
Add A2P Registration Flag
- If Twilio A2P not verified:
prices['sms'].a2p_registration_pending = true
- If Twilio A2P not verified:
-
Return Pricing Map
Pricing Calculation Examples:
// Dynamic pricing with sub-account markup
Base Price: $0.0075 (from config)
Parent Rebill Multiplier: 1.3
Sub-Account Price: $0.0075 ร 1.3 = $0.00975
// Fixed pricing with value override
Base Price: $25.00
Parent Rebill Value: $50.00 (overrides base)
Sub-Account Price: $50.00
Key Business Rules:
- Base pricing retrieved by account
pricing_type(pro/plus/platinum) - Sub-accounts inherit parent pricing with markup
- InstaSite excluded for sub-accounts (main account feature)
- Rebill can use
multiplier(percentage) ORvalue(fixed override) - A2P flag warns about SMS pricing limitations
- All prices converted to account's display currency
Error Handling:
notFound(404): Price config not found for tiernotFound(404): Parent OneBalance missing rebill configuration
Side Effects:
- โ ๏ธ Queries store subscription for billing cycle
- โ ๏ธ Counts usage logs for InstaSite credits
- โ ๏ธ May call external Twilio API for dynamic pricing
getInstasiteCreditInfo({ account_id, account })โ
Purpose: Calculates remaining InstaSite credits for current billing cycle. Handles both subscription-based and tier-override billing.
Parameters:
account_id(ObjectId) - Account identifieraccount(Object) - Account document withmain,parent_account,tier_override
Returns: Promise<Object|null> - Credit information or null if no billing
Business Logic Flow:
-
Find Active Subscription
- Query
StoreSubscription:- Account:
account_idORparent_account(for sub-accounts) - Plan:
metadata.software = 'true'ANDmetadata.product_type = 'software' - Status:
active,trialing, orpast_due
- Account:
- Sort by
updated_atdescending (get latest)
- Query
-
Check Tier Override
- Call
tierOverride(account_id, tier_override) - If tier override exists but no plan returned: return null
- If tier override missing and no subscription: return null
- Call
-
Extract Billing Period
- Tier Override: Use
start_dateandrenewal(unix timestamps) - Subscription: Use
current_period_startandcurrent_period_end - Convert unix timestamps to Date objects (ร 1000)
- Tier Override: Use
-
Get Configured Credit Limit
- Query OneBalance for main account or parent
- Extract
rebill.instasite.credits - Default: 1000 credits if not configured
-
Count Usage in Billing Cycle
- Query
OnebalanceLogs:account_id: account OR parentevent: 'instasite'status: 'success'created_at: BetweencurrentPeriodStartandcurrentPeriodEnd
- Use
countDocuments()for efficiency
- Query
-
Calculate Remaining Credits
remaining = initialCredits - instasiteUsage
-
Return Credit Object
Key Business Rules:
- Credits reset monthly based on subscription billing cycle
- Sub-accounts use parent's subscription and credit allocation
- Tier overrides take precedence over subscriptions
- Only successful InstaSite generations counted
- Default allocation: 1000 credits/month
- Main accounts only (sub-accounts don't get this info)
Edge Cases:
- No Subscription: Returns null (no credit allocation)
- Tier Override Without Plan: Returns null
- Negative Credits: Possible if configured limit reduced mid-cycle
- Parent Account Lookup: Sub-accounts must use parent's billing
Example Usage:
const creditInfo = await onebalanceService.getInstasiteCreditInfo({
account_id: req.account_id,
account: req.account,
});
// Returns: { credits: 850, credit_details: {...}, billing_cycle: {...} }
๐ฐ Pricing Hierarchyโ
Main Account Pricingโ
Base Provider Price (e.g., Twilio)
โ
Platform Markup (1.05x)
โ
Price Charged to Main Account
Sub-Account Pricingโ
Base Provider Price
โ
Platform Markup (1.05x)
โ
Parent Rebill Multiplier (1.2x)
โ
Price Charged to Sub-Account
Example SMS Calculation:
// Twilio charges: $0.0075 per SMS
// Platform markup: 1.05
// Main account pays: $0.0075 ร 1.05 = $0.007875
// Parent sets rebill multiplier: 1.2
// Sub-account pays: $0.007875 ร 1.2 = $0.00945
// Parent profit: $0.00945 - $0.007875 = $0.001575 per SMS
๐ณ Auto-Reload Systemโ
Configurationโ
reload: {
enabled: true,
threshold: 20.00, // Trigger when balance < $20
amount: 100.00, // Reload $100
payment_method: "pm_..." // Saved payment method
}
Trigger Logicโ
Checked during verifyBalance() before each transaction:
if (onebalance.balance < onebalance.reload.threshold && onebalance.reload.enabled) {
// Queue auto-reload job
await chargePaymentMethod({
amount: onebalance.reload.amount,
payment_method: onebalance.reload.payment_method,
});
// Add to balance
onebalance.balance += onebalance.reload.amount;
}
Failure Handlingโ
- Email notification sent to account owner
- Balance remains unchanged
- Transaction proceeds if balance sufficient
- Transaction fails if balance insufficient
๐ข Rebilling Configurationโ
Main Account Controlsโ
rebill: {
sms: {
enabled: true,
multiplier: 1.2 // Charge sub-accounts 1.2x
},
email: {
enabled: true,
multiplier: 1.15
},
listing: {
enabled: true,
value: 15.00, // Fixed price to sub-accounts
multiplier: null // Ignored if value set
},
instasite: {
enabled: true,
credits: 10 // Monthly credits per sub-account
}
}
Rebilling Rulesโ
- Multiplier-based: Used for dynamic pricing (SMS, email, calls)
- Value-based: Used for static pricing (listings, domains)
- Credits: Used for InstaSites (monthly allocation)
Disable Rebilling:
rebill.sms.enabled = false; // Sub-accounts cannot use SMS
๐ฐ Balance Managementโ
Balance Storageโ
- Database: Stored as cents (integers)
- API: Returned as dollars (floats)
Conversion:
// Reading
balance_dollars = balance_cents / 100;
// Writing
balance_cents = balance_dollars * 100;
Balance Updatesโ
Debit (usage):
await OnebalanceLogs.create({
account_id,
event: 'sms',
cost: 8, // 8 cents
type: 'debit',
status: 'success',
});
// Balance reduced automatically via queue
Credit (reload, refund):
await OnebalanceLogs.create({
account_id,
event: 'refill',
cost: 10000, // $100 = 10000 cents
type: 'credit',
status: 'success',
});
๐ Security & Validationโ
Authorizationโ
- Main accounts: Full access to rebill configuration
- Sub-accounts: Cannot view/edit rebill settings
- Each account can only access their own OneBalance
Input Validationโ
// Threshold must be positive
if (body.reload.threshold <= 0) {
throw badRequest('Threshold must be greater than 0');
}
// Multiplier must be >= 1 (cannot sell below cost)
if (body.rebill.sms.multiplier < 1) {
throw badRequest('Multiplier must be >= 1');
}
// Payment method required if auto-reload enabled
if (body.reload.enabled && !body.reload.payment_method) {
throw badRequest('Payment method required for auto-reload');
}
๐ Integration Pointsโ
External Servicesโ
-
Twilio Pricing API
- Endpoint:
/v1/e/twilio/pricing/{type}?country=US - Types:
sms,phone,call - Status: Currently unused (base prices used instead)
- Endpoint:
-
Currency Utility
- Class:
CurrencyUtilfromshared/utilities/currency.js - Method:
convert(account_id, amount, from_currency, to_currency)
- Class:
-
Stripe Integration
- Payment method storage
- Auto-reload charge processing
Internal Dependenciesโ
shared/utilities/onebalance.js-initOnebalance()for auto-creationshared/utilities/credits.js-tierOverride()for manual tier overridesshared/utilities/index.js-flattenObject()for nested updatesshared/utilities/catch-errors.js- Error helpers (notFound,badRequest)shared/utilities/constants.js-DEFAULT_PRICING_TYPE
๐งช Edge Cases & Special Handlingโ
OneBalance Auto-Initializationโ
Condition: First access for new account
Handling: Call initOnebalance() to create default configuration
Result: Seamless experience, always returns valid record
InstaSite Credits After Config Changeโ
Condition: Admin reduces credit allocation mid-cycle
Handling: Calculation may show negative credits
Display: Frontend should show 0 if negative
Sub-Account Without Parent Rebillโ
Condition: Parent account hasn't configured rebilling
Handling: Throw notFound('Onebalance billing record not found')
Prevention: Parent must set rebill config before sub-account usage
Currency Conversion Failureโ
Condition: Currency service unavailable
Handling: Currently no error handling (will throw)
Improvement: Should fall back to USD
A2P Registration Pendingโ
Condition: Twilio A2P campaign not verified
Handling: Add a2p_registration_pending: true flag to SMS pricing
Display: Frontend shows warning about SMS limitations
โ ๏ธ Important Notesโ
- ๐ต Cent-Based Storage: All monetary values stored in cents (ร 100) to prevent floating-point errors
- ๐ Conversion Pattern: Input dollars โ convert to cents โ store โ retrieve cents โ convert to dollars
- ๐ฏ Main vs Sub: Main accounts get rebill config, sub-accounts get pricing with markup
- ๐ InstaSite Credits: Reset monthly based on subscription billing cycle
- ๐ Currency Support: All prices converted to account's display currency
- ๐ Tier-Based Pricing: Different base prices for pro/plus/platinum tiers
- ๐ Dynamic Pricing: Twilio integration exists but unused (base prices used)
- ๐ Billing Cycle: Tied to Store subscription, not calendar month
- โ ๏ธ No Transaction: Updates not wrapped in transactions (potential inconsistency)
- ๐จ Missing Validation: No checks for negative balance or invalid multipliers
๐ Related Documentationโ
- Parent Module: OneBalance Module
- Related Service: Analytics Service
- Controller:
internal/api/v1/onebalance/controllers/onebalance.js - Routes:
internal/api/v1/onebalance/routes/index.js - Utilities:
- OneBalance Verification (link removed - file does not exist)
- Currency Conversion (link removed - file does not exist)
- Credits Management (link removed - file does not exist)