Hold stale tender notification retries for review
This commit is contained in:
+23
-3
@@ -9,7 +9,7 @@ import {deadlineInfo} from '../src/services/tenderCalendar.js';
|
||||
* NOTIFY_MAX=25 → e-postada listelenecek en fazla ihale
|
||||
*/
|
||||
|
||||
import { list, markBaseline, reserveDelivery, acceptDelivery, failDelivery, available } from './notify-store.mjs';
|
||||
import { list, markBaseline, reserveDelivery, acceptDelivery, failDelivery, holdDelivery, available } from './notify-store.mjs';
|
||||
import { emailConfigured, emailFrom, sendEmail, tenderDigestHtml, escapeHtml } from './email.mjs';
|
||||
import { telegramConfigured, sendTelegram, tgKacir } from './telegram.mjs';
|
||||
import { getData } from './user-store.mjs';
|
||||
@@ -62,13 +62,30 @@ export function bildirilebilir(t,now=new Date()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A retry must preserve its original provider request; stale content needs review,
|
||||
// not a rewritten payload under the old idempotency key.
|
||||
export function deliverySourceProblem(delivery,current,clock) {
|
||||
const pinned = delivery.payload.digest;
|
||||
if (!Array.isArray(pinned) || pinned.length !== delivery.tenderIds.length)
|
||||
return 'Gönderim kaynağı doğrulanamadı; otomatik tekrar durduruldu. Yönetici incelemesi gerekli.';
|
||||
const byId = new Map(current.map(t=>[t.id,t]));
|
||||
for (let i=0;i<pinned.length;i++) {
|
||||
const old = pinned[i], fresh = old && byId.get(old.id);
|
||||
if (!old || old.id !== delivery.tenderIds[i] || !fresh || !bildirilebilir(fresh,clock)
|
||||
|| !bildirilebilir(old,clock)
|
||||
|| deadlineInfo(old,clock).deadline?.getTime() !== deadlineInfo(fresh,clock).deadline?.getTime())
|
||||
return 'İhale kaynağı, son teklif zamanı veya başvuru durumu değişti/doğrulanamıyor. Eski e-posta yeniden gönderilmedi; sağlayıcı kaydı ve güncel ilan için yönetici incelemesi gerekli.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tüm aktif abonelikleri işler.
|
||||
* @param {Function} tenderGetir () => Promise<{tenders: []}>
|
||||
*/
|
||||
let processing=false;
|
||||
export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}, dependencies={}) {
|
||||
const {readSubscriptions=list,saveBaseline=markBaseline,reserve=reserveDelivery,accepted=acceptDelivery,failed=failDelivery,configured=emailConfigured,
|
||||
const {readSubscriptions=list,saveBaseline=markBaseline,reserve=reserveDelivery,accepted=acceptDelivery,failed=failDelivery,hold=holdDelivery,configured=emailConfigured,
|
||||
deliverEmail=sendEmail,telegramReady=telegramConfigured,deliverTelegram=sendTelegram,now=()=>new Date()}=dependencies;
|
||||
if(processing)return {atlandi:'önceki tur sürüyor'};
|
||||
processing=true;
|
||||
@@ -81,7 +98,8 @@ export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}, depe
|
||||
if (!abonelikler.length) return { abonelik: 0 };
|
||||
|
||||
const veri = await tenderGetir();
|
||||
const tumu = notificationSource(veri).filter(t=>bildirilebilir(t,now()));
|
||||
const source = notificationSource(veri);
|
||||
const tumu = source.filter(t=>bildirilebilir(t,now()));
|
||||
const requested=Number(process.env.NOTIFY_MAX || 25);
|
||||
const maxAdet=Number.isInteger(requested)&&requested>0?Math.min(requested,200):25;
|
||||
|
||||
@@ -125,6 +143,8 @@ export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}, depe
|
||||
},gonderilecek.map(t=>t.id),{force:zorla});
|
||||
if(!delivery)continue;
|
||||
if(delivery.blocked){hata++;continue;}
|
||||
const sourceProblem=deliverySourceProblem(delivery,source,now());
|
||||
if(sourceProblem){hold(delivery,sourceProblem);hata++;continue;}
|
||||
try {
|
||||
const result=await deliverEmail({...delivery.payload,idempotencyKey:'notify/'+delivery.id});
|
||||
accepted(delivery,result?.id);
|
||||
|
||||
+10
-1
@@ -206,7 +206,8 @@ export function reserveDelivery(subscriptionId,payload,tenderIds,{clock=Date.now
|
||||
db.prepare("INSERT INTO notification_outbox(id,subscription_id,payload,tender_ids,state) VALUES (?,?,?,?,'pending')").run(id,subscriptionId,JSON.stringify(payload),JSON.stringify(tenderIds));
|
||||
row=db.prepare('SELECT * FROM notification_outbox WHERE id=?').get(id);
|
||||
}
|
||||
if(row.state==='uncertain'||(row.first_attempt!==null&&clock-row.first_attempt>=23*3600000)){
|
||||
if(row.state==='uncertain'){db.exec('COMMIT');return {blocked:true};}
|
||||
if(row.first_attempt!==null&&clock-row.first_attempt>=23*3600000){
|
||||
db.prepare("UPDATE notification_outbox SET state='uncertain',error=? WHERE id=?").run('Gönderim sonucu belirsiz; sağlayıcı kaydı kontrol edilmeden tekrar gönderilmez.',row.id);
|
||||
db.exec('COMMIT');return {blocked:true};
|
||||
}
|
||||
@@ -237,6 +238,14 @@ export function failDelivery(delivery,error) {
|
||||
finally{db.close();}
|
||||
}
|
||||
|
||||
/** Stop replay without altering the pinned request or acknowledging delivery. */
|
||||
export function holdDelivery(delivery,reason) {
|
||||
const db=open();
|
||||
try{return db.prepare("UPDATE notification_outbox SET state='uncertain',error=? WHERE id=? AND lease=? AND state='sending'")
|
||||
.run(String(reason),delivery.id,delivery.lease).changes>0;}
|
||||
finally{db.close();}
|
||||
}
|
||||
|
||||
export function deliveryQueue() {
|
||||
const db=open();
|
||||
try{return db.prepare("SELECT o.*,r.actor,r.action,r.note,r.reviewed_at FROM notification_outbox o LEFT JOIN notification_outbox_reviews r ON r.id=o.id WHERE o.state IN ('pending','sending','uncertain','sent_manual','skipped') ORDER BY o.first_attempt DESC LIMIT 50").all().map(row=>{
|
||||
|
||||
Reference in New Issue
Block a user