Add Schema.org JSON-LD, SEO meta overrides, and resilient bulk SMS.
Expose Organization/LocalBusiness and Product/BlogPosting jsonLd, optional seoMeta fields on content/categories, and continue mass SMS after per-recipient failures. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
db305c58a4
commit
aaf7a83742
+121
-114
@@ -1,4 +1,12 @@
|
||||
import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
HttpException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ServiceUnavailableException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { randomBytes } from 'crypto';
|
||||
@@ -769,7 +777,8 @@ export class CustomersService {
|
||||
|
||||
/**
|
||||
* Bulk SMS. Empty/omitted userIds → all enabled business customers.
|
||||
* With a personalized template, messages are sent per-recipient (not one bulk payload).
|
||||
* Always sends per-recipient so one failure does not abort the rest.
|
||||
* Returns a report with sent/failed counts and per-number errors.
|
||||
*/
|
||||
async sendBulkSms(
|
||||
businessIdRaw: string,
|
||||
@@ -863,139 +872,137 @@ export class CustomersService {
|
||||
enabled: false,
|
||||
message: 'SMS is disabled. Message was not sent.',
|
||||
recipientCount: recipients.length,
|
||||
sentCount: 0,
|
||||
failedCount: 0,
|
||||
failures: [],
|
||||
};
|
||||
}
|
||||
|
||||
const sample = await this.resolveOutboundSms(
|
||||
businessId,
|
||||
business.settings,
|
||||
dto.message,
|
||||
dto.templateId,
|
||||
{
|
||||
firstName: recipients[0].firstName,
|
||||
lastName: recipients[0].lastName,
|
||||
titleName: recipients[0].titleName,
|
||||
},
|
||||
);
|
||||
|
||||
const personalized =
|
||||
Boolean(dto.templateId) || smsTemplateUsesVariables(dto.message);
|
||||
|
||||
let charged = 0;
|
||||
let personalizedPayloads: Array<{
|
||||
const payloads: Array<{
|
||||
cellNumber: string;
|
||||
message: string;
|
||||
source: string;
|
||||
cost: number;
|
||||
templateId: string | null;
|
||||
templateVerified: boolean | null;
|
||||
}> = [];
|
||||
|
||||
if (!personalized) {
|
||||
charged = await this.smsBilling.charge(
|
||||
for (const recipient of recipients) {
|
||||
const resolved = await this.resolveOutboundSms(
|
||||
businessId,
|
||||
sample.message,
|
||||
recipients.length,
|
||||
'mass',
|
||||
);
|
||||
} else {
|
||||
personalizedPayloads = [];
|
||||
let totalCost = 0;
|
||||
for (const recipient of recipients) {
|
||||
const resolved = await this.resolveOutboundSms(
|
||||
businessId,
|
||||
business.settings,
|
||||
dto.message,
|
||||
dto.templateId,
|
||||
{
|
||||
firstName: recipient.firstName,
|
||||
lastName: recipient.lastName,
|
||||
titleName: recipient.titleName,
|
||||
},
|
||||
);
|
||||
const cost = this.smsBilling.costForMessage(resolved.message);
|
||||
personalizedPayloads.push({
|
||||
cellNumber: recipient.cellNumber,
|
||||
message: resolved.message,
|
||||
source: resolved.source,
|
||||
cost,
|
||||
});
|
||||
totalCost += cost;
|
||||
}
|
||||
charged = await this.smsBilling.chargeAmount(
|
||||
businessId,
|
||||
totalCost,
|
||||
personalizedPayloads.length,
|
||||
'mass',
|
||||
business.settings,
|
||||
dto.message,
|
||||
dto.templateId,
|
||||
{
|
||||
firstName: recipient.firstName,
|
||||
lastName: recipient.lastName,
|
||||
titleName: recipient.titleName,
|
||||
},
|
||||
);
|
||||
payloads.push({
|
||||
cellNumber: recipient.cellNumber,
|
||||
message: resolved.message,
|
||||
source: resolved.source,
|
||||
cost: this.smsBilling.costForMessage(resolved.message),
|
||||
templateId: resolved.templateId,
|
||||
templateVerified: resolved.templateVerified,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (!personalized) {
|
||||
const result = await this.sms.sendBulkMessage(
|
||||
recipients.map((r) => r.cellNumber),
|
||||
sample.message,
|
||||
{ source: sample.source },
|
||||
);
|
||||
return {
|
||||
enabled: true,
|
||||
message: 'Message sent successfully',
|
||||
recipientCount: result.recipientCount,
|
||||
serverIds: result.serverIds,
|
||||
source: sample.source,
|
||||
templateId: sample.templateId,
|
||||
templateVerified: sample.templateVerified,
|
||||
};
|
||||
const serverIds: string[] = [];
|
||||
const failures: Array<{ cellNumber: string; error: string }> = [];
|
||||
let sentCount = 0;
|
||||
let balanceExhausted = false;
|
||||
|
||||
for (const payload of payloads) {
|
||||
if (balanceExhausted) {
|
||||
failures.push({
|
||||
cellNumber: payload.cellNumber,
|
||||
error: 'Insufficient SMS balance. Contact Meshkee to top up.',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const serverIds: string[] = [];
|
||||
for (let i = 0; i < personalizedPayloads.length; i++) {
|
||||
const payload = personalizedPayloads[i];
|
||||
try {
|
||||
const result = await this.sms.sendMessage(
|
||||
payload.cellNumber,
|
||||
payload.message,
|
||||
{ source: payload.source },
|
||||
);
|
||||
serverIds.push(result.serverId);
|
||||
} catch (err) {
|
||||
const remaining = personalizedPayloads.slice(i);
|
||||
const refundAmount = remaining.reduce((sum, item) => sum + item.cost, 0);
|
||||
await this.smsBilling.refund(
|
||||
businessId,
|
||||
refundAmount,
|
||||
remaining.length,
|
||||
'mass',
|
||||
);
|
||||
charged = 0;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
message: 'Message sent successfully',
|
||||
recipientCount: serverIds.length,
|
||||
serverIds,
|
||||
source: sample.source,
|
||||
templateId: sample.templateId,
|
||||
templateVerified: sample.templateVerified,
|
||||
};
|
||||
} catch (err) {
|
||||
if (charged > 0) {
|
||||
await this.smsBilling.refund(
|
||||
let charged = 0;
|
||||
try {
|
||||
charged = await this.smsBilling.chargeAmount(
|
||||
businessId,
|
||||
charged,
|
||||
personalized ? personalizedPayloads.length : recipients.length,
|
||||
payload.cost,
|
||||
1,
|
||||
'mass',
|
||||
);
|
||||
} catch (err) {
|
||||
const message = this.smsFailureMessage(err);
|
||||
if (message.toLowerCase().includes('insufficient sms balance')) {
|
||||
balanceExhausted = true;
|
||||
}
|
||||
failures.push({ cellNumber: payload.cellNumber, error: message });
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
err instanceof BadRequestException ||
|
||||
err instanceof ServiceUnavailableException
|
||||
) {
|
||||
throw err;
|
||||
|
||||
try {
|
||||
const result = await this.sms.sendMessage(
|
||||
payload.cellNumber,
|
||||
payload.message,
|
||||
{ source: payload.source },
|
||||
);
|
||||
serverIds.push(result.serverId);
|
||||
sentCount += 1;
|
||||
} catch (err) {
|
||||
if (charged > 0) {
|
||||
await this.smsBilling.refund(businessId, charged, 1, 'mass');
|
||||
}
|
||||
failures.push({
|
||||
cellNumber: payload.cellNumber,
|
||||
error: this.smsFailureMessage(err),
|
||||
});
|
||||
}
|
||||
throw new ServiceUnavailableException('Unable to send SMS');
|
||||
}
|
||||
|
||||
const sample = payloads[0];
|
||||
const failedCount = failures.length;
|
||||
const message =
|
||||
failedCount === 0
|
||||
? 'Message sent successfully'
|
||||
: sentCount === 0
|
||||
? 'No messages were sent'
|
||||
: `Sent ${sentCount} of ${payloads.length}; ${failedCount} failed`;
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
message,
|
||||
recipientCount: payloads.length,
|
||||
sentCount,
|
||||
failedCount,
|
||||
serverIds,
|
||||
failures,
|
||||
source: sample?.source,
|
||||
templateId: sample?.templateId,
|
||||
templateVerified: sample?.templateVerified,
|
||||
};
|
||||
}
|
||||
|
||||
private smsFailureMessage(err: unknown): string {
|
||||
if (err instanceof HttpException) {
|
||||
const response = err.getResponse();
|
||||
if (typeof response === 'string' && response.trim()) {
|
||||
return response.trim();
|
||||
}
|
||||
if (typeof response === 'object' && response !== null && 'message' in response) {
|
||||
const message = (response as { message?: string | string[] }).message;
|
||||
if (Array.isArray(message)) {
|
||||
return message.filter(Boolean).join(', ') || err.message;
|
||||
}
|
||||
if (typeof message === 'string' && message.trim()) {
|
||||
return message.trim();
|
||||
}
|
||||
}
|
||||
return err.message || 'Unable to send SMS';
|
||||
}
|
||||
if (err instanceof Error && err.message.trim()) {
|
||||
return err.message.trim();
|
||||
}
|
||||
return 'Unable to send SMS';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user