feat: add website social link, fix herkunft origin display, GUI backup schedule, and format-based batch numbering
- 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:
1 parent
7b23862a41
commit
412d033d8d
42 files changed
+1045
-119
No files matched your search
@@ -0,0 +1,131 @@
|
||||
import { BackupSettingsService } from './backup-settings.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
|
||||
describe('BackupSettingsService', () => {
|
||||
let service: BackupSettingsService;
|
||||
let prisma: {
|
||||
backupSettings: {
|
||||
findUnique: jest.Mock;
|
||||
create: jest.Mock;
|
||||
update: jest.Mock;
|
||||
};
|
||||
};
|
||||
let auditLog: { record: jest.Mock };
|
||||
|
||||
const existing = {
|
||||
id: 'singleton',
|
||||
storagePath: '/data/backups',
|
||||
cronSchedule: '0 3 * * *',
|
||||
retentionCount: 14,
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = {
|
||||
backupSettings: {
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
};
|
||||
auditLog = { record: jest.fn().mockResolvedValue(undefined) };
|
||||
service = new BackupSettingsService(
|
||||
prisma as unknown as PrismaService,
|
||||
auditLog as unknown as AuditLogService,
|
||||
);
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('returns the existing singleton row', async () => {
|
||||
prisma.backupSettings.findUnique.mockResolvedValue(existing);
|
||||
|
||||
const result = await service.get();
|
||||
|
||||
expect(result).toEqual(existing);
|
||||
expect(prisma.backupSettings.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates the singleton row on first access', async () => {
|
||||
prisma.backupSettings.findUnique.mockResolvedValue(null);
|
||||
prisma.backupSettings.create.mockResolvedValue({
|
||||
id: 'singleton',
|
||||
storagePath: null,
|
||||
cronSchedule: null,
|
||||
retentionCount: null,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const result = await service.get();
|
||||
|
||||
expect(prisma.backupSettings.create).toHaveBeenCalledWith({
|
||||
data: { id: 'singleton' },
|
||||
});
|
||||
expect(result.id).toBe('singleton');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('stores a valid value and records an audit entry', async () => {
|
||||
prisma.backupSettings.findUnique.mockResolvedValue(existing);
|
||||
prisma.backupSettings.update.mockResolvedValue({
|
||||
...existing,
|
||||
retentionCount: 30,
|
||||
});
|
||||
|
||||
const result = await service.update({ retentionCount: 30 }, 'user-1');
|
||||
|
||||
expect(prisma.backupSettings.update).toHaveBeenCalledWith({
|
||||
where: { id: 'singleton' },
|
||||
data: {
|
||||
storagePath: undefined,
|
||||
cronSchedule: undefined,
|
||||
retentionCount: 30,
|
||||
},
|
||||
});
|
||||
expect(result.retentionCount).toBe(30);
|
||||
expect(auditLog.record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityType: 'BackupSettings',
|
||||
action: 'UPDATE',
|
||||
userId: 'user-1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('clears a string field when given an empty string', async () => {
|
||||
prisma.backupSettings.findUnique.mockResolvedValue(existing);
|
||||
prisma.backupSettings.update.mockResolvedValue({
|
||||
...existing,
|
||||
storagePath: null,
|
||||
});
|
||||
|
||||
await service.update({ storagePath: '' }, 'user-1');
|
||||
|
||||
expect(prisma.backupSettings.update).toHaveBeenCalledWith({
|
||||
where: { id: 'singleton' },
|
||||
data: {
|
||||
storagePath: null,
|
||||
cronSchedule: undefined,
|
||||
retentionCount: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves fields untouched when omitted from the dto', async () => {
|
||||
prisma.backupSettings.findUnique.mockResolvedValue(existing);
|
||||
prisma.backupSettings.update.mockResolvedValue(existing);
|
||||
|
||||
await service.update({}, 'user-1');
|
||||
|
||||
expect(prisma.backupSettings.update).toHaveBeenCalledWith({
|
||||
where: { id: 'singleton' },
|
||||
data: {
|
||||
storagePath: undefined,
|
||||
cronSchedule: undefined,
|
||||
retentionCount: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user