import {z} from "zod" import type {TelegramSendResult} from "../alerts-telegram-send.js" const payloadSchema=z.object({text:z.string().min(1),chatId:z.string().optional(),history:z.object({id:z.string().min(1),ruleId:z.string().nullable().optional(),groupId:z.string().nullable().optional(),ruleName:z.string(),severity:z.enum(["critical","warning","info"]),message:z.string(),firedAt:z.string().refine(value=>Number.isFinite(Date.parse(value)))}).strict()}).strict() export type AlertOutboxPayload=z.output export type OutboxRow={id:string;retryCount:number;maxRetries:number;payloadJson:unknown} export type OutboxSettlement={status:"pending"|"sent"|"failed"|"unknown";retryCount?:number;nextAttemptAt?:string;sentAt?:string;lastError:string|null;history?:AlertOutboxPayload["history"]&{sentOk:boolean}} type Result={kind:"sent"|"failed"|"retry"|"held"|"skipped";apiAccepted?:true;error?:string} interface Dependencies{claim:(id:string)=>Promise;send:(payload:AlertOutboxPayload)=>Promise;settle:(id:string,value:OutboxSettlement)=>Promise;now:()=>number} export async function executeOutboxDelivery(id:string,deps:Dependencies):Promise{ let row:OutboxRow|undefined try{row=await deps.claim(id)}catch{return {kind:"held",error:"Не подтверждён захват сообщения; отправка не начата"}} if(!row)return {kind:"skipped"} if(row.id!==id)return {kind:"held",error:"Захвачено другое сообщение; отправка не начата"} async function hold(error:string,apiAccepted?:true):Promise{ try{await deps.settle(id,{status:"unknown",lastError:error})}catch{return {kind:"held",apiAccepted,error:error+". Сохранение статуса не подтверждено; processing не повторяется автоматически"}} return {kind:"held",apiAccepted,error} } let raw=row.payloadJson if(typeof raw==="string"){try{raw=JSON.parse(raw)}catch{raw=null}} const parsed=payloadSchema.safeParse(raw) if(!parsed.success||!Number.isInteger(row.retryCount)||row.retryCount<0||!Number.isInteger(row.maxRetries)||row.maxRetries<1){ try{await deps.settle(id,{status:"failed",lastError:"Некорректная запись очереди"});return {kind:"failed"}}catch{return hold("Не удалось отклонить некорректную запись очереди")} } const payload=parsed.data let send:TelegramSendResult try{send=await deps.send(payload)}catch{return hold("Результат отправки неизвестен: ответ не получен")} if(send?.ok===true){ try{await deps.settle(id,{status:"sent",sentAt:new Date(deps.now()).toISOString(),lastError:null,history:{...payload.history,sentOk:true}});return {kind:"sent",apiAccepted:true}} catch{return hold("Telegram API принял сообщение, но запись результата в БД не подтверждена",true)} } if(send?.ok!==false||send.outcome!=="rejected")return hold(send?.ok===false?send.error:"Telegram не подтвердил результат отправки") const retryCount=row.retryCount+1,exhausted=retryCount>=row.maxRetries try{await deps.settle(id,{retryCount,status:exhausted?"failed":"pending",nextAttemptAt:new Date(deps.now()+Math.min(300_000,retryCount*15_000)).toISOString(),lastError:send.error,...(exhausted?{history:{...payload.history,sentOk:false}}:{})});return {kind:exhausted?"failed":"retry",error:send.error}} catch{return hold("Отказ Telegram получен, но запись следующей попытки не подтверждена")} }