Skip to content

Commit 3e3fe88

Browse files
authored
Merge pull request #18352 from mozilla/FXA-10478
feat(auth): cleanup old carts script
2 parents 3bea834 + 6e09bd4 commit 3e3fe88

File tree

4 files changed

+228
-0
lines changed

4 files changed

+228
-0
lines changed

packages/fxa-auth-server/lib/payments/processing-tasks-setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,6 @@ export async function setupProcessingTaskObjects(processName: string) {
8888
database,
8989
senders,
9090
stripeHelper,
91+
config,
9192
};
9293
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
import program from 'commander';
5+
6+
import { setupProcessingTaskObjects } from '../lib/payments/processing-tasks-setup';
7+
import { CartCleanup } from './cleanup-old-carts/cleanup-old-carts';
8+
import { setupAccountDatabase } from '@fxa/shared/db/mysql/account';
9+
10+
const pckg = require('../package.json');
11+
12+
const anonymizeableFields = new Set(['email', 'taxAddress'] as const);
13+
14+
const parseDeleteBefore = (deleteBefore: string | number) => {
15+
const date = new Date(deleteBefore);
16+
if (!date.getTime()) {
17+
throw new Error('deleteBefore is invalid');
18+
}
19+
return date;
20+
};
21+
22+
const parseAnonymizeBefore = (anonymizeBefore: string | number) => {
23+
if (!anonymizeBefore) {
24+
return null;
25+
}
26+
const date = new Date(anonymizeBefore);
27+
if (!date.getTime()) {
28+
throw new Error('anonymizeBefore is invalid');
29+
}
30+
return date;
31+
};
32+
33+
const parseAnonymizeFields = (
34+
anonymizeFields: string
35+
): typeof anonymizeableFields | null => {
36+
if (!anonymizeFields) {
37+
return null;
38+
}
39+
const fields = new Set(anonymizeFields.split(','));
40+
for (const field of fields) {
41+
if (!(anonymizeableFields as Set<string>).has(field)) {
42+
throw new Error(`Invalid anonymized field name: ${field}`);
43+
}
44+
}
45+
return fields as typeof anonymizeableFields;
46+
};
47+
48+
async function init() {
49+
program
50+
.version(pckg.version)
51+
.option(
52+
'-d, --delete-before [string]',
53+
'An ISO 8601 date string. All carts last updated before this date will be deleted.'
54+
)
55+
.option(
56+
'-a, --anonymize-before [string]',
57+
'An ISO 8601 date string. All carts last updated before this date will be anonymized.'
58+
)
59+
.option(
60+
'-f, --anonymize-fields [string]',
61+
`A comma separated list of fields. Can be any of: ${[
62+
...anonymizeableFields,
63+
].join(', ')}`
64+
)
65+
.parse(process.argv);
66+
67+
const { config } = await setupProcessingTaskObjects('cleanup-old-carts');
68+
69+
const database = await setupAccountDatabase(config.database.mysql.auth);
70+
71+
const deleteBefore = parseDeleteBefore(program.deleteBefore);
72+
const anonymizeBefore = parseAnonymizeBefore(program.anonymizeBefore);
73+
const anonymizeFields = parseAnonymizeFields(program.anonymizeFields);
74+
75+
if (anonymizeBefore && !anonymizeFields) {
76+
throw new Error(
77+
'Anonymize fields must be provided if anonymize before is used'
78+
);
79+
}
80+
81+
const cartCleanup = new CartCleanup(
82+
deleteBefore,
83+
anonymizeBefore,
84+
anonymizeFields,
85+
database
86+
);
87+
88+
await cartCleanup.run();
89+
90+
return 0;
91+
}
92+
93+
if (require.main === module) {
94+
init()
95+
.catch((err) => {
96+
console.error(err);
97+
process.exit(1);
98+
})
99+
.then((result) => process.exit(result));
100+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { AccountDatabase } from '@fxa/shared/db/mysql/account';
6+
7+
export class CartCleanup {
8+
constructor(
9+
private deleteBefore: Date,
10+
private anonymizeBefore: Date | null,
11+
private anonymizeFields: Set<'email' | 'taxAddress'> | null,
12+
private database: AccountDatabase
13+
) {}
14+
15+
async run(): Promise<void> {
16+
await this.database
17+
.deleteFrom('carts')
18+
.where('updatedAt', '<', this.deleteBefore.getTime())
19+
.execute();
20+
21+
if (this.anonymizeBefore && this.anonymizeFields?.size) {
22+
let anonymizePendingQuery = this.database
23+
.updateTable('carts')
24+
.where('updatedAt', '<', this.anonymizeBefore.getTime());
25+
26+
if (this.anonymizeFields.has('email')) {
27+
anonymizePendingQuery = anonymizePendingQuery.set('email', null);
28+
}
29+
if (this.anonymizeFields.has('taxAddress')) {
30+
anonymizePendingQuery = anonymizePendingQuery.set('taxAddress', null);
31+
}
32+
33+
await anonymizePendingQuery.execute();
34+
}
35+
}
36+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import sinon from 'sinon';
6+
import { expect } from 'chai';
7+
import { CartCleanup } from '../../scripts/cleanup-old-carts/cleanup-old-carts';
8+
import Sinon from 'sinon';
9+
10+
describe('CartCleanup', () => {
11+
let cartCleanup: CartCleanup;
12+
let dbStub: {
13+
deleteFrom: Sinon.SinonSpy;
14+
where: Sinon.SinonSpy;
15+
execute: Sinon.SinonSpy;
16+
updateTable: Sinon.SinonSpy;
17+
set: Sinon.SinonSpy;
18+
};
19+
20+
const deleteBefore = new Date('2024-01-01T00:00:00Z');
21+
const anonymizeBefore = new Date('2023-06-01T00:00:00Z');
22+
const anonymizeFields = new Set(['email', 'taxAddress'] as const);
23+
24+
beforeEach(() => {
25+
dbStub = {
26+
deleteFrom: sinon.stub().returnsThis(),
27+
where: sinon.stub().returnsThis(),
28+
execute: sinon.stub().resolves(),
29+
updateTable: sinon.stub().returnsThis(),
30+
set: sinon.stub().returnsThis(),
31+
};
32+
33+
cartCleanup = new CartCleanup(
34+
deleteBefore,
35+
anonymizeBefore,
36+
anonymizeFields,
37+
dbStub
38+
);
39+
});
40+
41+
afterEach(() => {
42+
sinon.restore();
43+
});
44+
45+
describe('run', () => {
46+
it('deletes old carts', async () => {
47+
await cartCleanup.run();
48+
49+
expect(dbStub.deleteFrom.calledWith('carts')).to.be.true;
50+
expect(dbStub.where.calledWith('updatedAt', '<', deleteBefore.getTime()))
51+
.to.be.true;
52+
expect(dbStub.execute.called).to.be.true;
53+
});
54+
55+
it('anonymizes fields within carts', async () => {
56+
await cartCleanup.run();
57+
58+
expect(dbStub.updateTable.calledWith('carts')).to.be.true;
59+
expect(
60+
dbStub.where.calledWith('updatedAt', '<', anonymizeBefore.getTime())
61+
).to.be.true;
62+
expect(dbStub.set.calledWith('email', null)).to.be.true;
63+
expect(dbStub.set.calledWith('taxAddress', null)).to.be.true;
64+
expect(dbStub.execute.calledTwice).to.be.true;
65+
});
66+
67+
it('does not anonymize if no fields are provided', async () => {
68+
cartCleanup = new CartCleanup(
69+
deleteBefore,
70+
anonymizeBefore,
71+
new Set(),
72+
dbStub
73+
);
74+
await cartCleanup.run();
75+
76+
expect(dbStub.updateTable.called).to.be.false;
77+
});
78+
79+
it('does not anonymize if anonymizeBefore is null', async () => {
80+
cartCleanup = new CartCleanup(
81+
deleteBefore,
82+
null,
83+
anonymizeFields,
84+
dbStub
85+
);
86+
await cartCleanup.run();
87+
88+
expect(dbStub.updateTable.called).to.be.false;
89+
});
90+
});
91+
});

0 commit comments

Comments
 (0)