@@ -10,6 +10,15 @@ import { StripeHelper } from '../../lib/payments/stripe';
1010import { PayPalHelper } from 'packages/fxa-auth-server/lib/payments/paypal' ;
1111import { RefundType } from '@fxa/payments/paypal' ;
1212
13+ /**
14+ * For RAM-preserving pruposes only
15+ */
16+ const QUEUE_SIZE_LIMIT = 1000 ;
17+ /**
18+ * For RAM-preserving pruposes only
19+ */
20+ const QUEUE_CONCURRENCY_LIMIT = 100 ;
21+
1322export class PlanCanceller {
1423 private stripeQueue : PQueue ;
1524 private stripe : Stripe ;
@@ -18,6 +27,7 @@ export class PlanCanceller {
1827 * A tool to cancel all subscriptions under a plan
1928 * @param priceId A Stripe plan or price ID for which all subscriptions will be cancelled
2029 * @param remainingValueMode Configuration for how to handle remaining subscription value
30+ * @param proratedRefundRate The rate per day (in whole cents) at which to refund subscriptions in proratedRefund mode
2131 * @param excludePlanIds A list of Stripe plan or price ID which if customers have will not be cancelled
2232 * @param outputFile A CSV file to output a report of affected subscriptions to
2333 * @param stripeHelper An instance of StripeHelper
@@ -28,13 +38,22 @@ export class PlanCanceller {
2838 constructor (
2939 private priceId : string ,
3040 private remainingValueMode : "noaction" | "refund" | "prorate" | "proratedRefund" ,
41+ private proratedRefundRate : number | null ,
3142 private excludePlanIds : string [ ] ,
3243 private outputFile : string ,
3344 private stripeHelper : StripeHelper ,
3445 private paypalHelper : PayPalHelper ,
3546 public dryRun : boolean ,
3647 rateLimit : number
3748 ) {
49+ if ( remainingValueMode === 'proratedRefund' && proratedRefundRate === null ) {
50+ throw new Error ( "proratedRefundRate must be provided when using proratedRefund mode" ) ;
51+ }
52+
53+ if ( proratedRefundRate !== null && proratedRefundRate <= 0 ) {
54+ throw new Error ( "proratedRefundRate must be greater than zero" ) ;
55+ }
56+
3857 this . stripe = this . stripeHelper . stripe ;
3958
4059 this . stripeQueue = new PQueue ( {
@@ -46,12 +65,22 @@ export class PlanCanceller {
4665 async run ( ) : Promise < void > {
4766 await this . writeReportHeader ( ) ;
4867
49- await this . stripe . subscriptions . list ( {
68+ const conversionQueue = new PQueue ( { concurrency : QUEUE_CONCURRENCY_LIMIT } ) ;
69+
70+ for await ( const subscription of this . stripe . subscriptions . list ( {
5071 price : this . priceId ,
5172 limit : 100 ,
52- } ) . autoPagingEach ( ( subscription ) => {
53- return this . processSubscription ( subscription ) ;
54- } ) ;
73+ } ) ) {
74+ if ( conversionQueue . size + conversionQueue . pending >= QUEUE_SIZE_LIMIT ) {
75+ await conversionQueue . onSizeLessThan ( QUEUE_SIZE_LIMIT - QUEUE_CONCURRENCY_LIMIT ) ;
76+ }
77+
78+ conversionQueue . add ( ( ) => {
79+ return this . processSubscription ( subscription ) ;
80+ } ) ;
81+ }
82+
83+ await conversionQueue . onIdle ( ) ;
5584 }
5685
5786 async processSubscription (
@@ -64,6 +93,8 @@ export class PlanCanceller {
6493 : subscription . customer . id ;
6594
6695 try {
96+ console . log ( `Processing ${ subscription . id } ` ) ;
97+
6798 const customer = await this . fetchCustomer ( customerId ) ;
6899 if ( ! customer ?. subscriptions ?. data ) {
69100 throw new Error ( `Customer not found: ${ customerId } ` ) ;
@@ -75,7 +106,7 @@ export class PlanCanceller {
75106 let approximateAmountWasOwed : number | null = null ;
76107 let daysSinceLastBill : number | null = null ;
77108 let daysUntilNextBill : number | null = null ;
78- let previousInvoiceAmountPaid : number | null = null ;
109+ let previousInvoiceAmountDue : number | null = null ;
79110 let isOwed = ! isExcluded ;
80111
81112 if ( ! isExcluded ) {
@@ -85,7 +116,7 @@ export class PlanCanceller {
85116 approximateAmountWasOwed = calculation . refundAmount ;
86117 daysSinceLastBill = calculation . daysSinceBill ;
87118 daysUntilNextBill = calculation . daysUntilNextBill ;
88- previousInvoiceAmountPaid = calculation . invoice . amount_paid ;
119+ previousInvoiceAmountDue = calculation . invoice . amount_due ;
89120 } catch ( e ) {
90121 console . warn ( e ) ;
91122 }
@@ -95,6 +126,9 @@ export class PlanCanceller {
95126 await this . enqueueRequest ( ( ) =>
96127 this . stripe . subscriptions . cancel ( subscription . id , {
97128 prorate : this . remainingValueMode === "prorate" ,
129+ cancellation_details : {
130+ comment : "administrative_cancellation:subplat_script"
131+ }
98132 } )
99133 ) ;
100134 }
@@ -131,7 +165,7 @@ export class PlanCanceller {
131165 approximateAmountWasOwed,
132166 daysSinceLastBill,
133167 daysUntilNextBill,
134- previousInvoiceAmountPaid ,
168+ previousInvoiceAmountDue ,
135169 isOwed,
136170 error : false ,
137171 } ) ;
@@ -147,7 +181,7 @@ export class PlanCanceller {
147181 approximateAmountWasOwed : null ,
148182 daysSinceLastBill : null ,
149183 daysUntilNextBill : null ,
150- previousInvoiceAmountPaid : null ,
184+ previousInvoiceAmountDue : null ,
151185 isOwed : false ,
152186 error : true ,
153187 } ) ;
@@ -213,7 +247,7 @@ export class PlanCanceller {
213247 }
214248 }
215249
216- return invoice . amount_paid ;
250+ return invoice . amount_due ;
217251 }
218252
219253 /**
@@ -222,6 +256,10 @@ export class PlanCanceller {
222256 * @returns Object containing refundAmount, daysSinceBill, daysUntilNextBill, and invoice
223257 */
224258 async calculateRefundAmount ( subscription : Stripe . Subscription ) : Promise < { refundAmount : number , daysSinceBill : number , daysUntilNextBill : number , invoice : Stripe . Invoice } > {
259+ if ( this . proratedRefundRate === null ) {
260+ throw new Error ( "proratedRefundRate must be specified to use calculateRefundAmount" ) ;
261+ }
262+
225263 if ( ! subscription . latest_invoice ) {
226264 throw new Error ( `No latest invoice for ${ subscription . id } ` ) ;
227265 }
@@ -245,15 +283,13 @@ export class PlanCanceller {
245283 const periodEnd = new Date ( subscription . current_period_end * 1000 ) ;
246284 const now = new Date ( ) ;
247285
248- const totalPeriodMs = periodEnd . getTime ( ) - periodStart . getTime ( ) ;
249286 const timeElapsedMs = now . getTime ( ) - periodStart . getTime ( ) ;
250287 const timeRemainingMs = periodEnd . getTime ( ) - now . getTime ( ) ;
251288
252289 const daysSinceBill = Math . floor ( timeElapsedMs / oneDayMs ) ;
253- const totalDaysInPeriod = Math . floor ( totalPeriodMs / oneDayMs ) ;
254290 const daysUntilNextBill = Math . floor ( timeRemainingMs / oneDayMs ) ;
255291
256- const refundAmount = Math . floor ( ( daysUntilNextBill / totalDaysInPeriod ) * invoice . amount_paid ) ;
292+ const refundAmount = daysUntilNextBill * this . proratedRefundRate ;
257293
258294 return { refundAmount, daysSinceBill, daysUntilNextBill, invoice } ;
259295 }
@@ -263,16 +299,16 @@ export class PlanCanceller {
263299 const refundAmount = calculation . refundAmount ;
264300 const invoice = calculation . invoice ;
265301
266- if ( refundAmount > invoice . amount_paid ) {
267- throw new Error ( `Will not refund ${ invoice . id } for ${ refundAmount } as it would eclipse the amount paid on the invoice` ) ;
302+ if ( refundAmount > invoice . amount_due ) {
303+ throw new Error ( `Will not refund ${ invoice . id } for ${ refundAmount } as it would eclipse the amount due on the invoice` ) ;
268304 }
269305
270306 if ( refundAmount <= 0 ) {
271307 throw new Error ( `Will not refund ${ invoice . id } for ${ refundAmount } as it is less than or equal to zero` ) ;
272308 }
273309
274310 if ( invoice . paid_out_of_band ) {
275- const behavior = refundAmount === invoice . amount_paid ? {
311+ const behavior = refundAmount === invoice . amount_due ? {
276312 refundType : RefundType . Full
277313 } as const : {
278314 refundType : RefundType . Partial ,
@@ -333,7 +369,7 @@ export class PlanCanceller {
333369 "approximateAmountWasOwed" ,
334370 "daysSinceLastBill" ,
335371 "daysUntilNextBill" ,
336- "previousInvoiceAmountPaid " ,
372+ "previousInvoiceAmountDue " ,
337373 "isOwed" ,
338374 "error"
339375 ] ;
@@ -354,7 +390,7 @@ export class PlanCanceller {
354390 approximateAmountWasOwed : number | null ,
355391 daysSinceLastBill : number | null ,
356392 daysUntilNextBill : number | null ,
357- previousInvoiceAmountPaid : number | null ,
393+ previousInvoiceAmountDue : number | null ,
358394 isOwed : boolean ,
359395 error : boolean
360396 } ) {
@@ -370,13 +406,14 @@ export class PlanCanceller {
370406 args . approximateAmountWasOwed ?? "null" ,
371407 args . daysSinceLastBill ?? "null" ,
372408 args . daysUntilNextBill ?? "null" ,
373- args . previousInvoiceAmountPaid ?? "null" ,
409+ args . previousInvoiceAmountDue ?? "null" ,
374410 args . isOwed ,
375411 args . error
376412 ] ;
377413
378414 const reportCSV = data . join ( ',' ) + '\n' ;
379415
416+ console . log ( reportCSV ) ;
380417 await writeFile ( this . outputFile , reportCSV , {
381418 flag : 'a+' ,
382419 encoding : 'utf-8' ,
0 commit comments