@@ -45,6 +45,7 @@ import {
4545 SetupIntentManager ,
4646 PromotionCodeNotFoundError ,
4747 STRIPE_SUBSCRIPTION_METADATA ,
48+ SubplatInterval ,
4849 SubscriptionManager ,
4950 TaxAddressFactory ,
5051 SubPlatPaymentMethodType ,
@@ -137,6 +138,9 @@ import {
137138 MockNimbusClientConfigProvider ,
138139 NimbusClient ,
139140} from '@fxa/shared/experiments' ;
141+ import { FreeTrialManager } from './free-trial.manager' ;
142+ import { MockFreeTrialConfigProvider } from './free-trial.config' ;
143+ import type { FreeTrial } from '@fxa/shared/cms' ;
140144
141145describe ( 'CheckoutService' , ( ) => {
142146 let accountCustomerManager : AccountCustomerManager ;
@@ -160,6 +164,8 @@ describe('CheckoutService', () => {
160164 let subscriptionManager : SubscriptionManager ;
161165 let paymentMethodManager : PaymentMethodManager ;
162166 let gleanService : PaymentsGleanService ;
167+ let freeTrialManager : FreeTrialManager ;
168+ let nimbusManager : NimbusManager ;
163169
164170 const mockLogger = {
165171 error : jest . fn ( ) ,
@@ -180,6 +186,8 @@ describe('CheckoutService', () => {
180186 CurrencyManager ,
181187 EligibilityManager ,
182188 EligibilityService ,
189+ FreeTrialManager ,
190+ MockFreeTrialConfigProvider ,
183191 AppleIapPurchaseManager ,
184192 AppleIapClient ,
185193 MockAppleIapClientConfigProvider ,
@@ -259,6 +267,8 @@ describe('CheckoutService', () => {
259267 subscriptionManager = moduleRef . get ( SubscriptionManager ) ;
260268 paymentMethodManager = moduleRef . get ( PaymentMethodManager ) ;
261269 gleanService = moduleRef . get ( PaymentsGleanService ) ;
270+ freeTrialManager = moduleRef . get ( FreeTrialManager ) ;
271+ nimbusManager = moduleRef . get ( NimbusManager ) ;
262272 } ) ;
263273
264274 describe ( 'prePaySteps' , ( ) => {
@@ -805,30 +815,33 @@ describe('CheckoutService', () => {
805815 } ) ;
806816 } ) ;
807817
808- it ( 'handles free payments for customers with default payment method ' , async ( ) => {
818+ it ( 'handles zero-amount invoices by creating a setup intent ' , async ( ) => {
809819 const freeInvoice = StripeResponseFactory (
810820 StripeInvoiceFactory ( {
811821 payment_intent : mockPaymentIntent . id ,
812822 amount_due : 0 ,
813823 status : 'paid' ,
814824 } )
815825 ) ;
816- const existingPayentMethod = StripeResponseFactory (
817- StripePaymentMethodFactory ( )
826+ const mockSetupIntent = StripeResponseFactory (
827+ StripeSetupIntentFactory ( {
828+ status : 'succeeded' ,
829+ payment_method : StripePaymentMethodFactory ( ) . id ,
830+ } )
818831 ) ;
819832 jest . spyOn ( invoiceManager , 'retrieve' ) . mockResolvedValue ( freeInvoice ) ;
820833 jest
821- . spyOn ( customerManager , 'getDefaultPaymentMethod ' )
822- . mockResolvedValue ( existingPayentMethod ) ;
823- expect (
834+ . spyOn ( setupIntentManager , 'createAndConfirm ' )
835+ . mockResolvedValue ( mockSetupIntent ) ;
836+ await expect (
824837 checkoutService . payWithStripe (
825838 mockCart ,
826839 mockConfirmationToken . id ,
827840 mockAttributionData ,
828841 mockRequestArgs ,
829842 mockCart . uid
830843 )
831- ) . resolves ;
844+ ) . resolves . not . toThrow ( ) ;
832845 } ) ;
833846
834847 describe ( 'upgrade' , ( ) => {
@@ -1640,4 +1653,197 @@ describe('CheckoutService', () => {
16401653 ) . rejects . toThrow ( / D e t e r m i n e C h e c k o u t A m o u n t S u b s c r i p t i o n R e q u i r e d E r r o r / ) ;
16411654 } ) ;
16421655 } ) ;
1656+
1657+ describe ( 'getFreeTrialEligibility' , ( ) => {
1658+ const mockUid = faker . string . uuid ( ) ;
1659+ const mockOfferingConfigId = faker . string . alphanumeric ( 10 ) ;
1660+ const mockCountryCode = 'US' ;
1661+ const mockInterval = 'monthly' as SubplatInterval ;
1662+
1663+ const mockFreeTrial : FreeTrial = {
1664+ internalName : 'test-free-trial' ,
1665+ intervals : [ 'monthly' ] ,
1666+ trialLengthDays : 30 ,
1667+ countries : [ 'US' , 'CA' ] ,
1668+ cooldownPeriodMonths : 6 ,
1669+ } ;
1670+
1671+ const mockNimbusResult = {
1672+ Features : {
1673+ 'welcome-feature' : { enabled : false } ,
1674+ 'free-trial-feature' : { enabled : true } ,
1675+ } ,
1676+ Enrollments : [ ] ,
1677+ } ;
1678+
1679+ const mockFreeTrialUtil = {
1680+ getResult : jest . fn ( ) . mockReturnValue ( [ mockFreeTrial ] ) ,
1681+ freeTrial : { freeTrials : [ mockFreeTrial ] } ,
1682+ } ;
1683+
1684+ const baseArgs = {
1685+ uid : mockUid ,
1686+ offeringConfigId : mockOfferingConfigId ,
1687+ countryCode : mockCountryCode ,
1688+ interval : mockInterval ,
1689+ eligibilityStatus : EligibilityStatus . CREATE ,
1690+ } ;
1691+
1692+ it ( 'returns null when eligibilityStatus is UPGRADE' , async ( ) => {
1693+ const result = await checkoutService . getFreeTrialEligibility ( {
1694+ ...baseArgs ,
1695+ eligibilityStatus : EligibilityStatus . UPGRADE ,
1696+ } ) ;
1697+
1698+ expect ( result ) . toBeNull ( ) ;
1699+ } ) ;
1700+
1701+ it ( 'returns null when Nimbus returns null' , async ( ) => {
1702+ jest
1703+ . spyOn ( nimbusManager , 'fetchExperiments' )
1704+ . mockResolvedValue ( null ) ;
1705+ jest
1706+ . spyOn ( nimbusManager , 'generateNimbusId' )
1707+ . mockReturnValue ( 'nimbus-id' ) ;
1708+ jest
1709+ . spyOn ( productConfigurationManager , 'getFreeTrial' )
1710+ . mockResolvedValue ( mockFreeTrialUtil as any ) ;
1711+
1712+ const result = await checkoutService . getFreeTrialEligibility ( baseArgs ) ;
1713+
1714+ expect ( result ) . toBeNull ( ) ;
1715+ } ) ;
1716+
1717+ it ( 'returns null when Nimbus flag is disabled' , async ( ) => {
1718+ jest . spyOn ( nimbusManager , 'fetchExperiments' ) . mockResolvedValue ( {
1719+ ...mockNimbusResult ,
1720+ Features : {
1721+ ...mockNimbusResult . Features ,
1722+ 'free-trial-feature' : { enabled : false } ,
1723+ } ,
1724+ } ) ;
1725+ jest
1726+ . spyOn ( nimbusManager , 'generateNimbusId' )
1727+ . mockReturnValue ( 'nimbus-id' ) ;
1728+ jest
1729+ . spyOn ( productConfigurationManager , 'getFreeTrial' )
1730+ . mockResolvedValue ( mockFreeTrialUtil as any ) ;
1731+
1732+ const result = await checkoutService . getFreeTrialEligibility ( baseArgs ) ;
1733+
1734+ expect ( result ) . toBeNull ( ) ;
1735+ } ) ;
1736+
1737+ it ( 'returns null when no Strapi free trial config exists' , async ( ) => {
1738+ jest
1739+ . spyOn ( nimbusManager , 'fetchExperiments' )
1740+ . mockResolvedValue ( mockNimbusResult ) ;
1741+ jest
1742+ . spyOn ( nimbusManager , 'generateNimbusId' )
1743+ . mockReturnValue ( 'nimbus-id' ) ;
1744+ jest . spyOn ( productConfigurationManager , 'getFreeTrial' ) . mockResolvedValue ( {
1745+ getResult : jest . fn ( ) . mockReturnValue ( undefined ) ,
1746+ freeTrial : { freeTrials : [ ] } ,
1747+ } as any ) ;
1748+
1749+ const result = await checkoutService . getFreeTrialEligibility ( baseArgs ) ;
1750+
1751+ expect ( result ) . toBeNull ( ) ;
1752+ } ) ;
1753+
1754+ it ( 'returns null when country not in config countries' , async ( ) => {
1755+ jest
1756+ . spyOn ( nimbusManager , 'fetchExperiments' )
1757+ . mockResolvedValue ( mockNimbusResult ) ;
1758+ jest
1759+ . spyOn ( nimbusManager , 'generateNimbusId' )
1760+ . mockReturnValue ( 'nimbus-id' ) ;
1761+ jest
1762+ . spyOn ( productConfigurationManager , 'getFreeTrial' )
1763+ . mockResolvedValue ( mockFreeTrialUtil as any ) ;
1764+
1765+ const result = await checkoutService . getFreeTrialEligibility ( {
1766+ ...baseArgs ,
1767+ countryCode : 'DE' ,
1768+ } ) ;
1769+
1770+ expect ( result ) . toBeNull ( ) ;
1771+ } ) ;
1772+
1773+ it ( 'returns null when interval not in config intervals' , async ( ) => {
1774+ jest
1775+ . spyOn ( nimbusManager , 'fetchExperiments' )
1776+ . mockResolvedValue ( mockNimbusResult ) ;
1777+ jest
1778+ . spyOn ( nimbusManager , 'generateNimbusId' )
1779+ . mockReturnValue ( 'nimbus-id' ) ;
1780+ jest
1781+ . spyOn ( productConfigurationManager , 'getFreeTrial' )
1782+ . mockResolvedValue ( mockFreeTrialUtil as any ) ;
1783+
1784+ const result = await checkoutService . getFreeTrialEligibility ( {
1785+ ...baseArgs ,
1786+ interval : 'yearly' as SubplatInterval ,
1787+ } ) ;
1788+
1789+ expect ( result ) . toBeNull ( ) ;
1790+ } ) ;
1791+
1792+ it ( 'returns null when trialLengthDays is 0' , async ( ) => {
1793+ jest
1794+ . spyOn ( nimbusManager , 'fetchExperiments' )
1795+ . mockResolvedValue ( mockNimbusResult ) ;
1796+ jest
1797+ . spyOn ( nimbusManager , 'generateNimbusId' )
1798+ . mockReturnValue ( 'nimbus-id' ) ;
1799+ jest . spyOn ( productConfigurationManager , 'getFreeTrial' ) . mockResolvedValue ( {
1800+ getResult : jest . fn ( ) . mockReturnValue ( [
1801+ { ...mockFreeTrial , trialLengthDays : 0 } ,
1802+ ] ) ,
1803+ freeTrial : { freeTrials : [ { ...mockFreeTrial , trialLengthDays : 0 } ] } ,
1804+ } as any ) ;
1805+
1806+ const result = await checkoutService . getFreeTrialEligibility ( baseArgs ) ;
1807+
1808+ expect ( result ) . toBeNull ( ) ;
1809+ } ) ;
1810+
1811+ it ( 'returns null when blocked by cooldown' , async ( ) => {
1812+ jest
1813+ . spyOn ( nimbusManager , 'fetchExperiments' )
1814+ . mockResolvedValue ( mockNimbusResult ) ;
1815+ jest
1816+ . spyOn ( nimbusManager , 'generateNimbusId' )
1817+ . mockReturnValue ( 'nimbus-id' ) ;
1818+ jest
1819+ . spyOn ( productConfigurationManager , 'getFreeTrial' )
1820+ . mockResolvedValue ( mockFreeTrialUtil as any ) ;
1821+ jest
1822+ . spyOn ( freeTrialManager , 'isBlockedByCooldown' )
1823+ . mockResolvedValue ( true ) ;
1824+
1825+ const result = await checkoutService . getFreeTrialEligibility ( baseArgs ) ;
1826+
1827+ expect ( result ) . toBeNull ( ) ;
1828+ } ) ;
1829+
1830+ it ( 'returns the FreeTrial object when all criteria pass' , async ( ) => {
1831+ jest
1832+ . spyOn ( nimbusManager , 'fetchExperiments' )
1833+ . mockResolvedValue ( mockNimbusResult ) ;
1834+ jest
1835+ . spyOn ( nimbusManager , 'generateNimbusId' )
1836+ . mockReturnValue ( 'nimbus-id' ) ;
1837+ jest
1838+ . spyOn ( productConfigurationManager , 'getFreeTrial' )
1839+ . mockResolvedValue ( mockFreeTrialUtil as any ) ;
1840+ jest
1841+ . spyOn ( freeTrialManager , 'isBlockedByCooldown' )
1842+ . mockResolvedValue ( false ) ;
1843+
1844+ const result = await checkoutService . getFreeTrialEligibility ( baseArgs ) ;
1845+
1846+ expect ( result ) . toEqual ( mockFreeTrial ) ;
1847+ } ) ;
1848+ } ) ;
16431849} ) ;
0 commit comments