feat: integrate analytics and fragment receipt improvements
This commit is contained in:
@@ -9,6 +9,7 @@ import type { GatewaySubmitResultDto, GatewaySubmitSegmentResultDto, GatewayRece
|
||||
import { positiveInteger, normalizeReceiptStatus, normalizeCarrier, DEFAULT_DOWNSTREAM_RETRY_DELAY_MS, DEFAULT_DOWNSTREAM_RETRY_MAX_DELAY_MS, DEFAULT_DOWNSTREAM_MAX_RETRIES, DEFAULT_DOWNSTREAM_PENDING_TIMEOUT_HOURS, DEFAULT_RECEIPT_TIMEOUT_HOURS, DEFAULT_GATEWAY_SUBMIT_REQUEUE_STALE_MS, DEFAULT_DOWNSTREAM_MANUAL_REQUEUE_STALE_MS, DEFAULT_UPSTREAM_RECEIPT_INBOX_PROCESSING_STALE_MS, DEFAULT_UPSTREAM_RECEIPT_INBOX_MAX_ATTEMPTS, DEFAULT_UPSTREAM_RECEIPT_INBOX_MAX_AGE_HOURS, gatewaySubmitRequeueKey, isObjectRecord, asDateOrNull, downstreamRetryDelayMs, downstreamAckTimeoutMs, downstreamRetryBaseDelayMs, downstreamRetryMaxDelayMs, downstreamMaxRetries, downstreamPendingTimeoutHours, downstreamControlFailureMessage, normalizeSubmitStatus, downstreamDeliveryAttemptKey, hasRecoveryAuditStateChanged, normalizeRecoveryFailureCategory, aggregateReceiptSegmentState, isSameUpstreamEndpointIdentity, receiptEventKey } from './send-chain.helpers';
|
||||
import type { SendSubmissionService } from './send-submission.service';
|
||||
import type { SendCompletionCallbacks, SendCompletionFacade } from './send-completion.service';
|
||||
import { queueFinalReceiptDeliveries } from './downstream-receipt-targets';
|
||||
|
||||
|
||||
/**
|
||||
@@ -33,23 +34,76 @@ export class SendTimeoutService {
|
||||
const candidates = await this.prisma.smsMessageRecord.findMany({
|
||||
where: {
|
||||
tenantId: { not: null },
|
||||
status: { in: ['submitted', 'unknown'] },
|
||||
submittedAt: { lte: cutoff },
|
||||
OR: [
|
||||
{ status: { in: ['submitted', 'unknown'] }, submittedAt: { lte: cutoff } },
|
||||
{ status: 'timeout', errorCode: 'RECEIPT_TIMEOUT', timeoutReceiptQueuedAt: null },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
tenantId: true,
|
||||
batchTaskId: true,
|
||||
applicationId: true,
|
||||
messageId: true,
|
||||
phoneNumber: true,
|
||||
amountCents: true,
|
||||
billingUnits: true,
|
||||
status: true,
|
||||
cmppSubmitSequenceId: true,
|
||||
cmppSubmitGroupMessageId: true,
|
||||
cmppRegisteredDelivery: true,
|
||||
timeoutAt: true,
|
||||
},
|
||||
select: { id: true, tenantId: true, batchTaskId: true, messageId: true, amountCents: true, billingUnits: true },
|
||||
take: 10000,
|
||||
});
|
||||
const timedOutTaskIds = new Set<string>();
|
||||
let timeout = 0;
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate.tenantId) continue;
|
||||
const transitioned = await this.prisma.smsMessageRecord.updateMany({
|
||||
where: { id: candidate.id, status: { in: ['submitted', 'unknown'] } },
|
||||
data: { status: 'timeout', timeoutAt: new Date(), errorMessage: `${olderThanHours}小时未收到明确回执,自动转超时` },
|
||||
});
|
||||
if (transitioned.count !== 1) continue;
|
||||
timeout += 1;
|
||||
const timedOutAt = candidate.timeoutAt ?? new Date();
|
||||
if (candidate.status !== 'timeout') {
|
||||
const transitioned = await this.prisma.smsMessageRecord.updateMany({
|
||||
where: { id: candidate.id, status: { in: ['submitted', 'unknown'] } },
|
||||
data: {
|
||||
status: 'timeout',
|
||||
receiptStatus: 'undelivered',
|
||||
receiptRawStatus: 'EXPIRED',
|
||||
errorCode: 'RECEIPT_TIMEOUT',
|
||||
errorMessage: `${olderThanHours}小时未收到明确回执,自动转超时`,
|
||||
timeoutAt: timedOutAt,
|
||||
},
|
||||
});
|
||||
if (transitioned.count !== 1) continue;
|
||||
timeout += 1;
|
||||
}
|
||||
// Refund uses the platform-message idempotency key. Re-running it for a
|
||||
// timeout whose downstream outbox was not fully queued also recovers a
|
||||
// crash between the state transition and the original refund call.
|
||||
await this.facade.refundMessage(candidate as typeof candidate & { tenantId: string }, `${olderThanHours}小时未收到明确回执,自动超时退款`);
|
||||
const queued = await queueFinalReceiptDeliveries(
|
||||
this.prisma,
|
||||
(request) => this.facade.queueAndTryDownstreamDelivery(request),
|
||||
{
|
||||
message: candidate,
|
||||
payload: {
|
||||
messageId: candidate.messageId,
|
||||
gatewayMessageId: `PLATFORM_TIMEOUT:${candidate.messageId}`,
|
||||
phoneNumber: candidate.phoneNumber,
|
||||
receiptStatus: 'undelivered',
|
||||
rawStatus: 'EXPIRED',
|
||||
errorCode: 'RECEIPT_TIMEOUT',
|
||||
errorMessage: `${olderThanHours}小时未收到明确回执,自动转超时`,
|
||||
deliveredAt: timedOutAt.toISOString(),
|
||||
},
|
||||
propagateHttpQueueError: true,
|
||||
},
|
||||
);
|
||||
if (queued.queued) {
|
||||
await this.prisma.smsMessageRecord.updateMany({
|
||||
where: { id: candidate.id, status: 'timeout', timeoutReceiptQueuedAt: null },
|
||||
data: { timeoutReceiptQueuedAt: new Date() },
|
||||
});
|
||||
}
|
||||
if (candidate.batchTaskId) timedOutTaskIds.add(candidate.batchTaskId);
|
||||
}
|
||||
for (const batchTaskId of timedOutTaskIds) {
|
||||
|
||||
Reference in New Issue
Block a user