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:
Alireza Hassani
2026-09-09 23:30:24 +03:30
co-authored by Cursor
parent db305c58a4
commit aaf7a83742
33 changed files with 1395 additions and 188 deletions
+121 -114
View File
@@ -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';
}
/**