Skip to main content

๐Ÿ”„ 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:

  1. Cron Initialization: queue-manager/crons/communication/a2p/renewal.js
  2. Service Processing: queue-manager/services/communication/a2p.js (campaignRenewalCheck)
  3. 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_renewal date 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 data
    • campaignStatus: 'VERIFIED' (required for renewal)
    • usAppToPersonUsecase: 'SOLE_PROPRIETOR' | 'LOW_VOLUME_STANDARD'
    • sid: Campaign SID
  • messaging_service: Object - Messaging service data
    • sid: 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_renewal date (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_queue records to charge accounts
  • Campaign Check Job: Monitors campaignStatus (deleted campaigns become unavailable)
  • 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: true prevents 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

๐Ÿ’ฌ

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM