1
1
const moment = require ( 'moment' ) ;
2
2
3
+ // Import centralized date utilities
4
+ const { getDateBoundaries, applyDateFilter} = require ( './utils/date-utils' ) ;
5
+
3
6
// Source normalization mapping - consolidated from frontend apps
4
7
const SOURCE_NORMALIZATION_MAP = new Map ( [
5
8
// Social Media Consolidation
@@ -252,45 +255,44 @@ class ReferrersStatsService {
252
255
253
256
/**
254
257
* Fetch MRR sources with date range
255
- * @param {string } startDate
256
- * @param {string } endDate
258
+ * @param {Object } options
257
259
* @returns {Promise<MrrCountStatDate[]> }
258
260
**/
259
- async fetchMrrSourcesWithRange ( startDate , endDate ) {
261
+ async fetchMrrSourcesWithRange ( options ) {
260
262
const knex = this . knex ;
261
- const startDateTime = moment . utc ( startDate ) . startOf ( 'day' ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ;
262
- const endDateTime = moment . utc ( endDate ) . endOf ( 'day' ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ;
263
+ const { dateFrom : startDateTime , dateTo : endDateTime } = getDateBoundaries ( options ) ;
263
264
264
265
// Join subscription created events with paid subscription events to get MRR changes
265
- const rows = await knex ( 'members_subscription_created_events as msce' )
266
+ let query = knex ( 'members_subscription_created_events as msce' )
266
267
. join ( 'members_paid_subscription_events as mpse' , function ( ) {
267
268
this . on ( 'msce.member_id' , '=' , 'mpse.member_id' )
268
269
. andOn ( 'msce.subscription_id' , '=' , 'mpse.subscription_id' ) ;
269
270
} )
270
271
. select ( knex . raw ( `DATE(msce.created_at) as date` ) )
271
272
. select ( knex . raw ( `SUM(mpse.mrr_delta) as mrr` ) )
272
273
. select ( knex . raw ( `msce.referrer_source as source` ) )
273
- . where ( 'msce.created_at' , '>=' , startDateTime )
274
- . where ( 'msce.created_at' , '<=' , endDateTime )
275
274
. where ( 'mpse.mrr_delta' , '>' , 0 ) // Only positive MRR changes (new subscriptions)
276
275
. whereNotNull ( 'msce.referrer_source' ) // Only entries with attribution
277
276
. groupBy ( 'date' , 'msce.referrer_source' )
278
277
. orderBy ( 'date' ) ;
278
+
279
+ // Apply centralized date filtering
280
+ applyDateFilter ( query , startDateTime , endDateTime , 'msce.created_at' ) ;
281
+
282
+ const rows = await query ;
279
283
280
284
return rows ;
281
285
}
282
286
283
287
/**
284
288
* Fetch deduplicated member counts by source with date range
285
289
* Returns both free signups (excluding those who converted) and paid conversions
286
- * @param {string } startDate
287
- * @param {string } endDate
290
+ * @param {Object } options
288
291
* @returns {Promise<{source: string, signups: number, paid_conversions: number}[]> }
289
292
**/
290
- async fetchMemberCountsBySource ( startDate , endDate ) {
293
+ async fetchMemberCountsBySource ( options ) {
291
294
const knex = this . knex ;
292
- const startDateTime = moment . utc ( startDate ) . startOf ( 'day' ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ;
293
- const endDateTime = moment . utc ( endDate ) . endOf ( 'day' ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ;
295
+ const { dateFrom : startDateTime , dateTo : endDateTime } = getDateBoundaries ( options ) ;
294
296
295
297
// Query 1: Free members who haven't converted to paid within the same time window
296
298
const freeSignupsQuery = knex ( 'members_created_events as mce' )
@@ -302,18 +304,20 @@ class ReferrersStatsService {
302
304
. andOn ( 'msce.created_at' , '>=' , knex . raw ( '?' , [ startDateTime ] ) )
303
305
. andOn ( 'msce.created_at' , '<=' , knex . raw ( '?' , [ endDateTime ] ) ) ;
304
306
} )
305
- . where ( 'mce.created_at' , '>=' , startDateTime )
306
- . where ( 'mce.created_at' , '<=' , endDateTime )
307
307
. whereNull ( 'msce.id' )
308
308
. groupBy ( 'mce.referrer_source' ) ;
309
+
310
+ // Apply date filtering to the main query
311
+ applyDateFilter ( freeSignupsQuery , startDateTime , endDateTime , 'mce.created_at' ) ;
309
312
310
313
// Query 2: Paid conversions
311
314
const paidConversionsQuery = knex ( 'members_subscription_created_events as msce' )
312
315
. select ( 'msce.referrer_source as source' )
313
316
. select ( knex . raw ( 'COUNT(DISTINCT msce.member_id) as paid_conversions' ) )
314
- . where ( 'msce.created_at' , '>=' , startDateTime )
315
- . where ( 'msce.created_at' , '<=' , endDateTime )
316
317
. groupBy ( 'msce.referrer_source' ) ;
318
+
319
+ // Apply date filtering to the paid conversions query
320
+ applyDateFilter ( paidConversionsQuery , startDateTime , endDateTime , 'msce.created_at' ) ;
317
321
318
322
// Execute both queries in parallel
319
323
const [ freeResults , paidResults ] = await Promise . all ( [
@@ -353,17 +357,21 @@ class ReferrersStatsService {
353
357
/**
354
358
* Return aggregated attribution sources for a date range, grouped by source only (not by date)
355
359
* This is used for "Top Sources" tables that need server-side sorting
356
- * @param {string } startDate - Start date in YYYY-MM-DD format
357
- * @param {string } endDate - End date in YYYY-MM-DD format
358
- * @param {string } [orderBy='signups desc'] - Sort order: 'signups desc', 'paid_conversions desc', 'mrr desc', 'source desc'
359
- * @param {number } [limit=50] - Maximum number of sources to return
360
+ * @param {Object } options
361
+ * @param {string } [options.date_from] - Start date in YYYY-MM-DD format
362
+ * @param {string } [options.date_to] - End date in YYYY-MM-DD format
363
+ * @param {string } [options.timezone] - Timezone to use for date interpretation
364
+ * @param {string } [options.orderBy='signups desc'] - Sort order: 'signups desc', 'paid_conversions desc', 'mrr desc', 'source desc'
365
+ * @param {number } [options.limit=50] - Maximum number of sources to return
360
366
* @returns {Promise<{data: AttributionCountStatWithMrr[], meta: {}}> }
361
367
*/
362
- async getTopSourcesWithRange ( startDate , endDate , orderBy = 'signups desc' , limit = 50 ) {
368
+ async getTopSourcesWithRange ( options = { } ) {
369
+ const { orderBy = 'signups desc' , limit = 50 } = options ;
370
+
363
371
// Get deduplicated member counts and MRR data in parallel
364
372
const [ memberCounts , mrrEntries ] = await Promise . all ( [
365
- this . fetchMemberCountsBySource ( startDate , endDate ) ,
366
- this . fetchMrrSourcesWithRange ( startDate , endDate )
373
+ this . fetchMemberCountsBySource ( options ) ,
374
+ this . fetchMrrSourcesWithRange ( options )
367
375
] ) ;
368
376
369
377
// Aggregate by source (not by date + source)
0 commit comments