feat(automation): separate lot number counters per raw material kind
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:
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({
|
||||
|
||||
Reference in new issue
Block a user