From 5a5bd4a138caa139ca175bea460fcd4393e622eb Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Tue, 22 Sep 2026 02:37:00 +0200 Subject: [PATCH] fix: print the jar size on labels in grams, not millilitres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../label-templates.service.spec.ts | 37 +++++++++++++++++- .../label-templates.service.ts | 38 ++++++++++++++----- 2 files changed, 65 insertions(+), 10 deletions(-) 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