feat: review uncertain weekly brief deliveries with audit history

This commit is contained in:
2026-09-06 14:37:32 +03:00
parent 99a7f2f571
commit 7a01e4bed7
11 changed files with 141 additions and 15 deletions
+37
View File
@@ -97,3 +97,40 @@ test('weekly id uses Monday in Istanbul even while UTC is still Sunday',()=>{
assert.equal(briefWeekKey(new Date('2026-09-06T20:59:59Z')),'2026-08-31');
assert.equal(briefWeekKey(new Date('2026-09-06T21:00:00Z')),'2026-09-07');
});
test('review needs provider evidence, persists actor and closes only the selected message',t=>{
const {outbox,directory}=fixture(t);
const batch=outbox.prepare('2026-09-07','review@example.test',[{channel:'email',to:'review@example.test'},{channel:'telegram',chatId:'test'}]);
const first=outbox.claim(batch.id,0,1000),second=outbox.claim(batch.id,1,1000);
outbox.hold(first,'Unknown response');outbox.hold(second,'Unknown response');
const id=`${batch.id}:0`,input={action:'confirmed_sent',note:'Sağlayıcı kayıtları incelendi ve eşleşti.',providerId:'provider-test-id',confirmed:true};
assert.throws(()=>outbox.resolve(id,'admin@example.test',{...input,confirmed:false}),/onayı/);
assert.throws(()=>outbox.resolve(id,'admin@example.test',{...input,providerId:''}),/kimliği/);
assert.throws(()=>outbox.resolve(id,'admin@example.test',{...input,note:'short'}),/açıklaması/);
assert.equal(outbox.resolve(id,'admin@example.test',input).state,'sent_manual');
assert.throws(()=>outbox.resolve(id,'other-admin@example.test',input),{status:409});
const rows=createBriefOutbox(directory).queue(10000),done=rows.find(row=>row.id===id);
assert.equal(done.review.actor,'admin@example.test');assert.equal(done.review.note,input.note);
assert.equal(done.providerId,'provider-test-id');assert.equal(done.state,'sent_manual');
assert.equal(rows.find(row=>row.id!==id).state,'uncertain');
assert.equal(outbox.claim(batch.id,0).state,'accepted');
});
test('skip never claims delivery and a fully reviewed changed plan cannot start new messages',t=>{
const {outbox}=fixture(t);
const batch=outbox.prepare('2026-09-07','review@example.test',[{channel:'email'}]);
outbox.hold(outbox.claim(batch.id,0),'Uncertain');
outbox.resolve(`${batch.id}:0`,'admin@example.test',{action:'skip',note:'Sonuç doğrulanamadı, tekrar gönderilmeden kapatıldı.'});
assert.equal(outbox.claim(batch.id,0).state,'skipped');
const row=outbox.queue()[0];assert.equal(row.providerId,null);assert.equal(row.state,'skipped');
assert.equal(outbox.prepare('2026-09-07','review@example.test',[{channel:'email',text:'Changed'},{channel:'telegram'}]).closed,true);
});
test('review queue exposes expired interrupted sends but does not allow reviewing a live claim',t=>{
const {outbox}=fixture(t);
const batch=outbox.prepare('2026-09-07','review@example.test',[{channel:'email'}]);
outbox.claim(batch.id,0,1000);
assert.equal(outbox.queue(2000)[0].state,'sending');
assert.throws(()=>outbox.resolve(`${batch.id}:0`,'admin@example.test',{action:'skip',note:'Bu gönderim halen etkin durumdadır.'}),{status:409});
assert.equal(outbox.queue(121001)[0].state,'uncertain');
});
+4
View File
@@ -15,6 +15,10 @@ const keyUser={email:'keys-browser-qa@example.test',name:'API QA',isAdmin:true};
const keyToken=signToken(keyUser);
let apiKeyListFailure=false;
process.env.IHALEIST_USER_DATA_DIR=dir;
const {createBriefOutbox}=await import('../server/brief-outbox.mjs');
const qaBriefOutbox=createBriefOutbox(dir);
const qaBriefBatch=qaBriefOutbox.prepare('2026-09-07','weekly-browser@example.test',[{channel:'email',to:'weekly-browser@example.test'},{channel:'telegram',chatId:'synthetic-chat'}]);
for(const position of [0,1])qaBriefOutbox.hold(qaBriefOutbox.claim(qaBriefBatch.id,position),'QA: Sağlayıcı sonucu belirsiz.');
const qaJobs=await import('../server/ai-job-store.mjs');
const {qualifyOfferDraft}=await import('../server/offer-draft.mjs');
const {executeAnalysisJob}=await import('../server/analysis-job.mjs');
+22
View File
@@ -265,3 +265,25 @@ test('weekly brief preference loads without feed preview and changes only with a
assert.equal((await request('/api/me/brief','GET',null,other)).data.kapali,false);
assert.equal((await request('/api/me/brief','POST',{kapali:false,revision:saved.data.revision})).status,200);
});
test('weekly brief review is admin-only and persists evidence without sending messages',async()=>{
const route='/api/admin/brief/outbox';
assert.equal((await request(route,'GET',null,null)).status,401);
assert.equal((await request(route)).status,403);
assert.equal((await request(route,'POST',{id:'x',action:'skip',note:'Yetkisiz işlem kontrolü.'})).status,403);
const admin=signToken({email:'weekly-admin@example.test',name:'Admin',isAdmin:true});
const {createBriefOutbox}=await import('../server/brief-outbox.mjs');
const box=createBriefOutbox(dir),batch=box.prepare('2026-09-07','weekly-test@example.test',[{channel:'email',to:'weekly-test@example.test'}]);
box.hold(box.claim(batch.id,0),'Test sonucu belirsiz');
const list=await request(route,'GET',null,admin);
assert.equal(list.status,200);assert.equal(list.data.deliveries.length,1);
const id=list.data.deliveries[0].id;
assert.equal((await request(route,'DELETE',null,admin)).status,405);
const input={id,action:'confirmed_sent',providerId:'test-provider-receipt',confirmed:true,note:'İzole test sağlayıcı kaydı doğrulandı.'};
assert.equal((await request(route,'POST',{...input,confirmed:false},admin)).status,400);
assert.equal((await request(route,'POST',input,admin)).status,200);
assert.equal((await request(route,'POST',input,admin)).status,409);
const saved=(await request(route,'GET',null,admin)).data.deliveries[0];
assert.equal(saved.review.actor,'weekly-admin@example.test');assert.equal(saved.providerId,input.providerId);
assert.equal(saved.state,'sent_manual');
});