feat: T4 wave 5 - colony end cleanup, withdrawal chain, next steps, inspection takeover UI
- Colony end releases the queen in the same transaction; open suggestions become obsolete and a CLEANUP suggestion lists open tasks and the queen; apply-cleanup validates evidence and blocks reactivated colonies - Withdrawal chain: plausibility warnings on harvest and bottling, conflict notifications also when a treatment is entered or corrected after the harvest (naming bottled lots), withdrawal spans in the calendar and M365 sync, Berlin-day end events - Next 3 steps per colony and the "Heute" endpoint with ranking and an explicit merge table by intent - Inspection-triggered rule suggestions follow the colony's latest inspection (resolve when the finding is gone, refresh when it stays) - Frontend: colony card takeover in the inspection form, feed status as condition, occupied frames 0-40; legacy offline entries keep syncing - Notifications and preferences are deleted with their user Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
02eb16924e
commit
ef33e5549b
122 files changed
+10707
-504
No files matched your search
@@ -42,6 +42,7 @@ describe('AutomationEventHandlers', () => {
|
||||
let tx: Record<RecordModel, { findUnique: jest.Mock }>;
|
||||
let automation: Record<
|
||||
| 'evaluateInspectionIn'
|
||||
| 'evaluateInspectionCorrectionIn'
|
||||
| 'evaluateVarroaMeasurementIn'
|
||||
| 'evaluateTreatmentIn'
|
||||
| 'evaluateFeedingIn'
|
||||
@@ -63,6 +64,7 @@ describe('AutomationEventHandlers', () => {
|
||||
) as Record<RecordModel, { findUnique: jest.Mock }>;
|
||||
automation = {
|
||||
evaluateInspectionIn: jest.fn().mockResolvedValue(undefined),
|
||||
evaluateInspectionCorrectionIn: jest.fn().mockResolvedValue(undefined),
|
||||
evaluateVarroaMeasurementIn: jest.fn().mockResolvedValue(undefined),
|
||||
evaluateTreatmentIn: jest.fn().mockResolvedValue(undefined),
|
||||
evaluateFeedingIn: jest.fn().mockResolvedValue(undefined),
|
||||
@@ -84,7 +86,7 @@ describe('AutomationEventHandlers', () => {
|
||||
|
||||
expect(registered('inspectionRules')).toEqual([
|
||||
{
|
||||
types: ['InspectionRecorded'],
|
||||
types: ['InspectionRecorded', 'InspectionCorrected'],
|
||||
handlerId: 'automation.inspection-rules',
|
||||
},
|
||||
]);
|
||||
@@ -208,6 +210,21 @@ describe('AutomationEventHandlers', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('a corrected inspection only follows open suggestions', async () => {
|
||||
tx.inspection.findUnique.mockResolvedValueOnce({
|
||||
id: 'record-1',
|
||||
colonyId: 'colony-1',
|
||||
});
|
||||
tx.colony.findUnique.mockResolvedValueOnce({ status: 'ACTIVE' });
|
||||
|
||||
await run('inspectionRules', 'InspectionCorrected' as never);
|
||||
|
||||
expect(automation.evaluateInspectionCorrectionIn).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
expect(automation.evaluateInspectionIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('lets an evaluation failure propagate, so the delivery is retried', async () => {
|
||||
tx.inspection.findUnique.mockResolvedValueOnce({
|
||||
id: 'record-1',
|
||||
|
||||
@@ -35,9 +35,14 @@ export class AutomationEventHandlers {
|
||||
return colony !== null && isLivingColonyStatus(colony.status);
|
||||
}
|
||||
|
||||
@OnDomainEvent('InspectionRecorded', 'automation.inspection-rules')
|
||||
// A correction proposes nothing new; it only lets an open suggestion
|
||||
// follow the corrected values.
|
||||
@OnDomainEvent(
|
||||
['InspectionRecorded', 'InspectionCorrected'],
|
||||
'automation.inspection-rules',
|
||||
)
|
||||
async inspectionRules(
|
||||
event: AnyDomainEventEnvelope<'InspectionRecorded'>,
|
||||
event: AnyDomainEventEnvelope<'InspectionRecorded' | 'InspectionCorrected'>,
|
||||
tx: Tx,
|
||||
): Promise<void> {
|
||||
const inspection = await tx.inspection.findUnique({
|
||||
@@ -49,7 +54,11 @@ export class AutomationEventHandlers {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await this.automation.evaluateInspectionIn(tx, inspection);
|
||||
if (event.type === 'InspectionRecorded') {
|
||||
await this.automation.evaluateInspectionIn(tx, inspection);
|
||||
} else {
|
||||
await this.automation.evaluateInspectionCorrectionIn(tx, inspection);
|
||||
}
|
||||
}
|
||||
|
||||
// A correction can lift a count over the threshold, so it is evaluated
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('AutomationService', () => {
|
||||
};
|
||||
colony: { findUnique: jest.Mock };
|
||||
suggestion: { findFirst: jest.Mock };
|
||||
inspection: { findFirst: jest.Mock };
|
||||
treatment: { findMany: jest.Mock };
|
||||
stockMovement: { findFirst: jest.Mock };
|
||||
rawMaterialLot: { findFirst: jest.Mock };
|
||||
@@ -90,6 +91,16 @@ describe('AutomationService', () => {
|
||||
};
|
||||
|
||||
const asTx = () => tx as unknown as Prisma.TransactionClient;
|
||||
// The inspection rules decide on the colony's latest inspection: the one
|
||||
// under test is it.
|
||||
const evaluateInspection = (inspection: never) => {
|
||||
tx.inspection.findFirst.mockResolvedValue(inspection);
|
||||
return service.evaluateInspectionIn(asTx(), inspection);
|
||||
};
|
||||
const evaluateInspectionCorrection = (inspection: never) => {
|
||||
tx.inspection.findFirst.mockResolvedValue(inspection);
|
||||
return service.evaluateInspectionCorrectionIn(asTx(), inspection);
|
||||
};
|
||||
const withRules = (...list: Rule[]) => {
|
||||
for (const rule of list) {
|
||||
rules.set(rule.triggerType, rule);
|
||||
@@ -125,6 +136,7 @@ describe('AutomationService', () => {
|
||||
}),
|
||||
},
|
||||
suggestion: { findFirst: jest.fn().mockResolvedValue(null) },
|
||||
inspection: { findFirst: jest.fn().mockResolvedValue(null) },
|
||||
treatment: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
stockMovement: { findFirst: jest.fn().mockResolvedValue(null) },
|
||||
rawMaterialLot: { findFirst: jest.fn().mockResolvedValue(null) },
|
||||
@@ -502,7 +514,7 @@ describe('AutomationService', () => {
|
||||
});
|
||||
|
||||
it('read the rules on the given transaction client and write no seed', async () => {
|
||||
await service.evaluateInspectionIn(asTx(), queenlessInspection as never);
|
||||
await evaluateInspection(queenlessInspection as never);
|
||||
|
||||
expect(tx.automationRule.findUnique).toHaveBeenCalled();
|
||||
expect(prisma.automationRule.createMany).not.toHaveBeenCalled();
|
||||
@@ -520,7 +532,7 @@ describe('AutomationService', () => {
|
||||
it('proposes a queen check anchored on the inspection when the queen was not seen and no eggs are present', async () => {
|
||||
withRules(queenlessRule);
|
||||
|
||||
await service.evaluateInspectionIn(asTx(), queenlessInspection as never);
|
||||
await evaluateInspection(queenlessInspection as never);
|
||||
|
||||
expect(propose).toHaveBeenCalledTimes(1);
|
||||
expect(propose).toHaveBeenCalledWith(tx, 'QUEENLESS_NO_EGGS', {
|
||||
@@ -543,7 +555,7 @@ describe('AutomationService', () => {
|
||||
it('dates the suggestion from the inspection day when the rule has dueInDays', async () => {
|
||||
withRules({ ...queenlessRule, config: { dueInDays: 3 } });
|
||||
|
||||
await service.evaluateInspectionIn(asTx(), queenlessInspection as never);
|
||||
await evaluateInspection(queenlessInspection as never);
|
||||
|
||||
expect(propose).toHaveBeenCalledWith(
|
||||
tx,
|
||||
@@ -555,7 +567,7 @@ describe('AutomationService', () => {
|
||||
it('does not trigger when the queen was seen', async () => {
|
||||
withRules(queenlessRule);
|
||||
|
||||
await service.evaluateInspectionIn(asTx(), {
|
||||
await evaluateInspection({
|
||||
...queenlessInspection,
|
||||
queenSeen: true,
|
||||
} as never);
|
||||
@@ -566,7 +578,7 @@ describe('AutomationService', () => {
|
||||
it('proposes feeding when the feed status is CRITICAL', async () => {
|
||||
withRules(feedRule);
|
||||
|
||||
await service.evaluateInspectionIn(asTx(), {
|
||||
await evaluateInspection({
|
||||
...queenlessInspection,
|
||||
id: 'inspection-3',
|
||||
queenSeen: true,
|
||||
@@ -590,7 +602,7 @@ describe('AutomationService', () => {
|
||||
it('proposes both findings of one inspection', async () => {
|
||||
withRules(queenlessRule, feedRule);
|
||||
|
||||
await service.evaluateInspectionIn(asTx(), {
|
||||
await evaluateInspection({
|
||||
...queenlessInspection,
|
||||
feedStatus: 'CRITICAL',
|
||||
} as never);
|
||||
@@ -604,7 +616,7 @@ describe('AutomationService', () => {
|
||||
{ ...feedRule, active: false },
|
||||
);
|
||||
|
||||
await service.evaluateInspectionIn(asTx(), {
|
||||
await evaluateInspection({
|
||||
...queenlessInspection,
|
||||
feedStatus: 'CRITICAL',
|
||||
} as never);
|
||||
@@ -613,7 +625,7 @@ describe('AutomationService', () => {
|
||||
});
|
||||
|
||||
it('reads no rule when there is no finding', async () => {
|
||||
await service.evaluateInspectionIn(asTx(), {
|
||||
await evaluateInspection({
|
||||
...queenlessInspection,
|
||||
queenSeen: true,
|
||||
} as never);
|
||||
@@ -622,6 +634,212 @@ describe('AutomationService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('open inspection suggestions follow the latest inspection', () => {
|
||||
let propose: jest.SpyInstance;
|
||||
const latestOf = (overrides: Record<string, unknown> = {}) =>
|
||||
({
|
||||
...queenlessInspection,
|
||||
id: 'inspection-2',
|
||||
date: new Date('2026-05-12T00:00:00.000Z'),
|
||||
createdAt: new Date('2026-05-12T08:00:00.000Z'),
|
||||
queenSeen: true,
|
||||
eggsPresent: true,
|
||||
...overrides,
|
||||
}) as never;
|
||||
const lockedTables = () =>
|
||||
(tx.$queryRaw.mock.calls as [Prisma.Sql][]).map(
|
||||
([query]) => /FROM "(\w+)"/.exec(query.sql)?.[1],
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
propose = jest.spyOn(service, 'proposeForRuleIn').mockResolvedValue(null);
|
||||
withRules(queenlessRule, feedRule);
|
||||
tx.suggestion.findFirst.mockImplementation(
|
||||
({ where }: { where: { ruleKey: string } }) =>
|
||||
Promise.resolve(
|
||||
where.ruleKey === 'QUEENLESS_NO_EGGS' ? { id: 'open-1' } : null,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('locks the colony first, reads the latest inspection and the open suggestion under it, then locks the suggestion', async () => {
|
||||
await evaluateInspection(latestOf());
|
||||
|
||||
expect(lockedTables()).toEqual(['Colony', 'Suggestion']);
|
||||
const [colonyLock, suggestionLock] =
|
||||
tx.$queryRaw.mock.invocationCallOrder;
|
||||
const [latestRead] = tx.inspection.findFirst.mock.invocationCallOrder;
|
||||
const [openRead] = tx.suggestion.findFirst.mock.invocationCallOrder;
|
||||
expect(colonyLock).toBeLessThan(latestRead);
|
||||
expect(latestRead).toBeLessThan(openRead);
|
||||
expect(openRead).toBeLessThan(suggestionLock);
|
||||
});
|
||||
|
||||
it('takes the latest inspection by date, then creation time, then id', async () => {
|
||||
await evaluateInspection(latestOf());
|
||||
|
||||
expect(tx.inspection.findFirst).toHaveBeenCalledWith({
|
||||
where: { colonyId: 'colony-1' },
|
||||
orderBy: [{ date: 'desc' }, { createdAt: 'desc' }, { id: 'desc' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('looks for the open suggestion of each rule for the colony', async () => {
|
||||
await evaluateInspection(latestOf());
|
||||
|
||||
expect(tx.suggestion.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
ruleKey: 'QUEENLESS_NO_EGGS',
|
||||
colonyId: 'colony-1',
|
||||
status: 'OPEN',
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves it when the latest inspection no longer shows the finding', async () => {
|
||||
await evaluateInspection(latestOf());
|
||||
|
||||
expect(suggestions.resolveIn).toHaveBeenCalledWith(
|
||||
tx,
|
||||
['open-1'],
|
||||
'Bedingung entfällt',
|
||||
);
|
||||
expect(propose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes it with the latest inspection when the finding persists', async () => {
|
||||
await evaluateInspection(
|
||||
latestOf({ queenSeen: false, eggsPresent: false }),
|
||||
);
|
||||
|
||||
expect(suggestions.refreshIn).toHaveBeenCalledWith(tx, 'open-1', {
|
||||
evidence: {
|
||||
colonyId: 'colony-1',
|
||||
inspectionId: 'inspection-2',
|
||||
inspectedOn: '2026-05-12',
|
||||
},
|
||||
dueDate: null,
|
||||
priority: 'HIGH',
|
||||
expiresAt: null,
|
||||
});
|
||||
expect(suggestions.resolveIn).not.toHaveBeenCalled();
|
||||
expect(propose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('recomputes the due date from dueInDays when it refreshes', async () => {
|
||||
withRules({ ...queenlessRule, config: { dueInDays: 4 } });
|
||||
|
||||
await evaluateInspection(
|
||||
latestOf({ queenSeen: false, eggsPresent: false }),
|
||||
);
|
||||
|
||||
expect(suggestions.refreshIn).toHaveBeenCalledWith(
|
||||
tx,
|
||||
'open-1',
|
||||
expect.objectContaining({ dueDate: '2026-05-16' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('is idempotent: a re-delivery refreshes the same evidence again', async () => {
|
||||
const inspection = latestOf({ queenSeen: false, eggsPresent: false });
|
||||
|
||||
await evaluateInspection(inspection);
|
||||
await evaluateInspection(inspection);
|
||||
|
||||
const [first, second] = suggestions.refreshIn.mock.calls as unknown[][];
|
||||
expect(second).toEqual(first);
|
||||
expect(suggestions.resolveIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('proposes nothing and changes nothing when the event inspection is not the latest (late, retried or backdated)', async () => {
|
||||
tx.inspection.findFirst.mockResolvedValue(latestOf({ id: 'newer' }));
|
||||
|
||||
await service.evaluateInspectionIn(
|
||||
asTx(),
|
||||
latestOf({
|
||||
id: 'older',
|
||||
queenSeen: false,
|
||||
eggsPresent: false,
|
||||
date: new Date('2026-05-01T00:00:00.000Z'),
|
||||
}),
|
||||
);
|
||||
await service.evaluateInspectionCorrectionIn(asTx(), latestOf());
|
||||
|
||||
expect(propose).not.toHaveBeenCalled();
|
||||
expect(suggestions.resolveIn).not.toHaveBeenCalled();
|
||||
expect(suggestions.refreshIn).not.toHaveBeenCalled();
|
||||
expect(tx.suggestion.findFirst).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when the colony is gone or has no inspection any more', async () => {
|
||||
tx.$queryRaw.mockResolvedValueOnce([]);
|
||||
await evaluateInspection(latestOf());
|
||||
|
||||
tx.inspection.findFirst.mockResolvedValue(null);
|
||||
await service.evaluateInspectionIn(asTx(), latestOf());
|
||||
|
||||
expect(suggestions.resolveIn).not.toHaveBeenCalled();
|
||||
expect(suggestions.refreshIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('the correction of the latest inspection resolves it when the finding is gone', async () => {
|
||||
await evaluateInspectionCorrection(latestOf());
|
||||
|
||||
expect(suggestions.resolveIn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps it OPEN when the corrected latest inspection still shows the finding', async () => {
|
||||
await evaluateInspectionCorrection(
|
||||
latestOf({ queenSeen: false, eggsPresent: false }),
|
||||
);
|
||||
|
||||
expect(suggestions.resolveIn).not.toHaveBeenCalled();
|
||||
expect(suggestions.refreshIn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('a correction never proposes', async () => {
|
||||
tx.suggestion.findFirst.mockResolvedValue(null);
|
||||
|
||||
await evaluateInspectionCorrection(
|
||||
latestOf({ queenSeen: false, eggsPresent: false }),
|
||||
);
|
||||
|
||||
expect(propose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('leaves open suggestions of an inactive rule untouched', async () => {
|
||||
withRules({ ...queenlessRule, active: false });
|
||||
|
||||
await evaluateInspection(latestOf());
|
||||
await evaluateInspection(
|
||||
latestOf({ queenSeen: false, eggsPresent: false }),
|
||||
);
|
||||
|
||||
expect(suggestions.resolveIn).not.toHaveBeenCalled();
|
||||
expect(suggestions.refreshIn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('follows the feed rule the same way', async () => {
|
||||
tx.suggestion.findFirst.mockImplementation(
|
||||
({ where }: { where: { ruleKey: string } }) =>
|
||||
Promise.resolve(
|
||||
where.ruleKey === 'FEED_STATUS_CRITICAL'
|
||||
? { id: 'open-feed' }
|
||||
: null,
|
||||
),
|
||||
);
|
||||
|
||||
await evaluateInspection(latestOf());
|
||||
|
||||
expect(suggestions.resolveIn).toHaveBeenCalledWith(
|
||||
tx,
|
||||
['open-feed'],
|
||||
'Bedingung entfällt',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateVarroaMeasurementIn', () => {
|
||||
let propose: jest.SpyInstance;
|
||||
const measurement = (
|
||||
|
||||
@@ -112,6 +112,17 @@ function dueDayOf(config: object, baseDay: string): string | null {
|
||||
return typeof dueInDays === 'number' ? addDays(baseDay, dueInDays) : null;
|
||||
}
|
||||
|
||||
function inspectionEvidence(
|
||||
inspection: Inspection,
|
||||
inspectedOn: string,
|
||||
): SuggestionEvidence {
|
||||
return {
|
||||
colonyId: inspection.colonyId,
|
||||
inspectionId: inspection.id,
|
||||
inspectedOn,
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AutomationService {
|
||||
private readonly logger = new Logger(AutomationService.name);
|
||||
@@ -229,24 +240,119 @@ export class AutomationService {
|
||||
// transaction, the legacy wrappers below with a transaction of their own.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** A new inspection: proposes new findings and follows the open ones. */
|
||||
async evaluateInspectionIn(tx: Tx, inspection: Inspection): Promise<void> {
|
||||
assertTransactionClient(tx, 'AutomationService.evaluateInspectionIn');
|
||||
if (inspection.queenSeen === false && inspection.eggsPresent === false) {
|
||||
await this.proposeOnInspectionIn(
|
||||
tx,
|
||||
'QUEENLESS_NO_EGGS',
|
||||
'QUEEN_CHECK',
|
||||
inspection,
|
||||
);
|
||||
await this.syncInspectionRulesIn(tx, inspection, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* A corrected inspection proposes nothing new, but an open suggestion it
|
||||
* supersedes follows the corrected values (resolved or refreshed).
|
||||
*/
|
||||
async evaluateInspectionCorrectionIn(
|
||||
tx: Tx,
|
||||
inspection: Inspection,
|
||||
): Promise<void> {
|
||||
assertTransactionClient(
|
||||
tx,
|
||||
'AutomationService.evaluateInspectionCorrectionIn',
|
||||
);
|
||||
await this.syncInspectionRulesIn(tx, inspection, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Colony lock first, then everything that decides is read under it: the
|
||||
* colony's actual latest inspection (date, createdAt, id) is the only one
|
||||
* that counts. An event of any other inspection - a late or retried
|
||||
* delivery, a backdated entry, the correction of an older one - proposes
|
||||
* nothing and changes nothing; the latest inspection has its own event.
|
||||
* (An inspection moved to another colony raises no event for the colony
|
||||
* it left, so a suggestion there stays until that colony's next
|
||||
* inspection.)
|
||||
*/
|
||||
private async syncInspectionRulesIn(
|
||||
tx: Tx,
|
||||
inspection: Inspection,
|
||||
proposeNew: boolean,
|
||||
): Promise<void> {
|
||||
if (!(await lockRowForUpdate(tx, 'Colony', inspection.colonyId))) {
|
||||
return;
|
||||
}
|
||||
if (inspection.feedStatus === 'CRITICAL') {
|
||||
await this.proposeOnInspectionIn(
|
||||
tx,
|
||||
'FEED_STATUS_CRITICAL',
|
||||
'FEED',
|
||||
inspection,
|
||||
);
|
||||
const latest = await tx.inspection.findFirst({
|
||||
where: { colonyId: inspection.colonyId },
|
||||
orderBy: [{ date: 'desc' }, { createdAt: 'desc' }, { id: 'desc' }],
|
||||
});
|
||||
if (latest === null || latest.id !== inspection.id) {
|
||||
return;
|
||||
}
|
||||
await this.syncInspectionRuleIn(
|
||||
tx,
|
||||
'QUEENLESS_NO_EGGS',
|
||||
'QUEEN_CHECK',
|
||||
latest,
|
||||
latest.queenSeen === false && latest.eggsPresent === false,
|
||||
proposeNew,
|
||||
);
|
||||
await this.syncInspectionRuleIn(
|
||||
tx,
|
||||
'FEED_STATUS_CRITICAL',
|
||||
'FEED',
|
||||
latest,
|
||||
latest.feedStatus === 'CRITICAL',
|
||||
proposeNew,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One inspection rule against the colony's latest inspection (colony
|
||||
* locked). No open suggestion: a met condition proposes - new inspections
|
||||
* only, a correction never proposes (a corrected value is not a new
|
||||
* finding; the beekeeper was told about the original). An open one follows
|
||||
* the latest inspection: condition gone, it is resolved; still met, its
|
||||
* evidence is refreshed. The refresh keeps the original dedupeKey/anchor,
|
||||
* so evidence.inspectionId may differ from the anchor. An inactive rule
|
||||
* leaves open suggestions untouched (as the varroa rule does).
|
||||
*/
|
||||
private async syncInspectionRuleIn(
|
||||
tx: Tx,
|
||||
trigger: 'QUEENLESS_NO_EGGS' | 'FEED_STATUS_CRITICAL',
|
||||
intent: SuggestionIntent,
|
||||
latest: Inspection,
|
||||
met: boolean,
|
||||
proposeNew: boolean,
|
||||
): Promise<void> {
|
||||
const open = await tx.suggestion.findFirst({
|
||||
where: {
|
||||
ruleKey: trigger,
|
||||
colonyId: latest.colonyId,
|
||||
status: 'OPEN',
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (open === null) {
|
||||
if (met && proposeNew) {
|
||||
await this.proposeOnInspectionIn(tx, trigger, intent, latest);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const rule = await readRule(tx, trigger);
|
||||
if (!rule.active) {
|
||||
return;
|
||||
}
|
||||
// Colony (held), then suggestion.
|
||||
await lockRowForUpdate(tx, 'Suggestion', open.id);
|
||||
if (!met) {
|
||||
await this.suggestions.resolveIn(tx, [open.id], 'Bedingung entfällt');
|
||||
return;
|
||||
}
|
||||
const inspectedOn = berlinDayOf(latest.date);
|
||||
await this.suggestions.refreshIn(tx, open.id, {
|
||||
evidence: inspectionEvidence(latest, inspectedOn),
|
||||
dueDate: dueDayOf(rule.config, inspectedOn),
|
||||
priority: 'HIGH',
|
||||
expiresAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
/** The reason code of an inspection rule is the rule's own name. */
|
||||
@@ -267,11 +373,7 @@ export class AutomationService {
|
||||
colonyId: inspection.colonyId,
|
||||
intent,
|
||||
reasonCode: trigger,
|
||||
evidence: {
|
||||
colonyId: inspection.colonyId,
|
||||
inspectionId: inspection.id,
|
||||
inspectedOn,
|
||||
},
|
||||
evidence: inspectionEvidence(inspection, inspectedOn),
|
||||
anchor: inspection.id,
|
||||
requiredPermission: 'inspections:read',
|
||||
dueDate: dueDayOf(rule.config, inspectedOn),
|
||||
|
||||
Reference in new issue
Block a user