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); 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; } 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 || ''); }