From 6213a04f7df1a39b37ddf8301c332ee040d404d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 09:40:49 +0000 Subject: [PATCH 1/3] Refine PII fields in anonymization job - Add privacyPolicyAcceptedAt (consent timestamp per movement) - Add paymentMethod/invoiceRecipientName (nested PII) - Remove aircraftType, mtow (not PII without immatriculation) - Remove carriageVoucher (boolean, not PII) https://claude.ai/code/session_01KHZDkEfyznUCcoGCWrGdsY --- functions/anonymizeMovements.js | 7 ++-- functions/anonymizeMovements.spec.js | 62 ++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/functions/anonymizeMovements.js b/functions/anonymizeMovements.js index 528b740f..007369a6 100644 --- a/functions/anonymizeMovements.js +++ b/functions/anonymizeMovements.js @@ -13,14 +13,12 @@ const PII_FIELDS = [ 'phone', 'memberNr', 'immatriculation', - 'aircraftType', - 'mtow', 'remarks', - 'carriageVoucher', 'createdBy', 'createdBy_orderKey', 'customsFormId', 'customsFormUrl', + 'privacyPolicyAcceptedAt', ]; function getAnonymizationUpdates(snapshot, cutoffIso) { @@ -35,6 +33,9 @@ function getAnonymizationUpdates(snapshot, cutoffIso) { updates[`${child.key}/${field}`] = null; } }); + if (val.paymentMethod && val.paymentMethod.invoiceRecipientName !== undefined) { + updates[`${child.key}/paymentMethod/invoiceRecipientName`] = null; + } updates[`${child.key}/anonymized`] = true; } }); diff --git a/functions/anonymizeMovements.spec.js b/functions/anonymizeMovements.spec.js index e5de6f88..f97f2d61 100644 --- a/functions/anonymizeMovements.spec.js +++ b/functions/anonymizeMovements.spec.js @@ -81,6 +81,7 @@ describe('anonymizeMovements', () => { remarks: 'test', createdBy: 'uid1', createdBy_orderKey: 'uid1_123', + privacyPolicyAcceptedAt: '2023-01-15T09:55:00.000Z', location: 'LSZT', flightType: 'private', negativeTimestamp: -1673776800000, @@ -103,13 +104,15 @@ describe('anonymizeMovements', () => { expect(departureUpdates['mov1/phone']).toBeNull(); expect(departureUpdates['mov1/memberNr']).toBeNull(); expect(departureUpdates['mov1/immatriculation']).toBeNull(); - expect(departureUpdates['mov1/aircraftType']).toBeNull(); - expect(departureUpdates['mov1/mtow']).toBeNull(); expect(departureUpdates['mov1/remarks']).toBeNull(); expect(departureUpdates['mov1/createdBy']).toBeNull(); expect(departureUpdates['mov1/createdBy_orderKey']).toBeNull(); + expect(departureUpdates['mov1/privacyPolicyAcceptedAt']).toBeNull(); expect(departureUpdates['mov1/anonymized']).toBe(true); + // Non-PII fields are preserved + expect(departureUpdates['mov1/aircraftType']).toBeUndefined(); + expect(departureUpdates['mov1/mtow']).toBeUndefined(); expect(departureUpdates['mov1/location']).toBeUndefined(); expect(departureUpdates['mov1/flightType']).toBeUndefined(); expect(departureUpdates['mov1/dateTime']).toBeUndefined(); @@ -147,13 +150,66 @@ describe('anonymizeMovements', () => { expect(updates['mov1/anonymized']).toBe(true); // Fields absent from the record must not appear in the update object - expect(updates['mov1/carriageVoucher']).toBeUndefined(); expect(updates['mov1/customsFormId']).toBeUndefined(); expect(updates['mov1/customsFormUrl']).toBeUndefined(); expect(updates['mov1/email']).toBeUndefined(); expect(updates['mov1/phone']).toBeUndefined(); }); + it('should anonymize invoiceRecipientName from paymentMethod', async () => { + mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(730); + + const snapshot = createSnapshot({ + 'mov1': { + dateTime: '2023-01-15T10:00:00.000Z', + firstname: 'Hans', + lastname: 'Muster', + paymentMethod: { + method: 'invoice', + invoiceRecipientName: 'Fluggruppe Thurgau', + }, + location: 'LSZT', + }, + }); + + mockRefData['/departures'] = { forEach: () => {} }; + mockRefData['/arrivals'] = snapshot; + + const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); + await scheduledAnonymizeMovements(); + + expect(mockUpdate).toHaveBeenCalledTimes(1); + + const updates = mockUpdate.mock.calls[0][0]; + expect(updates['mov1/paymentMethod/invoiceRecipientName']).toBeNull(); + // payment method itself is preserved + expect(updates['mov1/paymentMethod']).toBeUndefined(); + }); + + it('should not add paymentMethod update when invoiceRecipientName is absent', async () => { + mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(730); + + const snapshot = createSnapshot({ + 'mov1': { + dateTime: '2023-01-15T10:00:00.000Z', + firstname: 'Hans', + paymentMethod: { method: 'cash' }, + location: 'LSZT', + }, + }); + + mockRefData['/departures'] = { forEach: () => {} }; + mockRefData['/arrivals'] = snapshot; + + const { scheduledAnonymizeMovements } = require('./anonymizeMovements'); + await scheduledAnonymizeMovements(); + + expect(mockUpdate).toHaveBeenCalledTimes(1); + + const updates = mockUpdate.mock.calls[0][0]; + expect(updates['mov1/paymentMethod/invoiceRecipientName']).toBeUndefined(); + }); + it('should skip already anonymized movements', async () => { mockRefData['/settings/movementRetentionDays'] = createValueSnapshot(730); From 25615c5dd62a296f9ed00f4884e0166bf8298414 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 09:58:19 +0000 Subject: [PATCH 2/3] Handle null immatriculation in movement sorting Anonymized records have immatriculation set to null, which crashes localeCompare in compareDescending/compareAscending. Fall back to empty string for null values. https://claude.ai/code/session_01KHZDkEfyznUCcoGCWrGdsY --- src/util/movements.spec.ts | 49 ++++++++++++++++++++++++++++++++++++++ src/util/movements.ts | 4 ++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/util/movements.spec.ts b/src/util/movements.spec.ts index 5a518e56..715e5206 100644 --- a/src/util/movements.spec.ts +++ b/src/util/movements.spec.ts @@ -241,6 +241,55 @@ describe('util', () => { }); }); + describe('sorting with null immatriculation (anonymized records)', () => { + it('compareDescending does not crash when immatriculation is null', () => { + const a = { + date: '2016-03-08', + time: '08:30', + immatriculation: null, + }; + const b = { + date: '2016-03-08', + time: '08:30', + immatriculation: 'HB-KFW', + }; + + expect(() => compareDescending(a as any, b as any)).not.toThrow(); + expect(compareDescending(a as any, b as any)).toBe(-1); + }); + + it('compareAscending does not crash when immatriculation is null', () => { + const a = { + date: '2016-03-08', + time: '08:30', + immatriculation: 'HB-KFW', + }; + const b = { + date: '2016-03-08', + time: '08:30', + immatriculation: null, + }; + + expect(() => compareAscending(a as any, b as any)).not.toThrow(); + expect(compareAscending(a as any, b as any)).toBe(1); + }); + + it('compareDescending returns 0 when both immatriculations are null', () => { + const a = { + date: '2016-03-08', + time: '08:30', + immatriculation: null, + }; + const b = { + date: '2016-03-08', + time: '08:30', + immatriculation: null, + }; + + expect(compareDescending(a as any, b as any)).toBe(0); + }); + }); + describe('transferValues', () => { it('transfers values which are not undefined and not null', () => { const source = { diff --git a/src/util/movements.ts b/src/util/movements.ts index 9c6fe833..c4b966a5 100644 --- a/src/util/movements.ts +++ b/src/util/movements.ts @@ -129,7 +129,7 @@ export function compareDescending(a: Movement, b: Movement): number { return dateCompare; } - return a.immatriculation.localeCompare(b.immatriculation); + return (a.immatriculation || '').localeCompare(b.immatriculation || ''); } /** @@ -144,5 +144,5 @@ export function compareAscending(a: Movement, b: Movement): number { return dateCompare; } - return a.immatriculation.localeCompare(b.immatriculation); + return (a.immatriculation || '').localeCompare(b.immatriculation || ''); } From c7fc94afc2c4aea0845b9561108b587a924ba18a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 09:58:56 +0000 Subject: [PATCH 3/3] Skip anonymized arrivals in landings report Anonymized records have null immatriculation, which can't be meaningfully grouped in a per-aircraft landing summary. Skip them and add defensive null-safe sorting. https://claude.ai/code/session_01KHZDkEfyznUCcoGCWrGdsY --- src/util/LandingsReport.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/LandingsReport.ts b/src/util/LandingsReport.ts index bf8ae459..cc5874be 100644 --- a/src/util/LandingsReport.ts +++ b/src/util/LandingsReport.ts @@ -68,6 +68,8 @@ class LandingsReport { const arrival = firebaseToLocal(record.val()); const { immatriculation, mtow, landingCount } = arrival; + if (!immatriculation) return; + if (!map[immatriculation]) { map[immatriculation] = { immatriculation, @@ -90,7 +92,7 @@ class LandingsReport { Object.keys(map).forEach(key => { arr.push(map[key]); }); - arr.sort((a1, a2) => a1.immatriculation.localeCompare(a2.immatriculation)); + arr.sort((a1, a2) => (a1.immatriculation || '').localeCompare(a2.immatriculation || '')); return arr; }