@@ -361,6 +361,112 @@ describe('MembersCSVImporterStripeUtils', function () {
361361 NEW_STRIPE_PRICE_ID ,
362362 { prorationBehavior : 'none' }
363363 ) , true ) ;
364+
365+ // Assert the new price was not archived — that cleanup only runs when the update fails (#22115)
366+ sinon . assert . notCalled ( stripeAPIServiceStub . updatePrice ) ;
367+ } ) ;
368+
369+ it ( 'archives the newly-created price if updating the subscription fails (#22115)' , async function ( ) {
370+ const stripeAPIServiceStub = getStripeApiServiceStub ( ) ;
371+ const NEW_STRIPE_PRICE_ID = 'new_stripe_price_id' ;
372+ const updateError = new Error ( 'Stripe rejected the subscription update' ) ;
373+
374+ stripeAPIServiceStub . createPrice . resolves ( { id : NEW_STRIPE_PRICE_ID } ) ;
375+ stripeAPIServiceStub . updateSubscriptionItemPrice . rejects ( updateError ) ;
376+
377+ const productRepositoryStub = getProductRepositoryStub ( { resolveGhostProductPrice : false } ) ;
378+ const membersCSVImporterStripeUtils = new MembersCSVImporterStripeUtils ( {
379+ stripeAPIService : stripeAPIServiceStub ,
380+ productRepository : productRepositoryStub
381+ } ) ;
382+
383+ // The original error surfaces...
384+ await assert . rejects (
385+ membersCSVImporterStripeUtils . forceStripeSubscriptionToProduct ( {
386+ customer_id : CUSTOMER_ID ,
387+ product_id : PRODUCT_ID
388+ } , OPTIONS ) ,
389+ err => err === updateError
390+ ) ;
391+
392+ // ...and the price we just created is archived so it can't be orphaned/accumulate
393+ sinon . assert . calledOnce ( stripeAPIServiceStub . updatePrice ) ;
394+ assert . equal ( stripeAPIServiceStub . updatePrice . calledWithExactly ( NEW_STRIPE_PRICE_ID , { active : false } ) , true ) ;
395+ } ) ;
396+
397+ it ( 'surfaces the original update error even if archiving the orphaned price also fails (#22115)' , async function ( ) {
398+ const stripeAPIServiceStub = getStripeApiServiceStub ( ) ;
399+ const NEW_STRIPE_PRICE_ID = 'new_stripe_price_id' ;
400+ const updateError = new Error ( 'Stripe rejected the subscription update' ) ;
401+
402+ stripeAPIServiceStub . createPrice . resolves ( { id : NEW_STRIPE_PRICE_ID } ) ;
403+ stripeAPIServiceStub . updateSubscriptionItemPrice . rejects ( updateError ) ;
404+ stripeAPIServiceStub . updatePrice . rejects ( new Error ( 'archiving failed too' ) ) ;
405+
406+ const productRepositoryStub = getProductRepositoryStub ( { resolveGhostProductPrice : false } ) ;
407+ const membersCSVImporterStripeUtils = new MembersCSVImporterStripeUtils ( {
408+ stripeAPIService : stripeAPIServiceStub ,
409+ productRepository : productRepositoryStub
410+ } ) ;
411+
412+ // The original update error must surface, not the archive failure
413+ await assert . rejects (
414+ membersCSVImporterStripeUtils . forceStripeSubscriptionToProduct ( {
415+ customer_id : CUSTOMER_ID ,
416+ product_id : PRODUCT_ID
417+ } , OPTIONS ) ,
418+ err => err === updateError
419+ ) ;
420+ } ) ;
421+
422+ it ( 'does not archive anything when updating to an existing matching price fails — nothing was created (#22115)' , async function ( ) {
423+ const stripeAPIServiceStub = getStripeApiServiceStub ( ) ;
424+ const updateError = new Error ( 'Stripe rejected the subscription update' ) ;
425+ stripeAPIServiceStub . updateSubscriptionItemPrice . rejects ( updateError ) ;
426+
427+ // A matching Ghost price already exists, but with a different Stripe price id, so the
428+ // subscription genuinely needs updating (and can therefore fail) — yet no new price is
429+ // created on this path, so there must be nothing to archive.
430+ const productRepositoryStub = getProductRepositoryStub ( {
431+ resolveGhostProductPrice : true ,
432+ ghostProductStripePriceId : 'existing_stripe_price_id'
433+ } ) ;
434+ const membersCSVImporterStripeUtils = new MembersCSVImporterStripeUtils ( {
435+ stripeAPIService : stripeAPIServiceStub ,
436+ productRepository : productRepositoryStub
437+ } ) ;
438+
439+ await assert . rejects (
440+ membersCSVImporterStripeUtils . forceStripeSubscriptionToProduct ( {
441+ customer_id : CUSTOMER_ID ,
442+ product_id : PRODUCT_ID
443+ } , OPTIONS ) ,
444+ err => err === updateError
445+ ) ;
446+ sinon . assert . notCalled ( stripeAPIServiceStub . createPrice ) ;
447+ sinon . assert . notCalled ( stripeAPIServiceStub . updatePrice ) ;
448+ } ) ;
449+
450+ it ( 'does not update the subscription or archive when creating the new price itself fails (#22115)' , async function ( ) {
451+ const stripeAPIServiceStub = getStripeApiServiceStub ( ) ;
452+ const createError = new Error ( 'Stripe rejected createPrice' ) ;
453+ stripeAPIServiceStub . createPrice . rejects ( createError ) ;
454+
455+ const productRepositoryStub = getProductRepositoryStub ( { resolveGhostProductPrice : false } ) ;
456+ const membersCSVImporterStripeUtils = new MembersCSVImporterStripeUtils ( {
457+ stripeAPIService : stripeAPIServiceStub ,
458+ productRepository : productRepositoryStub
459+ } ) ;
460+
461+ await assert . rejects (
462+ membersCSVImporterStripeUtils . forceStripeSubscriptionToProduct ( {
463+ customer_id : CUSTOMER_ID ,
464+ product_id : PRODUCT_ID
465+ } , OPTIONS ) ,
466+ err => err === createError
467+ ) ;
468+ sinon . assert . notCalled ( stripeAPIServiceStub . updateSubscriptionItemPrice ) ;
469+ sinon . assert . notCalled ( stripeAPIServiceStub . updatePrice ) ;
364470 } ) ;
365471
366472 it ( 'creates a new product in Stripe if one does not already existing for the Ghost product' , async function ( ) {
0 commit comments