diff --git a/ApiCore-Backend/src/label-templates/label-templates.service.spec.ts b/ApiCore-Backend/src/label-templates/label-templates.service.spec.ts index f19ae51..4499f02 100644 --- a/ApiCore-Backend/src/label-templates/label-templates.service.spec.ts +++ b/ApiCore-Backend/src/label-templates/label-templates.service.spec.ts @@ -1,5 +1,8 @@ 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 { AuditLogService } from '../audit/audit-log.service'; import { LabelElement } from './label-element.types'; @@ -415,4 +418,36 @@ describe('LabelTemplatesService', () => { 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(''); + }); + }); }); diff --git a/ApiCore-Backend/src/label-templates/label-templates.service.ts b/ApiCore-Backend/src/label-templates/label-templates.service.ts index f371efa..1509449 100644 --- a/ApiCore-Backend/src/label-templates/label-templates.service.ts +++ b/ApiCore-Backend/src/label-templates/label-templates.service.ts @@ -104,6 +104,34 @@ function dataUrlToBuffer(dataUrl: string): 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 { + 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() export class LabelTemplatesService { constructor( @@ -210,15 +238,7 @@ export class LabelTemplatesService { throw new NotFoundException('Bottling batch not found'); } - const values: Record = { - 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 values = buildPlaceholderValues(bottlingBatch); const elements = elementsOf(template).map((element) => element.type === 'text' && element.placeholderKey