๐ Campaign Renewal (A2P)
๐ Overviewโ
The Campaign Renewal job handles monthly renewals for Twilio A2P messaging campaigns as required by 10DLC compliance regulations. It runs daily at noon, identifies accounts with campaigns due for renewal within 6 days, charges renewal fees via OneBalance ($200 for sole proprietor, $150 for low volume standard), and schedules the next renewal date 30 days in the future. If renewal fails due to insufficient funds and the renewal date is within 1 day, the job automatically deletes the campaign and sends cancellation notifications.
Complete Flow:
- Cron Initialization:
queue-manager/crons/communication/a2p/renewal.js - Service Processing:
queue-manager/services/communication/a2p.js(campaignRenewalCheck) - Queue Definition:
queue-manager/queues/communication/a2p/renewal.js
Execution Pattern: Daily polling (noon) with account-level queuing and 6-day advance notice
Queue Name: comm_a2p_campaignRenewal
Environment Flag: QM_COMMUNICATION_A2P_RENEWAL=true (in index.js)
๐ Complete Processing Flowโ
sequenceDiagram
participant CRON as Cron Schedule<br/>(daily at noon)
participant SERVICE as Campaign Renewal<br/>Service
participant ACCOUNT_DB as Accounts DB
participant QUEUE as Campaign Renewal<br/>Queue
participant OB_UTIL as OneBalance<br/>Utility
participant OB_QUEUE as OneBalance<br/>Queue Collection
participant NOTIF as Notification<br/>Service
participant TWILIO as Twilio A2P API
CRON->>SERVICE: campaignRenewalCheck()
SERVICE->>SERVICE: Calculate 6 days in future
SERVICE->>ACCOUNT_DB: Aggregate accounts where:<br/>- campaignStatus = 'VERIFIED'<br/>- next_renewal <= 6 days
ACCOUNT_DB-->>SERVICE: Accounts due for renewal
loop Each account
SERVICE->>QUEUE: Add job: {account}
end
loop Each queued account
QUEUE->>QUEUE: Get renewal date
alt No renewal date set
QUEUE->>QUEUE: Log warning, skip
else Renewal date exists
QUEUE->>QUEUE: Calculate amount due:<br/>SOLE_PROPRIETOR: $200<br/>Other: $150
QUEUE->>OB_UTIL: verifyBalance()<br/>Check sufficient funds
alt Insufficient funds
OB_UTIL-->>QUEUE: Error thrown
QUEUE->>QUEUE: Handle failed renewal
else Sufficient funds
OB_UTIL-->>QUEUE: Balance verified
QUEUE->>OB_QUEUE: Create OnebalanceQueue record<br/>Event: a2p_registration_renewal
QUEUE->>QUEUE: Calculate next renewal:<br/>current renewal + 30 days
QUEUE->>ACCOUNT_DB: Set next_renewal field
end
end
end
alt Job Failed: Insufficient funds
QUEUE->>QUEUE: Calculate days until renewal
alt Days <= 1 (urgent)
QUEUE->>TWILIO: DELETE Campaign<br/>Cancel campaign
QUEUE->>NOTIF: Send notification:<br/>type=a2p.renewal.cancelled
else Days > 1 (grace period)
QUEUE->>NOTIF: Send notification:<br/>type=a2p.renewal.failed<br/>data=amount due
end
end
๐ Source Filesโ
1. Cron Initializationโ
File: queue-manager/crons/communication/a2p/renewal.js
Purpose: Schedule campaign renewal check daily at noon
Cron Pattern: 0 12 * * * (daily at 12:00 PM)
Initialization:
const { campaignRenewalCheck } = require('../../../services/communication/a2p');
const cron = require('node-cron');
const logger = require('../../../utilities/logger');
let inProgress = false;
exports.start = async () => {
try {
cron.schedule('0 12 * * *', async () => {
if (!inProgress) {
inProgress = true;
await campaignRenewalCheck();
inProgress = false;
}
});
} catch (err) {
logger.error({ initiator: 'QM/communication/a2p/campaign-renewal', error: err });
}
};
In-Progress Lock: Prevents overlapping executions.
Execution Times: 12:00 PM every day
2. Service Processingโ
File: queue-manager/services/communication/a2p.js (campaignRenewalCheck export)
Purpose: Find accounts with campaigns due for renewal within 6 days and queue them
Key Features:
- 6-day advance notice for renewals
- Simple aggregation matching verified campaigns with approaching renewal dates
- Batch job queuing
Main Service Function:
const campaignRenewalCheck = require('../../queues/communication/a2p/renewal');
const Account = require('../../models/account');
const logger = require('../../utilities/logger');
exports.campaignRenewalCheck = async () => {
try {
let dateInSixDays = new Date();
dateInSixDays.setDate(dateInSixDays.getDate() + 6);
let accounts = await Account.aggregate([
{
$match: {
'twilio_account.a2p.messaging_campaign.campaignStatus': 'VERIFIED',
'twilio_account.a2p.next_renewal': { $lte: dateInSixDays },
},
},
]);
if (accounts.length) {
let queue = await campaignRenewalCheck.start();
await Promise.all(
accounts.map(async a => {
try {
await queue.add(
{
account: a,
},
{
removeOnComplete: true,
},
);
} catch (err) {
logger.error({
initiator: 'QM/communication/a2p/campaign-renewal/add-queue',
error: err,
});
}
}),
);
}
} catch (err) {
logger.error({ initiator: 'QM/communication/a2p/campaign-renewal/service', error: err });
}
};
Query: Matches accounts with:
campaignStatus: 'VERIFIED'next_renewaldate less than or equal to 6 days from now
6-Day Advance Window: Ensures accounts have time to add funds if balance is insufficient.
3. Queue Processing (THE RENEWAL LOGIC)โ
File: queue-manager/queues/communication/a2p/renewal.js
Purpose: Charge renewal fees, update renewal dates, handle failures
Key Functions:
- Verify sufficient OneBalance funds
- Create OneBalance billing queue record
- Calculate and update next renewal date
- Delete campaign and notify if renewal fails within 1 day
Complete Processor:
const mongoose = require('mongoose');
const QueueWrapper = require('../../../common/queue-wrapper');
const logger = require('../../../utilities/logger');
const a2p = require('../twilio/services/a2p');
const { verifyBalance } = require('../../../utilities/onebalance');
const OnebalanceQueue = require('../../../models/onebalance-queue');
const Account = require('../../../models/account');
const axios = require('axios');
const processCb = async (job, done) => {
try {
const { account } = job.data;
let renewalDate = account.twilio_account.a2p.next_renewal;
if (!renewalDate) {
logger.warn({
initiator: 'QM/communication/a2p/campaign-renewal/queue',
message: 'Next renewal date is not set.',
job: job.id,
job_data: job.data,
});
return done();
}
const LOW_VOLUME = 150;
const SOLE = 200;
let amountDue = 0;
if (account.twilio_account.a2p.messaging_campaign.usAppToPersonUsecase == 'SOLE_PROPRIETOR')
amountDue = SOLE;
else amountDue = LOW_VOLUME;
await verifyBalance({
event: 'a2p_registration_renewal',
account: account,
predefinedPrice: amountDue,
});
await new OnebalanceQueue({
account_id: account._id,
event: 'a2p_registration_renewal',
additional_info: {
reference: new mongoose.Types.ObjectId(),
},
price: amountDue,
}).save();
let nextRenewal = new Date(renewalDate);
nextRenewal.setMonth(nextRenewal.getMonth() + 1);
await Account.findByIdAndUpdate(account._id, {
$set: {
'twilio_account.a2p.next_renewal': nextRenewal,
},
});
return done();
} catch (err) {
done(err);
}
};
const failedCb = async (job, err) => {
logger.error({
initiator: 'QM/communication/a2p/campaign-renewal/queue',
message: `Failed to renew A2P Campaign`,
error: err,
job: job.id,
job_data: job.data,
});
const { account } = job.data;
const today = new Date();
const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
let renewalDate = new Date(account.twilio_account.a2p.next_renewal);
// Calculating the difference between today and the renewal date
const diffDays = Math.round((renewalDate.getTime() - today.getTime()) / oneDay);
if (diffDays <= 1) {
const metadata = {
twilio_creds: {
sid: account.twilio_account?.sid,
token: account.twilio_account?.authToken,
},
};
await a2p.deleteMessagingCampaign(
metadata,
account.twilio_account.a2p.messaging_service.sid,
account.twilio_account.a2p.messaging_campaign.sid,
);
await axios.get(process.env.NOTIFICATION_SERVICE_URL + '/run-service', {
params: {
type: 'a2p.renewal.cancelled',
id: account._id.toString(),
},
});
} else {
const LOW_VOLUME = 150;
const SOLE = 200;
let amountDue = 0;
if (account.twilio_account.a2p.messaging_campaign.usAppToPersonUsecase == 'SOLE_PROPRIETOR')
amountDue = SOLE;
else amountDue = LOW_VOLUME;
await axios.get(process.env.NOTIFICATION_SERVICE_URL + '/run-service', {
params: {
type: 'a2p.renewal.failed',
id: account._id.toString(),
data: amountDue,
},
});
}
};
const completedCb = async job => {
logger.log({
initiator: 'QM/communication/a2p/campaign-renewal/queue',
message: `A2P Campaign Renewed`,
job: job.id,
job_data: job.data,
});
};
let queue;
exports.start = async () => {
try {
if (!queue)
queue = QueueWrapper(`comm_a2p_campaignRenewal`, 'global', {
processCb,
completedCb,
failedCb,
});
return Promise.resolve(queue);
} catch (err) {
logger.error({
initiator: 'QM/communication/a2p/campaign-renewal/queue',
error: err,
message: `Error while starting queue`,
});
}
};
4. OneBalance Utilityโ
File: queue-manager/utilities/onebalance.js (verifyBalance)
Purpose: Check if account has sufficient OneBalance funds for renewal
Function Signature:
await verifyBalance({
event: 'a2p_registration_renewal',
account: account,
predefinedPrice: amountDue,
});
Behavior:
- Throws error if insufficient funds
- Allows processing to continue if sufficient funds
- Does NOT charge funds (only verifies availability)
5. Twilio A2P Service Utilitiesโ
File: queue-manager/queues/communication/twilio/services/a2p.js (deleteMessagingCampaign)
Purpose: Delete campaign from Twilio when renewal fails
Function Signature:
await a2p.deleteMessagingCampaign(metadata, serviceSid, campaignSid);
API Call: DELETE /v1/Services/{serviceSid}/UsAppToPerson/{campaignSid}
๐๏ธ Collections Usedโ
_accountsโ
- Operations: Aggregate, Update
- Model:
shared/models/account.js - Usage Context: Store A2P renewal tracking
Key Fields (twilio_account.a2p object):
messaging_campaign: Object - A2P campaign datacampaignStatus: 'VERIFIED' (required for renewal)usAppToPersonUsecase: 'SOLE_PROPRIETOR' | 'LOW_VOLUME_STANDARD'sid: Campaign SID
messaging_service: Object - Messaging service datasid: Messaging service SID
next_renewal: Date - Next campaign renewal date (updated with each renewal)
onebalance_queueโ
- Operations: Create
- Model:
shared/models/onebalance-queue.js - Usage Context: Queue billing events for OneBalance processing
Record Structure:
{
account_id: ObjectId,
event: "a2p_registration_renewal",
additional_info: {
reference: ObjectId // Unique reference for this renewal
},
price: 200 | 150 // Amount to charge
}
Note: OneBalance job processes this collection and charges accounts.
๐ง Job Configurationโ
Cron Scheduleโ
'0 12 * * *'; // Daily at 12:00 PM
Frequency: Once per day
Rationale: Daily check ensures renewals are caught within 6-day window; noon timing provides business hours coverage.
Queue Settingsโ
QueueWrapper(`comm_a2p_campaignRenewal`, 'global', {
processCb,
completedCb,
failedCb,
});
Queue Name: comm_a2p_campaignRenewal
Concurrency: Default (1)
Job Options:
{
removeOnComplete: true;
}
Note: No retry configuration - relies on default Bull settings (no retries).
๐ Processing Logic - Detailed Flowโ
1. Renewal Queryโ
Service Aggregation:
let dateInSixDays = new Date();
dateInSixDays.setDate(dateInSixDays.getDate() + 6);
await Account.aggregate([
{
$match: {
'twilio_account.a2p.messaging_campaign.campaignStatus': 'VERIFIED',
'twilio_account.a2p.next_renewal': { $lte: dateInSixDays },
},
},
]);
Matches: Accounts with:
- Verified campaigns (active, not pending or rejected)
- Renewal date within next 6 days
6-Day Window Logic:
- Today = Oct 13, 2025
- Date in 6 days = Oct 19, 2025
- Matches renewals from Oct 1 to Oct 19 (accounts with renewal dates in past or next 6 days)
Why 6 Days?:
- Provides grace period for insufficient funds
- Allows multiple daily attempts before 1-day deadline
- Balances advance notice with urgency
2. Renewal Fee Calculationโ
const LOW_VOLUME = 150;
const SOLE = 200;
let amountDue = 0;
if (account.twilio_account.a2p.messaging_campaign.usAppToPersonUsecase == 'SOLE_PROPRIETOR')
amountDue = SOLE;
else amountDue = LOW_VOLUME;
Fee Structure:
- SOLE_PROPRIETOR: $200 per month
- LOW_VOLUME_STANDARD (and others): $150 per month
Note: Fees are hardcoded (not pulled from Twilio or external config).
3. Balance Verificationโ
await verifyBalance({
event: 'a2p_registration_renewal',
account: account,
predefinedPrice: amountDue,
});
Behavior:
- Checks if account has sufficient OneBalance funds
- Throws error if insufficient (triggers
failedCb) - Continues processing if sufficient
Important: Does NOT deduct funds - only verifies availability.
4. OneBalance Billing Queueโ
await new OnebalanceQueue({
account_id: account._id,
event: 'a2p_registration_renewal',
additional_info: {
reference: new mongoose.Types.ObjectId(),
},
price: amountDue,
}).save();
Purpose: Create billing record for OneBalance job to process.
Fields:
event: 'a2p_registration_renewal' (identifies billing type)price: Amount to charge ($200 or $150)reference: Unique ObjectId for this renewal transaction
Actual Charging: Handled by separate OneBalance job (not this job).
5. Next Renewal Calculationโ
let nextRenewal = new Date(renewalDate);
nextRenewal.setMonth(nextRenewal.getMonth() + 1);
await Account.findByIdAndUpdate(account._id, {
$set: {
'twilio_account.a2p.next_renewal': nextRenewal,
},
});
Calculation: Current renewal date + 1 month
Example:
- Current renewal: Oct 13, 2025
- Next renewal: Nov 13, 2025
Note: Uses existing renewal date (not current date) to maintain consistent renewal schedule.
6. Failure Handlingโ
Failed Job Callback (failedCb):
const today = new Date();
const oneDay = 24 * 60 * 60 * 1000;
let renewalDate = new Date(account.twilio_account.a2p.next_renewal);
const diffDays = Math.round((renewalDate.getTime() - today.getTime()) / oneDay);
if (diffDays <= 1) {
// URGENT: Delete campaign and notify
await a2p.deleteMessagingCampaign(metadata, serviceSid, campaignSid);
await axios.get(process.env.NOTIFICATION_SERVICE_URL + '/run-service', {
params: {
type: 'a2p.renewal.cancelled',
id: account._id.toString(),
},
});
} else {
// GRACE PERIOD: Notify only
await axios.get(process.env.NOTIFICATION_SERVICE_URL + '/run-service', {
params: {
type: 'a2p.renewal.failed',
id: account._id.toString(),
data: amountDue,
},
});
}
Two Failure Modes:
Mode 1: Urgent (โค 1 Day Until Renewal)
- Delete campaign from Twilio (stops SMS sending)
- Send cancellation notification (
a2p.renewal.cancelled) - No retry - campaign removed permanently
Mode 2: Grace Period (> 1 Day Until Renewal)
- Send failure notification (
a2p.renewal.failed) with amount due - Do NOT delete campaign
- Daily cron will retry tomorrow (until within 1-day threshold)
Example Timeline:
- Day 6: Renewal check, insufficient funds โ Send "renewal failed" notification
- Day 5: Retry, still insufficient โ Send "renewal failed" notification
- ...
- Day 1: Retry, still insufficient โ Delete campaign, send "renewal cancelled" notification
๐จ Error Handlingโ
Common Error Scenariosโ
Insufficient OneBalance Fundsโ
Scenario: Account has less than $200/$150 available
Handling: verifyBalance throws error, triggers failedCb
Impact:
- Grace period (>1 day): Send notification, retry tomorrow
- Urgent (โค1 day): Delete campaign, send cancellation notification
Recovery: User adds funds, next daily run succeeds
Missing Renewal Dateโ
Scenario: next_renewal field not set
Handling: Log warning, skip account (job completes successfully)
Impact: Account never renewed (campaign expires)
Note: Should never occur if campaign check job ran successfully.
Campaign Already Deletedโ
Scenario: Campaign deleted externally or by previous failure
Handling: Twilio API error thrown when deleting, logged in failedCb
Impact: Error logged, but notification still sent
Note: Failure callback doesn't wrap delete in try/catch.
Notification Service Unavailableโ
Scenario: Notification service (5008) down or unreachable
Handling: Axios error thrown, logged
Impact: User not notified of renewal failure/cancellation
Note: Failure callback doesn't wrap notification in try/catch.
Database Update Failureโ
Scenario: MongoDB connection issue, validation error
Handling: Error thrown, job fails
Impact: OneBalance charged, but next renewal date not updated (causes issues)
Note: Should use transactions to ensure atomicity.
Failed Job Callbackโ
See "Failure Handling" section above for detailed logic.
Completed Job Callbackโ
const completedCb = async job => {
logger.log({
initiator: 'QM/communication/a2p/campaign-renewal/queue',
message: `A2P Campaign Renewed`,
job: job.id,
job_data: job.data,
});
};
Action: Log success with job details.
๐ Monitoring & Loggingโ
Success Loggingโ
Queue Level:
- Success log on job completion: "A2P Campaign Renewed"
Error Loggingโ
Cron Level:
- Error in cron initialization
Service Level:
- Error aggregating accounts
- Error queuing individual accounts
Queue Level:
- Warning if renewal date not set
- Error starting queue
- Failed job with error details
Performance Metricsโ
- Balance Verification: less than 1 second (local check)
- OneBalance Queue Creation: less than 1 second (MongoDB insert)
- Database Update: less than 1 second
- Campaign Deletion (on failure): 1-2 seconds (Twilio API call)
- Notification (on failure): 1-2 seconds (HTTP call)
- Total Job Time (Success): 2-3 seconds
- Total Job Time (Failure): 3-5 seconds (includes deletion and notification)
๐ Integration Pointsโ
Triggers This Jobโ
- Cron Schedule: Daily at noon (no external triggers)
- Campaign Check Job: Sets
next_renewaldate (prerequisite)
External Dependenciesโ
- OneBalance Utility: Balance verification
- OneBalance Queue Job: Processes billing records
- Notification Service (5008): Sends email/SMS notifications
- Twilio A2P API: Campaign deletion on failure
Jobs That Depend On Thisโ
- OneBalance Job: Processes
onebalance_queuerecords to charge accounts - Campaign Check Job: Monitors
campaignStatus(deleted campaigns become unavailable)
Related Featuresโ
- OneBalance Dashboard: Displays renewal charges
- Campaign Dashboard: Shows campaign status and renewal dates
- SMS Sending: Disabled if campaign deleted due to failed renewal
โ ๏ธ Important Notesโ
Side Effectsโ
- โ ๏ธ OneBalance Charges: Creates billing queue records (charges applied by separate job)
- โ ๏ธ Renewal Date Updates: Sets next renewal 30 days in future
- โ ๏ธ Campaign Deletion: Permanently removes campaign on urgent failures (โค1 day)
- โ ๏ธ SMS Sending Impact: Deleted campaigns disable SMS sending
Performance Considerationsโ
- Daily Schedule: Once per day minimizes load
- 6-Day Window: Ensures multiple retry attempts before cancellation
- Sequential Processing: One account at a time prevents resource contention
- No Retries: Default job configuration has no retry logic (relies on daily cron)
- Job Cleanup:
removeOnComplete: trueprevents queue bloat
Business Logicโ
Why Daily at Noon?
- Business hours timing for support availability
- Daily checks ensure renewals caught within 6-day window
- Noon provides mid-day coverage (not early morning or late night)
Why 6-Day Advance Window?
- Allows 6 daily retry attempts before 1-day deadline
- Provides grace period for users to add funds
- Balances urgency with flexibility
- Example: Renewal on Oct 19, first notification on Oct 13 (6 attempts: Oct 13-18)
Why $200 for Sole Proprietor, $150 for Others?
- Twilio's 10DLC pricing structure
- Sole proprietor campaigns have higher compliance requirements
- Low volume standard campaigns have lower fees
Why Delete Campaign at 1-Day Threshold?
- Prevents expired campaigns from sending (regulatory compliance)
- 1-day threshold gives final chance to add funds
- Automatic deletion prevents non-compliant SMS sending
Why Use OneBalance Instead of Direct Stripe?
- DashClicks uses prepaid OneBalance system for all billing
- Consistent billing experience across all services
- Allows customers to manage balance centrally
Why Calculate Next Renewal from Current Renewal Date?
- Maintains consistent monthly schedule
- Prevents drift if job runs late in day
- Example: Oct 13 renewal โ Nov 13 renewal (not Nov 14 if job runs on Oct 14)
Maintenance Notesโ
- Renewal Fees: $200/$150 hardcoded (consider environment variables or database)
- 6-Day Window: Hardcoded (consider configuration)
- 1-Day Threshold: Hardcoded in failure callback
- No Retries: Consider adding exponential backoff for transient failures
- No Transactions: Balance verification โ queue creation โ date update should be atomic
- Error Handling: Failure callback doesn't catch errors from delete/notification
- Notification Types: 'a2p.renewal.failed' and 'a2p.renewal.cancelled' must exist in notification service
Code Quality Issuesโ
Issue 1: No Transaction Atomicity
await verifyBalance(...); // Step 1
await new OnebalanceQueue(...).save(); // Step 2
await Account.findByIdAndUpdate(...); // Step 3
Issue: If Step 3 fails, OneBalance charge created but renewal date not updated.
Impact: Account charged multiple times (daily retries), renewal date never advances.
Suggestion: Use MongoDB transactions:
const session = await mongoose.startSession();
session.startTransaction();
try {
await verifyBalance(...);
await new OnebalanceQueue(...).save({ session });
await Account.findByIdAndUpdate(..., { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
Issue 2: Unhandled Errors in Failure Callback
await a2p.deleteMessagingCampaign(...); // Can throw
await axios.get(...); // Can throw
Issue: If delete or notification fails, error thrown from failure callback.
Impact: Failed job remains in error state, logs incomplete.
Suggestion: Wrap in try/catch:
try {
await a2p.deleteMessagingCampaign(...);
} catch (err) {
logger.error({...err, message: 'Failed to delete campaign'});
}
try {
await axios.get(...);
} catch (err) {
logger.error({...err, message: 'Failed to send notification'});
}
Issue 3: Hardcoded Fees
const LOW_VOLUME = 150;
const SOLE = 200;
Issue: Fees hardcoded, duplicated in failure callback.
Suggestion: Move to environment variables or database:
const RENEWAL_FEES = {
SOLE_PROPRIETOR: parseInt(process.env.A2P_RENEWAL_FEE_SOLE || '200'),
LOW_VOLUME_STANDARD: parseInt(process.env.A2P_RENEWAL_FEE_LOW_VOLUME || '150'),
};
let amountDue =
RENEWAL_FEES[account.twilio_account.a2p.messaging_campaign.usAppToPersonUsecase] || 150;
Issue 4: Missing Renewal Date Handling
if(!renewalDate) {
logger.warn(...);
return done();
}
Issue: Job succeeds (returns done()) even though renewal not processed.
Impact: Account appears successfully renewed but wasn't.
Suggestion: Fail job to trigger retry:
if (!renewalDate) {
return done(new Error('Next renewal date is not set'));
}
Issue 5: Hardcoded 1-Day Threshold
if (diffDays <= 1) {
// Delete campaign
}
Issue: 1-day threshold hardcoded.
Suggestion: Use environment variable:
const URGENT_THRESHOLD_DAYS = parseInt(process.env.A2P_RENEWAL_URGENT_DAYS || '1');
if (diffDays <= URGENT_THRESHOLD_DAYS) {
// Delete campaign
}
Issue 6: Edge Case - Month Overflow
nextRenewal.setMonth(nextRenewal.getMonth() + 1);
Issue: Same as campaign check - Jan 31 + 1 month โ Feb 28/29.
Suggestion: Use days for precision:
nextRenewal.setDate(nextRenewal.getDate() + 30);
Issue 7: Notification Service Availability
await axios.get(process.env.NOTIFICATION_SERVICE_URL+'/run-service', {...});
Issue: No timeout, retry, or circuit breaker for notification service calls.
Impact: Job hangs if notification service unresponsive.
Suggestion: Add timeout and error handling:
await axios.get(process.env.NOTIFICATION_SERVICE_URL+'/run-service', {
params: {...},
timeout: 5000, // 5-second timeout
}).catch(err => {
logger.error({...err, message: 'Notification service unavailable'});
});
๐งช Testingโ
Manual Triggerโ
# Via API (if QM_HOOKS=true)
POST http://localhost:6002/api/trigger/communication/a2p/renewal
Simulate Renewal Dueโ
const Account = require('./models/account');
// Set renewal date to 3 days from now (within 6-day window)
const renewalDate = new Date();
renewalDate.setDate(renewalDate.getDate() + 3);
await Account.findByIdAndUpdate(accountId, {
'twilio_account.a2p.messaging_campaign.campaignStatus': 'VERIFIED',
'twilio_account.a2p.messaging_campaign.usAppToPersonUsecase': 'SOLE_PROPRIETOR',
'twilio_account.a2p.next_renewal': renewalDate,
});
console.log('Renewal date set to:', renewalDate);
console.log('Waiting for daily cron run at noon');
Verify Renewal Processingโ
const OnebalanceQueue = require('./models/onebalance-queue');
// Check OneBalance billing queue
const billingRecords = await OnebalanceQueue.find({
account_id: accountId,
event: 'a2p_registration_renewal',
});
console.log('Billing records:', billingRecords);
// Verify next renewal date updated
const account = await Account.findById(accountId);
console.log('Next renewal:', account.twilio_account.a2p.next_renewal);
// Verify 30 days in future
const daysUntilRenewal =
(account.twilio_account.a2p.next_renewal - new Date()) / (1000 * 60 * 60 * 24);
console.log('Days until renewal:', Math.round(daysUntilRenewal)); // Should be ~33 (3 + 30)
Test Insufficient Funds Handlingโ
// Set account OneBalance to $0
await Account.findByIdAndUpdate(accountId, {
'onebalance.balance': 0,
});
// Set renewal date to urgent (tomorrow)
const urgentRenewal = new Date();
urgentRenewal.setDate(urgentRenewal.getDate() + 1);
await Account.findByIdAndUpdate(accountId, {
'twilio_account.a2p.next_renewal': urgentRenewal,
});
// Trigger renewal (will fail)
// After job runs, verify campaign deleted
const account = await Account.findById(accountId);
console.log('Campaign status:', account.twilio_account.a2p.messaging_campaign?.campaignStatus);
// Verify deletion (campaign should be removed from Twilio)
Test Grace Periodโ
// Set renewal date to 3 days from now
const gracePeriod = new Date();
gracePeriod.setDate(gracePeriod.getDate() + 3);
await Account.findByIdAndUpdate(accountId, {
'twilio_account.a2p.next_renewal': gracePeriod,
'onebalance.balance': 0, // Insufficient funds
});
// Trigger renewal (will fail but NOT delete)
// After job runs, verify campaign still exists
const account = await Account.findById(accountId);
console.log('Campaign status:', account.twilio_account.a2p.messaging_campaign?.campaignStatus);
// Should still be 'VERIFIED' (not deleted)
// Check notification sent
// (verify in notification service logs or email)
Monitor Queue Processingโ
# Watch logs during renewal
tail -f logs/queue-manager.log | grep "renewal"
# Expected outputs:
# [INFO] A2P Campaign Renewed (success)
# [ERROR] Failed to renew A2P Campaign (insufficient funds)
# [WARN] Next renewal date is not set (missing date)
Test Fee Calculationโ
// Test sole proprietor fee
const account1 = {
twilio_account: {
a2p: {
messaging_campaign: {
usAppToPersonUsecase: 'SOLE_PROPRIETOR',
},
},
},
};
const fee1 =
account1.twilio_account.a2p.messaging_campaign.usAppToPersonUsecase === 'SOLE_PROPRIETOR'
? 200
: 150;
console.log('Sole proprietor fee:', fee1); // $200
// Test low volume fee
const account2 = {
twilio_account: {
a2p: {
messaging_campaign: {
usAppToPersonUsecase: 'LOW_VOLUME_STANDARD',
},
},
},
};
const fee2 =
account2.twilio_account.a2p.messaging_campaign.usAppToPersonUsecase === 'SOLE_PROPRIETOR'
? 200
: 150;
console.log('Low volume fee:', fee2); // $150
Test 6-Day Windowโ
// Test accounts within 6-day window
const dateInSixDays = new Date();
dateInSixDays.setDate(dateInSixDays.getDate() + 6);
const accounts = [
{ next_renewal: new Date('2025-10-13') }, // Today - should match
{ next_renewal: new Date('2025-10-14') }, // Tomorrow - should match
{ next_renewal: new Date('2025-10-19') }, // 6 days - should match
{ next_renewal: new Date('2025-10-20') }, // 7 days - should NOT match
{ next_renewal: new Date('2025-10-10') }, // Past - should match
];
accounts.forEach((acc, i) => {
const matches = acc.next_renewal <= dateInSixDays;
console.log(`Account ${i + 1}:`, acc.next_renewal.toDateString(), 'โ', matches);
});
// Expected:
// Account 1: Sat Oct 13 2025 โ true
// Account 2: Sun Oct 14 2025 โ true
// Account 3: Fri Oct 19 2025 โ true
// Account 4: Sat Oct 20 2025 โ false
// Account 5: Wed Oct 10 2025 โ true
Job Type: Scheduled with Account-Level Queuing
Execution Frequency: Daily at 12:00 PM
Average Duration: 2-5 seconds per account
Status: Active