fix: print the jar size on labels in grams, not millilitres
renderLabelsPdf resolved the jarSize placeholder to "500 ml". Everywhere else in the app jarSize is a fill weight in grams - the bottling forms label it "Glasgröße (g)", the batch list and the public Herkunft page print "g", and BottlingService divides by 1000 to get kilograms. Only the printed label said millilitres, so every label produced so far declares the wrong net quantity for a food sold by mass. The placeholder mapping moved into an exported buildPlaceholderValues() so the wording can be asserted directly. It could not be before: the render output is a PDF buffer whose text a test cannot read, which is how the wrong unit shipped unnoticed. Three tests now cover it, including one that rejects any volume unit outright. Labels already printed with "500 ml" have to be reprinted - the code change does not correct paper. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
46a2322814
commit
5a5bd4a138
2 files changed
+65
-10
No files matched your search
@@ -1,5 +1,8 @@
|
|||||||
import { NotFoundException } from '@nestjs/common';
|
import { NotFoundException } from '@nestjs/common';
|
||||||
import { LabelTemplatesService } from './label-templates.service';
|
import {
|
||||||
|
LabelTemplatesService,
|
||||||
|
buildPlaceholderValues,
|
||||||
|
} from './label-templates.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { AuditLogService } from '../audit/audit-log.service';
|
import { AuditLogService } from '../audit/audit-log.service';
|
||||||
import { LabelElement } from './label-element.types';
|
import { LabelElement } from './label-element.types';
|
||||||
@@ -415,4 +418,36 @@ describe('LabelTemplatesService', () => {
|
|||||||
expect(countPages(pdf)).toBe(2);
|
expect(countPages(pdf)).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('buildPlaceholderValues', () => {
|
||||||
|
const bottlingBatch = {
|
||||||
|
jarSize: 500,
|
||||||
|
lotNumber: 'L-001',
|
||||||
|
date: new Date('2026-06-01T00:00:00.000Z'),
|
||||||
|
bestBeforeDate: new Date('2028-06-01T00:00:00.000Z'),
|
||||||
|
honeyBatch: { batchNumber: 'HB-001', honeyType: 'Bluetenhonig' },
|
||||||
|
product: { name: 'Glas 500g' },
|
||||||
|
};
|
||||||
|
|
||||||
|
it('prints the jar size in grams, the unit honey is sold by', () => {
|
||||||
|
expect(buildPlaceholderValues(bottlingBatch).jarSize).toBe('500 g');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never labels the jar size as a volume', () => {
|
||||||
|
expect(buildPlaceholderValues(bottlingBatch).jarSize).not.toMatch(
|
||||||
|
/\b(ml|l|cl)\b/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves an unset honey type and product as empty text, not "null"', () => {
|
||||||
|
const values = buildPlaceholderValues({
|
||||||
|
...bottlingBatch,
|
||||||
|
honeyBatch: { batchNumber: 'HB-001', honeyType: null },
|
||||||
|
product: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(values.honeyType).toBe('');
|
||||||
|
expect(values.productName).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -104,6 +104,34 @@ function dataUrlToBuffer(dataUrl: string): Buffer | null {
|
|||||||
return matchesFileSignature(buffer, mimeType) ? buffer : null;
|
return matchesFileSignature(buffer, mimeType) ? buffer : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text every placeholder resolves to on a printed label.
|
||||||
|
*
|
||||||
|
* Extracted from the render path so the wording is unit-testable: the render
|
||||||
|
* output is a PDF buffer whose text is not readable by a test, which is how
|
||||||
|
* the jar size once shipped printed as "500 ml". jarSize is a fill weight in
|
||||||
|
* grams everywhere else in the app, and honey is a food sold by mass, so a
|
||||||
|
* label stating millilitres declares the wrong net quantity.
|
||||||
|
*/
|
||||||
|
export function buildPlaceholderValues(bottlingBatch: {
|
||||||
|
jarSize: number;
|
||||||
|
lotNumber: string;
|
||||||
|
date: Date;
|
||||||
|
bestBeforeDate: Date;
|
||||||
|
honeyBatch: { batchNumber: string; honeyType: string | null };
|
||||||
|
product: { name: string } | null;
|
||||||
|
}): Record<PlaceholderKey, string> {
|
||||||
|
return {
|
||||||
|
batchNumber: bottlingBatch.honeyBatch.batchNumber,
|
||||||
|
honeyType: bottlingBatch.honeyBatch.honeyType ?? '',
|
||||||
|
lotNumber: bottlingBatch.lotNumber,
|
||||||
|
bottlingDate: formatDate(bottlingBatch.date),
|
||||||
|
bestBeforeDate: formatDate(bottlingBatch.bestBeforeDate),
|
||||||
|
jarSize: `${bottlingBatch.jarSize} g`,
|
||||||
|
productName: bottlingBatch.product?.name ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LabelTemplatesService {
|
export class LabelTemplatesService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -210,15 +238,7 @@ export class LabelTemplatesService {
|
|||||||
throw new NotFoundException('Bottling batch not found');
|
throw new NotFoundException('Bottling batch not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const values: Record<PlaceholderKey, string> = {
|
const values = buildPlaceholderValues(bottlingBatch);
|
||||||
batchNumber: bottlingBatch.honeyBatch.batchNumber,
|
|
||||||
honeyType: bottlingBatch.honeyBatch.honeyType ?? '',
|
|
||||||
lotNumber: bottlingBatch.lotNumber,
|
|
||||||
bottlingDate: formatDate(bottlingBatch.date),
|
|
||||||
bestBeforeDate: formatDate(bottlingBatch.bestBeforeDate),
|
|
||||||
jarSize: `${bottlingBatch.jarSize} ml`,
|
|
||||||
productName: bottlingBatch.product?.name ?? '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const elements = elementsOf(template).map((element) =>
|
const elements = elementsOf(template).map((element) =>
|
||||||
element.type === 'text' && element.placeholderKey
|
element.type === 'text' && element.placeholderKey
|
||||||
|
|||||||
Reference in new issue
Block a user