Created
September 20, 2021 14:53
-
-
Save biancadanforth/5e6e2d9cfa0f206e3d1c1a02f7778b5e to your computer and use it in GitHub Desktop.
FXA-3907 exploration: How many users are affected by off-session SCA?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* This Source Code Form is subject to the terms of the Mozilla Public | |
* License, v. 2.0. If a copy of the MPL was not distributed with this | |
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | |
import { AuthLogger } from 'fxa-auth-server/lib/types'; | |
import { ACTIVE_SUBSCRIPTION_STATUSES } from 'fxa-shared/subscriptions/stripe'; | |
import { StatsD } from 'hot-shots'; | |
import stripe from 'stripe'; | |
import Container from 'typedi'; | |
import { CurrencyHelper } from '../lib/payments/currencies'; | |
import { INVOICES_RESOURCE, StripeHelper, SUBSCRIPTIONS_RESOURCE } from '../lib/payments/stripe'; | |
import { configureSentry } from '../lib/sentry'; | |
const config = require('../config').getProperties(); | |
class InvoiceRequiresSCAChecker { | |
private stripe: stripe; | |
constructor(private log: AuthLogger, private stripeHelper: StripeHelper) { | |
this.stripe = (this.stripeHelper as any).stripe as stripe; | |
} | |
private timer(ms: number) { | |
return new Promise(res => setTimeout(res, ms)); | |
} | |
private async *invoicesForPaymentIntentRequiresAction(limit?: number) { | |
let count = 0; | |
for await (const event of this.stripe.events.list({ | |
limit: 100, | |
type: 'payment_intent.requires_action', | |
// Unfortunately this API doesn't support expandable objects | |
})) { | |
const paymentIntent = event.data.object; | |
this.log.info('paymentIntent found', { | |
eventId: event.id, | |
paymentIntent, | |
}); | |
await this.timer(200); | |
// @ts-ignore next line | |
const invoice = await this.stripeHelper.expandResource(paymentIntent.invoice, INVOICES_RESOURCE); | |
// We only care about recurring (i.e. off-session) payments. | |
if ( | |
!invoice || | |
(invoice as any).billing_reason !== 'subscription_cycle' | |
) { | |
continue; | |
} | |
this.log.info('recurring Invoice found', { | |
invoice, | |
}); | |
yield invoice; | |
count++; | |
if (limit && count >= limit) { | |
break; | |
} | |
await this.timer(200); | |
} | |
} | |
async countRecurringInvoicesRequiringSCA() { | |
let invoiceCount = 0; | |
let activeSubscriptionCount = 0; | |
for await (const invoice of this.invoicesForPaymentIntentRequiresAction()) { | |
invoiceCount++; | |
// We've found a recurring invoice with a paymentIntent.status of "requires_action". | |
// In other words, this is a recurring invoice that requires SCA | |
// Get the subscription associated with the invoice and check its status, | |
// to see if the user resolved the SCA on their own. | |
await this.timer(200); | |
const subscription = await this.stripeHelper.expandResource(invoice.subscription, SUBSCRIPTIONS_RESOURCE); | |
if (ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status)) { | |
this.log.info('active Subscription found', { | |
subscriptionId: subscription.id, | |
}); | |
activeSubscriptionCount++; | |
} | |
} | |
// While it is typical for there to be more than one invoice per subscription (1 invoice per billing period), | |
// since we're only looking at the last 30 days, and our shortest billing period in prod is monthly, | |
// invoices:subscriptions should map 1:1 in prod, so we can use the invoice count as a proxy for the number | |
// of subscriptions that have required SCA on a recurring charge in the past month. | |
this.log.info('Total recurring invoices that require(d) SCA: ', { total: invoiceCount }); | |
this.log.info('Total active subscriptions for those invoices: ', { total: activeSubscriptionCount }); | |
} | |
} | |
export async function init() { | |
configureSentry(undefined, config); | |
const statsd = config.statsd.enabled | |
? new StatsD({ | |
...config.statsd, | |
errorHandler: (err) => { | |
// eslint-disable-next-line no-use-before-define | |
log.error('statsd.error', err); | |
}, | |
}) | |
: ({ | |
increment: () => {}, | |
timing: () => {}, | |
close: () => {}, | |
} as unknown as StatsD); | |
Container.set(StatsD, statsd); | |
const log = require('../lib/log')({ ...config.log, statsd }); | |
const currencyHelper = new CurrencyHelper(config); | |
Container.set(CurrencyHelper, currencyHelper); | |
const stripeHelper = new StripeHelper(log, config, statsd); | |
Container.set(StripeHelper, stripeHelper); | |
const scaChecker = new InvoiceRequiresSCAChecker(log, stripeHelper); | |
await scaChecker.countRecurringInvoicesRequiringSCA(); | |
return 0; | |
} | |
if (require.main === module) { | |
init() | |
.catch((err) => { | |
console.error(err); | |
process.exit(1); | |
}) | |
.then((result) => process.exit(result)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
From the mozilla/fxa root dir (after fetching the latest
main
andyarn install
):fxa/packages/fxa-auth-server/scripts
yarn start
cd packages/fxa-auth-server
stripe config --list
to get thetest_mode_api_key
value and copy it to the clipboardexport SUBHUB_STRIPE_APIKEY=${test_mode_api_key_value}
NODE_ENV=dev ts-node ./scripts/temp-fxa-3907-script.ts
Example output:
To use with the
live_mode_api_key
:SUBHUB_STRIPE_APIKEY
env var to use a restricted live Stripe API key (can create one from the Stripe dashboard) and add some sleeps in between Stripe API calls to avoid hitting Stripe's request rate limit.