@@ -15,6 +15,14 @@ import {
1515 CreditPurchaseInput ,
1616 CreditTransferInput ,
1717} from '../types/credit' ;
18+ import {
19+ DEFAULT_PAUSE_LIMITS ,
20+ PauseLimits ,
21+ PauseReason ,
22+ PauseState ,
23+ PauseValidationResult ,
24+ type PauseRecord ,
25+ } from '../types/pause' ;
1826import { InvoiceStatus , isOpenInvoice } from '../types/invoice' ;
1927import { dummySubscriptions } from '../utils/dummyData' ; // eslint-disable-line
2028import { advanceBillingDate } from '../utils/billingDate' ;
@@ -56,7 +64,16 @@ import {
5664 applyCreditMemo ,
5765 ProrationPreview ,
5866 CreditMemo ,
67+ getPeriodDays ,
5968} from '../utils/proration' ;
69+ import {
70+ calculateEarlyResumeCredit ,
71+ calculatePauseCredit ,
72+ resumePause ,
73+ validatePauseRequest ,
74+ } from './pauseStore' ;
75+
76+ export type { PauseRecord } from '../types/pause' ;
6077
6178const STORAGE_KEY = 'subtrackr-subscriptions' ;
6279const STORE_VERSION = 2 ;
@@ -72,7 +89,10 @@ const generateUniqueId = (): string => {
7289 return `${ timestamp } -${ randomComponent } ` ;
7390} ;
7491
75- type PersistedSubscriptionSlice = Pick < SubscriptionState , 'subscriptions' | 'creditAccounts' > ;
92+ type PersistedSubscriptionSlice = Pick <
93+ SubscriptionState ,
94+ 'subscriptions' | 'creditAccounts' | 'pauseHistory'
95+ > ;
7696
7797const toValidDate = ( value : unknown , fallback = new Date ( ) ) : Date => {
7898 if ( value instanceof Date && ! Number . isNaN ( value . getTime ( ) ) ) return value ;
@@ -246,14 +266,22 @@ const serializeForStorage = (state: PersistedSubscriptionSlice): PersistedSubscr
246266 } ,
247267 ] )
248268 ) as Record < string , CreditAccountState > ,
269+ pauseHistory : ( state . pauseHistory ?? [ ] ) . map ( ( record ) => ( {
270+ ...record ,
271+ pausedAt : new Date ( record . pausedAt ) ,
272+ scheduledResumeAt : new Date ( record . scheduledResumeAt ) ,
273+ resumedAt : record . resumedAt ? new Date ( record . resumedAt ) : undefined ,
274+ plannedResumeDate : record . plannedResumeDate ? new Date ( record . plannedResumeDate ) : undefined ,
275+ resumeAt : record . resumeAt ? new Date ( record . resumeAt ) : undefined ,
276+ } ) ) ,
249277} ) ;
250278
251279const migratePersistedState = (
252280 persisted : unknown ,
253281 _version : number
254282) : PersistedSubscriptionSlice => {
255283 if ( ! persisted || typeof persisted !== 'object' ) {
256- return { subscriptions : [ ] , creditAccounts : { } } ;
284+ return { subscriptions : [ ] , creditAccounts : { } , pauseHistory : [ ] } ;
257285 }
258286
259287 const maybeState = persisted as Partial < PersistedSubscriptionSlice > ;
@@ -272,8 +300,20 @@ const migratePersistedState = (
272300 return acc ;
273301 } , { } )
274302 : { } ;
303+ const pauseHistory = Array . isArray ( maybeState . pauseHistory )
304+ ? maybeState . pauseHistory . map ( ( entry ) => ( {
305+ ...entry ,
306+ pausedAt : toValidDate ( entry . pausedAt , new Date ( ) ) ,
307+ scheduledResumeAt : toValidDate ( entry . scheduledResumeAt , new Date ( ) ) ,
308+ resumedAt : entry . resumedAt ? toValidDate ( entry . resumedAt , new Date ( ) ) : undefined ,
309+ plannedResumeDate : entry . plannedResumeDate
310+ ? toValidDate ( entry . plannedResumeDate , new Date ( ) )
311+ : undefined ,
312+ resumeAt : entry . resumeAt ? toValidDate ( entry . resumeAt , new Date ( ) ) : undefined ,
313+ } ) )
314+ : [ ] ;
275315
276- return { subscriptions, creditAccounts } ;
316+ return { subscriptions, creditAccounts, pauseHistory } ;
277317} ;
278318
279319const pendingWrites = new Map < string , string > ( ) ;
@@ -324,13 +364,24 @@ const debouncedAsyncStorage: StateStorage = {
324364interface SubscriptionState {
325365 subscriptions : Subscription [ ] ;
326366 creditAccounts : Record < string , CreditAccountState > ;
367+ pauseHistory : PauseRecord [ ] ;
327368 stats : SubscriptionStats ;
328369 isLoading : boolean ;
329370 error : AppError | null ;
330371 prorationPreview : ProrationPreview | null ;
331372 creditMemos : Record < string , CreditMemo > ;
332373
333374 // Actions
375+ pauseSubscription : (
376+ subscriptionOrId : string | Subscription ,
377+ pauseDays : number ,
378+ reason ?: PauseReason | string ,
379+ limits ?: PauseLimits ,
380+ note ?: string
381+ ) => PauseRecord ;
382+ resumeSubscription : ( id : string , early ?: boolean ) => PauseRecord | null ;
383+ getPauseHistory : ( subscriptionId ?: string ) => PauseRecord [ ] ;
384+ getActivePause : ( subscriptionId : string ) => PauseRecord | undefined ;
334385 addSubscription : ( data : SubscriptionFormData ) => Promise < void > ;
335386 updateSubscription : ( id : string , data : Partial < Subscription > ) => Promise < void > ;
336387 deleteSubscription : ( id : string ) => Promise < void > ;
@@ -372,6 +423,7 @@ export const useSubscriptionStore = create<SubscriptionState>()(
372423 ( set , get ) => ( {
373424 subscriptions : dummySubscriptions ,
374425 creditAccounts : { } ,
426+ pauseHistory : [ ] ,
375427 stats : {
376428 totalActive : 0 ,
377429 totalMonthlySpend : 0 ,
@@ -383,6 +435,121 @@ export const useSubscriptionStore = create<SubscriptionState>()(
383435 prorationPreview : null ,
384436 creditMemos : { } ,
385437
438+ pauseSubscription : (
439+ subscriptionOrId ,
440+ pauseDays ,
441+ reason = PauseReason . OTHER ,
442+ limits = DEFAULT_PAUSE_LIMITS ,
443+ note
444+ ) => {
445+ const subscription =
446+ typeof subscriptionOrId === 'string'
447+ ? get ( ) . subscriptions . find ( ( sub ) => sub . id === subscriptionOrId )
448+ : subscriptionOrId ;
449+ if ( ! subscription ) throw new Error ( 'Subscription not found' ) ;
450+
451+ const validation = validatePauseRequest (
452+ subscription . id ,
453+ pauseDays ,
454+ get ( ) . pauseHistory ,
455+ limits
456+ ) as PauseValidationResult ;
457+ if ( ! validation . valid ) {
458+ throw new Error ( validation . reason ?? 'Pause request validation failed.' ) ;
459+ }
460+
461+ const creditAmount = calculatePauseCredit ( subscription , pauseDays ) ;
462+ const now = new Date ( ) ;
463+ const scheduledResumeAt = new Date ( now . getTime ( ) + pauseDays * 24 * 60 * 60 * 1000 ) ;
464+ const record : PauseRecord = {
465+ id : generateUniqueId ( ) ,
466+ subscriptionId : subscription . id ,
467+ state : PauseState . PAUSED ,
468+ reason : reason ?? PauseReason . OTHER ,
469+ note,
470+ pausedAt : now ,
471+ scheduledResumeAt,
472+ creditAmount,
473+ currency : subscription . currency ,
474+ creditRemaining : creditAmount ,
475+ creditExpired : false ,
476+ creditExpiryDays : 90 ,
477+ billingAdjustment : creditAmount ,
478+ plannedResumeDate : scheduledResumeAt ,
479+ status : 'active' ,
480+ } ;
481+
482+ set ( ( state ) => ( {
483+ pauseHistory : [ ...state . pauseHistory , record ] ,
484+ subscriptions : state . subscriptions . map ( ( sub ) =>
485+ sub . id === subscription . id ? { ...sub , isActive : false , updatedAt : now } : sub
486+ ) ,
487+ } ) ) ;
488+ get ( ) . calculateStats ( ) ;
489+ return record ;
490+ } ,
491+
492+ resumeSubscription : ( id , early = false ) => {
493+ const activePause = get ( ) . pauseHistory . find (
494+ ( record ) =>
495+ record . subscriptionId === id &&
496+ ( record . state === PauseState . PAUSED || record . status === 'active' )
497+ ) ;
498+ if ( ! activePause ) return null ;
499+
500+ const resumed = resumePause ( activePause , early ) ;
501+ const now = new Date ( ) ;
502+ const nextDays = Math . max (
503+ 1 ,
504+ Math . ceil (
505+ ( new Date ( activePause . scheduledResumeAt ) . getTime ( ) - new Date ( activePause . pausedAt ) . getTime ( ) ) /
506+ ( 1000 * 60 * 60 * 24 )
507+ )
508+ ) ;
509+ const updatedRecord : PauseRecord = {
510+ ...resumed ,
511+ resumedAt : now ,
512+ billingAdjustment : resumed . creditRemaining ,
513+ plannedResumeDate : activePause . scheduledResumeAt ,
514+ resumeAt : now ,
515+ status : 'resumed' ,
516+ reason : activePause . reason ,
517+ } ;
518+
519+ set ( ( state ) => ( {
520+ pauseHistory : state . pauseHistory . map ( ( record ) =>
521+ record . id === activePause . id ? updatedRecord : record
522+ ) ,
523+ subscriptions : state . subscriptions . map ( ( sub ) => {
524+ if ( sub . id !== id ) return sub ;
525+ const shiftedNextBillingDate = new Date (
526+ sub . nextBillingDate . getTime ( ) + nextDays * 24 * 60 * 60 * 1000
527+ ) ;
528+ return {
529+ ...sub ,
530+ isActive : true ,
531+ nextBillingDate : shiftedNextBillingDate ,
532+ updatedAt : now ,
533+ } ;
534+ } ) ,
535+ } ) ) ;
536+ get ( ) . calculateStats ( ) ;
537+ return updatedRecord ;
538+ } ,
539+
540+ getPauseHistory : ( subscriptionId ) => {
541+ const history = get ( ) . pauseHistory ;
542+ if ( ! subscriptionId ) return history ;
543+ return history . filter ( ( record ) => record . subscriptionId === subscriptionId ) ;
544+ } ,
545+
546+ getActivePause : ( subscriptionId ) =>
547+ get ( ) . pauseHistory . find (
548+ ( record ) =>
549+ record . subscriptionId === subscriptionId &&
550+ ( record . state === PauseState . PAUSED || record . status === 'active' )
551+ ) ,
552+
386553 previewPlanChange : (
387554 id : string ,
388555 newPrice : number ,
@@ -994,6 +1161,7 @@ export const useSubscriptionStore = create<SubscriptionState>()(
9941161 serializeForStorage ( {
9951162 subscriptions : state . subscriptions ,
9961163 creditAccounts : state . creditAccounts ,
1164+ pauseHistory : state . pauseHistory ,
9971165 } ) ,
9981166 migrate : ( persistedState , version ) => migratePersistedState ( persistedState , version ) ,
9991167 merge : ( persistedState , currentState ) => ( {
@@ -1024,9 +1192,13 @@ export const useSubscriptionStore = create<SubscriptionState>()(
10241192 state ?. creditAccounts && typeof state . creditAccounts === 'object'
10251193 ? state . creditAccounts
10261194 : { } ;
1195+ const pauseHistory = Array . isArray ( ( state as { pauseHistory ?: PauseRecord [ ] } | undefined ) ?. pauseHistory )
1196+ ? ( state as { pauseHistory ?: PauseRecord [ ] } ) . pauseHistory ?? [ ]
1197+ : [ ] ;
10271198 useSubscriptionStore . setState ( {
10281199 subscriptions,
10291200 creditAccounts,
1201+ pauseHistory,
10301202 isLoading : false ,
10311203 error : null ,
10321204 } ) ;
0 commit comments