Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions functions/anonymizeMovements.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ const PII_FIELDS = [
'phone',
'memberNr',
'immatriculation',
'aircraftType',
'mtow',
'remarks',
'carriageVoucher',
'createdBy',
'createdBy_orderKey',
'customsFormId',
'customsFormUrl',
'privacyPolicyAcceptedAt',
];

function getAnonymizationUpdates(snapshot, cutoffIso) {
Expand All @@ -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;
}
});
Expand Down
62 changes: 59 additions & 3 deletions functions/anonymizeMovements.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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);

Expand Down
4 changes: 3 additions & 1 deletion src/util/LandingsReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand Down
49 changes: 49 additions & 0 deletions src/util/movements.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 2 additions & 2 deletions src/util/movements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || '');
}

/**
Expand All @@ -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 || '');
}
Loading