feat(automation): separate lot number counters per raw material kind
Backend Image / build-and-push (push) Successful in 2m15s
Frontend Image / build-and-push (push) Successful in 2m3s

With {ART} in the raw material number format (default {ART}-{YYYY}-{SEQ:4}),
beeswax and propolis each count on their own: WA-2026-0001, PR-2026-0001.
Counters live in the new AutomationSequence table (one row per rule and
scope). Without {ART} the rule keeps its single shared counter, since
separate counters behind one prefix would hand out duplicate numbers.
Resetting a rule clears all of its counters.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-07 19:33:10 +02:00
1 parent 5408711b2d
commit cfd3a187af
9 files changed
+252 -24

No files matched your search

@@ -15,6 +15,7 @@ describe('AutomationService', () => {
findUnique: jest.Mock;
update: jest.Mock;
};
automationSequence: { upsert: jest.Mock };
task: { create: jest.Mock; findFirst: jest.Mock };
};
let auditLog: { record: jest.Mock };
@@ -51,6 +52,7 @@ describe('AutomationService', () => {
findUnique: jest.fn(),
update: jest.fn(),
},
automationSequence: { upsert: jest.fn() },
task: {
create: jest.fn().mockResolvedValue({ id: 'task-1' }),
findFirst: jest.fn().mockResolvedValue(null),
@@ -461,6 +463,73 @@ describe('AutomationService', () => {
});
expect(result).toBe(`WH-${new Date().getFullYear()}-0007`);
});
const rawFormatRule = {
id: 'rule-8',
triggerType: 'RAW_MATERIAL_LOT_NUMBER_FORMAT',
active: true,
config: { format: '{ART}-{YYYY}-{SEQ:4}' },
sequenceValue: 0,
};
it('counts per scope when the format tells the scopes apart', async () => {
prisma.automationRule.findUnique.mockResolvedValue(rawFormatRule);
prisma.automationSequence.upsert.mockResolvedValue({ value: 3 });
const result = await service.generateNumber(
'RAW_MATERIAL_LOT_NUMBER_FORMAT',
{ scope: 'PROPOLIS', vars: { ART: 'PR' } },
);
expect(prisma.automationSequence.upsert).toHaveBeenCalledWith({
where: {
triggerType_scope: {
triggerType: 'RAW_MATERIAL_LOT_NUMBER_FORMAT',
scope: 'PROPOLIS',
},
},
create: {
triggerType: 'RAW_MATERIAL_LOT_NUMBER_FORMAT',
scope: 'PROPOLIS',
value: 1,
},
update: { value: { increment: 1 } },
select: { value: true },
});
expect(prisma.automationRule.update).not.toHaveBeenCalled();
expect(result).toBe(`PR-${new Date().getFullYear()}-0003`);
});
it('keeps one shared counter when the format has no {ART}', async () => {
prisma.automationRule.findUnique.mockResolvedValue({
...rawFormatRule,
config: { format: 'RS-{YYYY}-{SEQ:4}' },
});
prisma.automationRule.update.mockResolvedValue({ sequenceValue: 5 });
const result = await service.generateNumber(
'RAW_MATERIAL_LOT_NUMBER_FORMAT',
{ scope: 'BEESWAX', vars: { ART: 'WA' } },
);
expect(prisma.automationSequence.upsert).not.toHaveBeenCalled();
expect(result).toBe(`RS-${new Date().getFullYear()}-0005`);
});
it('retries once when two first uses of a scope collide', async () => {
prisma.automationRule.findUnique.mockResolvedValue(rawFormatRule);
prisma.automationSequence.upsert
.mockRejectedValueOnce({ code: 'P2002' })
.mockResolvedValueOnce({ value: 2 });
const result = await service.generateNumber(
'RAW_MATERIAL_LOT_NUMBER_FORMAT',
{ scope: 'BEESWAX', vars: { ART: 'WA' } },
);
expect(prisma.automationSequence.upsert).toHaveBeenCalledTimes(2);
expect(result).toBe(`WA-${new Date().getFullYear()}-0002`);
});
});
describe('resetSequence', () => {
@@ -481,7 +550,7 @@ describe('AutomationService', () => {
expect(prisma.automationRule.update).toHaveBeenCalledWith({
where: { id: 'rule-4' },
data: { sequenceValue: 0 },
data: { sequenceValue: 0, sequences: { deleteMany: {} } },
});
expect(auditLog.record).toHaveBeenCalledWith(
expect.objectContaining({