import debug from "debug" ; const L = debug ( "hub:sync-subscriptions" ) ; import { PostgreSQL } from "../types" ; import { TIMEOUT_S } from "./const" ; import { delay } from "awaiting" ; const WAIT_AFTER_UPDATE_MS = 20 ; interface Subscription { id : string ; metadata : { license_id ? : string ; account_id ? : string ; } ; object : string ; status : string ; created : number ; customer : string ; cancel_at : number | null ; } interface RawSubscriptions { rows : { sub ? : { data : Subscription [ ] ; } ; } [ ] ; } type LicenseSubs = { [ license_id ] : Subscription [ ] ; } ; type LicenseInfo = { [ license_id ] : { expires : Date | undefined ; trial : boolean ; } ; } ; async function get_licenses ( db , account_id ? , expires_unset = false ) { const query = { select : [ "id" , "expires" , "info" ] , table : "site_licenses" , } as { select : string [ ] ; table : string ; where ? : string ; params ? : string [ ] ; } ; if ( account_id != null && expires_unset ) { throw new Error ( "setting the account_id requires expires_unset == false" ) ; } if ( account_id != null ) { query . where = "$1 = ANY(managers)" ; query . params = [ account_id ] ; } else if ( expires_unset ) { query . where = "expires IS NULL" ; } const results = await db . async_query ( query ) ; const licenses = { } ; for ( const x of results . rows ) { licenses [ x . id ] = { expires : x . expires , trial : x . info ?. trial === true } ; } return licenses ; } async function get_subs ( db , account_id ? ) { const subs = await db . async_query ( { select : "stripe_customer#>'{subscriptions}' as sub" , table : "accounts" , where : account_id == null ? "stripe_customer_id IS NOT NULL" : { account_id } , timeout_s : TIMEOUT_S , } ) ; const ret = { } ; for ( const x of subs . rows ) { if ( x . sub ?. data == null ) continue ; for ( const sub of x . sub . data ) { const license_id = sub . metadata . license_id ; if ( license_id == null ) { continue ; } if ( ret [ license_id ] == null ) { ret [ license_id ] = [ ] ; } else { L ( `more than one subscription for license '${ license_id }'` ) ; } ret [ license_id ] . push ( sub ) ; } } return ret ; } function * iter ( subs ) { for ( const license_id in subs ) { const sub_list = subs [ license_id ] ; for ( const sub of sub_list ) { yield { license_id , sub } ; } } } function is_funding ( sub ) { const cancelled = typeof sub . cancel_at === "number" ? new Date ( sub . cancel_at * 1000 ) < new Date ( ) : false ; return ( sub . status == "active" || sub . status == "trialing" ) && ! cancelled ; } async function sync_subscriptions_to_licenses ( db , licenses , subs , test_mode ) { let n = 0 ; for ( const { license_id , sub } of iter ( subs ) ) { if ( licenses [ license_id ] == null ) { L ( `WARNING: no known license '${ license_id }' for subscription '${ sub . id }'` ) ; } const expires = licenses [ license_id ] . expires ; if ( is_funding ( sub ) ) { if ( expires != null ) { if ( test_mode ) { L ( `DRYRUN: set 'expires = null' where license_id='${ license_id }'` ) ; } else { await db . async_query ( { query : "UPDATE site_licenses" , set : { expires : null } , where : { id : license_id } , } ) ; } await delay ( WAIT_AFTER_UPDATE_MS ) ; n += 1 ; } } else { if ( expires == null || expires > new Date ( ) ) { if ( test_mode ) { L ( `DRYRUN: set 'expires = ${ new Date ( ) . toISOString ( ) }' where license_id='${ license_id }'` ) ; } else { await db . async_query ( { query : "UPDATE site_licenses" , set : { expires : new Date ( ) } , where : { id : license_id } , } ) ; } await delay ( WAIT_AFTER_UPDATE_MS ) ; n += 1 ; } } } return n ; } async function expire_cancelled_subscriptions ( db , subs , test_mode ) { let n = 0 ; const licenses = await get_licenses ( db , undefined , true ) ; for ( const license_id in licenses ) { let funded = false ; if ( subs [ license_id ] != null ) { let i = 0 ; for ( const sub of subs [ license_id ] ) { if ( is_funding ( sub ) ) { funded = i ; break ; } i += 1 ; } } if ( typeof funded === "number" ) { L ( `license_id '${ license_id }' is funded by '${ subs [ license_id ] [ funded ] . id }'` ) ; } else { const msg = `license_id '${ license_id }' is not funded by any subscription` ; if ( licenses [ license_id ] . trial ) { L ( `${ msg }, but it is a trial` ) ; } else { L ( `${ msg }` ) ; if ( test_mode ) { L ( `DRYRUN: set 'expires = ${ new Date ( ) . toISOString ( ) }' where license_id='${ license_id }'` ) ; } else { await db . async_query ( { query : "UPDATE site_licenses" , set : { expires : new Date ( ) } , where : { id : license_id } , } ) ; } await delay ( WAIT_AFTER_UPDATE_MS ) ; n += 1 ; } } } return n ; } export async function sync_site_license_subscriptions ( db , account_id ? , test_mode = false ) { test_mode = test_mode || ! ! process . env . DRYRUN ; if ( test_mode ) L ( `DRYRUN TEST MODE -- UPDATE QUERIES ARE DISABLED` ) ; const licenses = await get_licenses ( db , account_id ) ; const subs = await get_subs ( db , account_id ) ; let n = await sync_subscriptions_to_licenses ( db , licenses , subs , test_mode ) ; if ( account_id == null ) { n += await expire_cancelled_subscriptions ( db , subs , test_mode ) ; } return n ; }