Skip to content

Commit 6a5867b

Browse files
authored
fix(#10802): check status before a scheduled_task is updated (#10803)
#10802
1 parent 7303e40 commit 6a5867b

File tree

3 files changed

+157
-2
lines changed

3 files changed

+157
-2
lines changed

shared-libs/transitions/src/schedule/due_tasks.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const lineage = require('@medic/lineage')(Promise, db.medic);
1111
const messageUtils = require('@medic/message-utils');
1212

1313
const BATCH_SIZE = 1000;
14+
const SCHEDULED_STATE = 'scheduled';
1415

1516
const getTemplateContext = async (doc) => {
1617
const context = {
@@ -44,6 +45,12 @@ const updateScheduledTasks = (doc, context, dueDates, clearFailing=false) => {
4445
let updatedTasks = false;
4546
// set task to pending for gateway to pick up
4647
doc.scheduled_tasks.forEach(task => {
48+
// only process tasks that are still in 'scheduled' state - skip tasks that have already
49+
// progressed to other states (e.g. pending, sent, delivered) to prevent re-sending
50+
if (task.state !== SCHEDULED_STATE) {
51+
return;
52+
}
53+
4754
// use the same due calculation as the `messages_by_state` view
4855
let due = task.due || task.timestamp || doc.reported_date;
4956
if (typeof due !== 'string') {
@@ -160,8 +167,8 @@ module.exports = {
160167
const overdue = now.clone().subtract(7, 'days');
161168
const opts = {
162169
include_docs: true,
163-
endkey: JSON.stringify([ 'scheduled', now.valueOf() ]),
164-
startkey: JSON.stringify([ 'scheduled', overdue.valueOf() ]),
170+
endkey: JSON.stringify([ SCHEDULED_STATE, now.valueOf() ]),
171+
startkey: JSON.stringify([ SCHEDULED_STATE, overdue.valueOf() ]),
165172
limit: BATCH_SIZE,
166173
};
167174

shared-libs/transitions/test/unit/due_tasks.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,4 +1971,83 @@ describe('due tasks', () => {
19711971
});
19721972
});
19731973

1974+
it('should not set already-sent tasks to pending when another task with the same due date is scheduled', () => {
1975+
// Reproduction of https://github.com/medic/cht-core/issues/10802
1976+
//
1977+
// Scenario: A document has two scheduled_tasks with the same due date.
1978+
// Task A (e.g. "Vaccination Day") has been sent successfully.
1979+
// Task B (e.g. "Age Based") is stuck in scheduled state because it cannot generate messages.
1980+
// The view returns the doc because of Task B. dueTasks collects the due date,
1981+
// then iterates ALL scheduled_tasks matching that due date — including Task A.
1982+
// Bug: Task A gets reset from sent back to pending.
1983+
const due = moment();
1984+
const id = 'report-with-duplicate-due';
1985+
1986+
const doc = {
1987+
scheduled_tasks: [
1988+
{
1989+
// Task A: already sent successfully
1990+
due: due.toISOString(),
1991+
state: 'sent',
1992+
state_history: [
1993+
{ state: 'scheduled', timestamp: moment().subtract(10, 'days').toISOString() },
1994+
{ state: 'pending', timestamp: moment().subtract(1, 'day').toISOString() },
1995+
{ state: 'sent', timestamp: moment().subtract(1, 'day').toISOString() },
1996+
],
1997+
type: 'Immunization Reminders Vaccination Day',
1998+
messages: [
1999+
{
2000+
to: '+1234567890',
2001+
uuid: 'msg-uuid-1',
2002+
message: 'Please visit the health facility',
2003+
},
2004+
],
2005+
},
2006+
{
2007+
// Task B: stuck in scheduled, same due date, no messages (generation fails)
2008+
due: due.toISOString(),
2009+
state: 'scheduled',
2010+
state_history: [
2011+
{ state: 'scheduled', timestamp: moment().subtract(10, 'days').toISOString() },
2012+
],
2013+
type: 'Immunization Reminders Age Based',
2014+
message_key: 'some.translation.key',
2015+
recipient: 'clinic',
2016+
// no messages — generation will fail
2017+
},
2018+
],
2019+
};
2020+
2021+
// The view returns this doc because Task B is in 'scheduled' state
2022+
const view = sinon.stub(request, 'get').resolves({
2023+
rows: [
2024+
{
2025+
id: id,
2026+
key: ['scheduled', due.valueOf()],
2027+
doc: doc,
2028+
},
2029+
],
2030+
});
2031+
2032+
sinon.stub(schedule._lineage, 'hydrateDocs').resolves([doc]);
2033+
sinon.stub(utils, 'getRegistrations').resolves([]);
2034+
// translate returns empty to simulate failed message generation for Task B
2035+
sinon.stub(utils, 'translate').returns('');
2036+
2037+
const saveDoc = sinon.stub(db.medic, 'put').resolves({});
2038+
2039+
return schedule.execute().then(() => {
2040+
assert.equal(view.callCount, 1);
2041+
2042+
// Task A should NOT have been touched — it's already sent
2043+
assert.equal(doc.scheduled_tasks[0].state, 'sent', 'Task A (already sent) should not have its state changed');
2044+
2045+
// Task B should still be scheduled (message generation failed)
2046+
assert.equal(doc.scheduled_tasks[1].state, 'scheduled', 'Task B should remain scheduled');
2047+
2048+
// The document should NOT be saved since no valid state changes occurred
2049+
assert.equal(saveDoc.callCount, 0, 'Document should not be saved when no tasks were validly updated');
2050+
});
2051+
});
2052+
19742053
});

tests/integration/sentinel/schedules/due-tasks.spec.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,48 @@ const contacts = [
100100
},
101101
];
102102

103+
const reportWithDuplicateDueDate = {
104+
_id: 'report_duplicate_due',
105+
type: 'data_record',
106+
contact: {
107+
_id: 'chw1',
108+
parent: { _id: 'clinic1', parent: { _id: CONTACT_TYPES.HEALTH_CENTER, parent: { _id: 'district_hospital' } } }
109+
},
110+
fields: { patient_id: 'patient1', value: 5 },
111+
reported_date: oneMonthAgo,
112+
scheduled_tasks: [
113+
{
114+
// Task A: already sent, same due date as Task B
115+
due: twoDaysAgo,
116+
message_key: 'messages.one',
117+
recipient: 'clinic',
118+
state_history: [
119+
{ state: 'scheduled', timestamp: oneMonthAgo },
120+
{ state: 'pending', timestamp: threeDaysAgo },
121+
{ state: 'sent', timestamp: threeDaysAgo },
122+
],
123+
state: 'sent',
124+
messages: [
125+
{
126+
to: '111222',
127+
uuid: 'uuid-already-sent',
128+
message: 'ONE. Reported by Chw1. Patient Patient1 (patient1). Value 5',
129+
},
130+
],
131+
},
132+
{
133+
// Task B: stuck in scheduled, same due date as Task A, missing translation
134+
due: twoDaysAgo,
135+
message_key: 'non.exisiting.key',
136+
recipient: 'clinic',
137+
state_history: [
138+
{ state: 'scheduled', timestamp: oneMonthAgo },
139+
],
140+
state: 'scheduled',
141+
},
142+
],
143+
};
144+
103145
const reports = [
104146
{
105147
_id: 'report1', // no tasks
@@ -499,4 +541,31 @@ describe('Due Tasks', () => {
499541
});
500542
chai.expect(report7.scheduled_tasks[3].messages).to.equal(undefined);
501543
});
544+
545+
it('should not reset already-sent tasks when another task with the same due date is stuck in scheduled', async () => {
546+
// Reproduction of https://github.com/medic/cht-core/issues/10802
547+
// When a document has two scheduled_tasks with the same due date and one is stuck in
548+
// 'scheduled' state (e.g. missing translation), dueTasks should NOT reset the other
549+
// task that has already been sent back to 'pending'.
550+
await sentinelUtils.waitForSentinel();
551+
await utils.toggleSentinelTransitions();
552+
await utils.saveDoc(reportWithDuplicateDueDate);
553+
await utils.toggleSentinelTransitions();
554+
await utils.runSentinelTasks();
555+
await sentinelUtils.waitForSentinel([reportWithDuplicateDueDate._id]);
556+
557+
// Wait briefly for dueTasks scheduler to process
558+
await new Promise(resolve => setTimeout(resolve, 5000));
559+
560+
const report = await utils.getDoc(reportWithDuplicateDueDate._id);
561+
562+
// Task A (already sent) should NOT have been changed to pending
563+
chai.expect(report.scheduled_tasks[0].state).to.equal('sent',
564+
'Already-sent task should not be reset to pending when another task with the same due date is scheduled');
565+
566+
// Task B (stuck with missing translation) should remain scheduled
567+
chai.expect(report.scheduled_tasks[1].state).to.equal('scheduled',
568+
'Task with missing translation should remain in scheduled state');
569+
chai.expect(report.scheduled_tasks[1].messages).to.equal(undefined);
570+
});
502571
});

0 commit comments

Comments
 (0)