feat: add website social link, fix herkunft origin display, GUI backup schedule, and format-based batch numbering
Backend Image / build-and-push (push) Successful in 2m15s
Frontend Image / build-and-push (push) Successful in 2m17s

- SiteSettings gains websiteUrl (e.g. online shop link) alongside the
  existing social links, editable in Verwaltung > Automatisierung.
- /herkunft/[lot] now shows the apiary's Standort instead of its internal
  Name (so multiple apiaries at one location read as one origin), adds
  small icons per social link, and surfaces previously-fetched-but-unused
  data (Honigsorte, Abfülldatum, stat cards) to make the page a richer
  customer-facing info page.
- Backup schedule (path/cron/retention) is now also configurable via the
  GUI (Verwaltung > Backup), stored in a new BackupSettings singleton;
  an env var always wins over the GUI value per field, and the scheduler
  reloads its cron job live when GUI settings change.
- New Automatisierung rules let an admin define a format (e.g.
  "WH-{YYYY}-{SEQ:4}") that auto-generates HoneyBatch.batchNumber and
  BottlingBatch.lotNumber on creation, with an atomic per-rule sequence
  counter and a manual reset. Manual entry still works when the rule is
  inactive.
This commit is contained in:
Nick Szaunig committed 2026-09-03 13:37:09 +02:00
1 parent 7b23862a41
commit 412d033d8d
42 files changed
+1045 -119

No files matched your search

@@ -79,7 +79,7 @@ describe('AutomationService', () => {
const result = await service.findAll();
expect(prisma.automationRule.upsert).toHaveBeenCalledTimes(6);
expect(prisma.automationRule.upsert).toHaveBeenCalledTimes(8);
expect(result).toHaveLength(3);
});
@@ -307,4 +307,86 @@ describe('AutomationService', () => {
);
});
});
describe('generateNumber', () => {
it('returns null when the rule is inactive', async () => {
prisma.automationRule.findUnique.mockResolvedValue({
id: 'rule-4',
triggerType: 'HONEY_BATCH_NUMBER_FORMAT',
active: false,
config: { format: 'WH-{YYYY}-{SEQ:4}' },
sequenceValue: 0,
});
const result = await service.generateNumber('HONEY_BATCH_NUMBER_FORMAT');
expect(result).toBeNull();
expect(prisma.automationRule.update).not.toHaveBeenCalled();
});
it('returns null when no format is configured', async () => {
prisma.automationRule.findUnique.mockResolvedValue({
id: 'rule-4',
triggerType: 'HONEY_BATCH_NUMBER_FORMAT',
active: true,
config: {},
sequenceValue: 0,
});
const result = await service.generateNumber('HONEY_BATCH_NUMBER_FORMAT');
expect(result).toBeNull();
});
it('atomically increments sequenceValue and formats the number', async () => {
prisma.automationRule.findUnique.mockResolvedValue({
id: 'rule-4',
triggerType: 'HONEY_BATCH_NUMBER_FORMAT',
active: true,
config: { format: 'WH-{YYYY}-{SEQ:4}' },
sequenceValue: 0,
});
prisma.automationRule.update.mockResolvedValue({ sequenceValue: 7 });
const result = await service.generateNumber('HONEY_BATCH_NUMBER_FORMAT');
expect(prisma.automationRule.update).toHaveBeenCalledWith({
where: { triggerType: 'HONEY_BATCH_NUMBER_FORMAT' },
data: { sequenceValue: { increment: 1 } },
select: { sequenceValue: true },
});
expect(result).toBe(`WH-${new Date().getFullYear()}-0007`);
});
});
describe('resetSequence', () => {
it('sets sequenceValue back to 0 and records an audit log entry', async () => {
prisma.automationRule.findUnique.mockResolvedValue({
id: 'rule-4',
triggerType: 'HONEY_BATCH_NUMBER_FORMAT',
active: true,
config: { format: 'WH-{YYYY}-{SEQ:4}' },
sequenceValue: 12,
});
prisma.automationRule.update.mockResolvedValue({
id: 'rule-4',
sequenceValue: 0,
});
await service.resetSequence('rule-4', 'user-1');
expect(prisma.automationRule.update).toHaveBeenCalledWith({
where: { id: 'rule-4' },
data: { sequenceValue: 0 },
});
expect(auditLog.record).toHaveBeenCalledWith(
expect.objectContaining({
entityType: 'AutomationRule',
entityId: 'rule-4',
action: 'UPDATE',
userId: 'user-1',
}),
);
});
});
});