@@ -22,7 +22,12 @@ function createBaseAdapter() {
2222const asAdapter = ( base : ReturnType < typeof createBaseAdapter > ) =>
2323 guardSubscriptionPlanWrites ( base as unknown as Parameters < typeof guardSubscriptionPlanWrites > [ 0 ] )
2424
25- const ORG_ROW = { id : 'sub-1' , referenceId : 'org-1' , plan : 'team_6000' }
25+ const ORG_ROW = {
26+ id : 'sub-1' ,
27+ referenceId : 'org-1' ,
28+ plan : 'team_6000' ,
29+ stripeSubscriptionId : 'stripe-sub-1' ,
30+ }
2631const WHERE = [ { field : 'id' , value : 'sub-1' } ]
2732
2833describe ( 'guardSubscriptionPlanWrites' , ( ) => {
@@ -144,6 +149,87 @@ describe('guardSubscriptionPlanWrites', () => {
144149 expect ( base . update ) . toHaveBeenCalled ( )
145150 } )
146151
152+ it ( 'blocks rebinding a personal subscription to a different Stripe subscription' , async ( ) => {
153+ const base = createBaseAdapter ( )
154+ const personalPro = {
155+ id : 'personal-pro' ,
156+ referenceId : 'user-1' ,
157+ plan : 'pro' ,
158+ stripeSubscriptionId : 'sub_personal_pro' ,
159+ }
160+ base . findOne . mockResolvedValueOnce ( personalPro )
161+
162+ const guarded = asAdapter ( base )
163+ await expect (
164+ guarded . update ( {
165+ model : 'subscription' ,
166+ where : [ { field : 'id' , value : personalPro . id } ] as never ,
167+ update : {
168+ stripeSubscriptionId : 'sub_enterprise' ,
169+ status : 'active' ,
170+ periodEnd : new Date ( '2026-09-11T18:36:09Z' ) ,
171+ billingInterval : 'month' ,
172+ } ,
173+ } )
174+ ) . rejects . toThrow ( / a l r e a d y b o u n d t o S t r i p e s u b s c r i p t i o n s u b _ p e r s o n a l _ p r o / )
175+
176+ expect ( base . update ) . not . toHaveBeenCalled ( )
177+ } )
178+
179+ it ( 'allows Stripe state updates when the subscription ID is unchanged' , async ( ) => {
180+ const base = createBaseAdapter ( )
181+ base . findOne . mockResolvedValueOnce ( {
182+ id : 'personal-pro' ,
183+ referenceId : 'user-1' ,
184+ plan : 'pro' ,
185+ stripeSubscriptionId : 'sub_personal_pro' ,
186+ } )
187+
188+ const guarded = asAdapter ( base )
189+ await guarded . update ( {
190+ model : 'subscription' ,
191+ where : WHERE as never ,
192+ update : {
193+ stripeSubscriptionId : 'sub_personal_pro' ,
194+ status : 'active' ,
195+ cancelAtPeriodEnd : true ,
196+ } ,
197+ } )
198+
199+ expect ( base . update ) . toHaveBeenCalledWith (
200+ expect . objectContaining ( {
201+ update : {
202+ stripeSubscriptionId : 'sub_personal_pro' ,
203+ status : 'active' ,
204+ cancelAtPeriodEnd : true ,
205+ } ,
206+ } )
207+ )
208+ } )
209+
210+ it ( 'allows binding an unbound local subscription to Stripe' , async ( ) => {
211+ const base = createBaseAdapter ( )
212+ base . findOne . mockResolvedValueOnce ( {
213+ id : 'new-subscription' ,
214+ referenceId : 'user-1' ,
215+ plan : 'pro' ,
216+ stripeSubscriptionId : null ,
217+ } )
218+
219+ const guarded = asAdapter ( base )
220+ await guarded . update ( {
221+ model : 'subscription' ,
222+ where : WHERE as never ,
223+ update : { stripeSubscriptionId : 'sub_new' , status : 'incomplete' } ,
224+ } )
225+
226+ expect ( base . update ) . toHaveBeenCalledWith (
227+ expect . objectContaining ( {
228+ update : { stripeSubscriptionId : 'sub_new' , status : 'incomplete' } ,
229+ } )
230+ )
231+ } )
232+
147233 it ( 'rejects creating an org-referenced subscription with a non-org plan' , async ( ) => {
148234 const base = createBaseAdapter ( )
149235 dbChainMockFns . limit . mockResolvedValueOnce ( [ { id : 'org-1' } ] )
0 commit comments