@liiift-studio/sales-portal 1.3.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,66 +1,29 @@
1
1
  // API endpoint to fetch Stripe balance transactions for reconciliation
2
- const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, {
3
- maxNetworkRetries: 3,
4
- });
2
+ import { stripe } from './utils/clients';
3
+ import { createDateRange, createStripeTimeRange } from './utils/dateUtils';
4
+ import { sendError, requirePost } from './utils/apiResponse';
5
+
6
+ export const config = { maxDuration: 300 };
5
7
 
6
- /**
7
- * Fetches Stripe balance transactions for a specific month
8
- * @param {Object} req - HTTP request object
9
- * @param {Object} res - HTTP response object
10
- */
11
8
  export default async function handler(req, res) {
12
- // Only allow POST requests
13
- if (req.method !== 'POST') {
14
- return res.status(405).json({ success: false, message: 'Method not allowed' });
15
- }
9
+ if (!requirePost(req, res)) return;
16
10
 
17
11
  try {
18
- // Get date or dateRange from request body
19
12
  const { date, dateRange, admin } = req.body;
20
-
21
- let timeRange;
22
-
23
- if (dateRange) {
24
- // If dateRange is provided, use it directly
25
- if (!dateRange.start || !dateRange.end) {
26
- return res.status(400).json({ success: false, message: 'dateRange must include start and end timestamps' });
27
- }
28
-
29
- // Convert millisecond timestamps to seconds for Stripe API
30
- timeRange = {
31
- gte: Math.floor(dateRange.start / 1000),
32
- lte: Math.floor(dateRange.end / 1000)
33
- };
34
-
35
- console.log('Using date range:', {
36
- start: new Date(dateRange.start).toISOString(),
37
- end: new Date(dateRange.end).toISOString()
38
- });
39
- } else if (date) {
40
- // If only date is provided, create a month range
41
- const targetDate = new Date(date);
42
- const startOfMonth = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), 1, 0, 0, 0));
43
- const endOfMonth = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth() + 1, 0, 23, 59, 59));
44
-
45
- timeRange = {
46
- gte: Math.floor(startOfMonth.getTime() / 1000),
47
- lte: Math.floor(endOfMonth.getTime() / 1000)
48
- };
49
-
50
- console.log('Using month range:', {
51
- start: startOfMonth.toISOString(),
52
- end: endOfMonth.toISOString()
53
- });
54
- } else {
55
- return res.status(400).json({ success: false, message: 'Either date or dateRange is required' });
13
+
14
+ if (!date && !dateRange) {
15
+ return sendError(res, 400, 'Either date or dateRange is required');
56
16
  }
57
17
 
58
- // Fetch all balance transactions for the month
18
+ // Reuse the same date range logic as getSales for consistent boundaries
19
+ const { startDate, endDate } = createDateRange({ date, dateRange });
20
+ const timeRange = createStripeTimeRange(startDate, endDate);
21
+
22
+ // Fetch all balance transactions for the period
59
23
  const balanceTransactions = await fetchAllBalanceTransactions(timeRange);
60
-
61
- // Calculate total balance change for the month (ignoring payouts and pending transactions)
24
+
25
+ // Calculate total balance change (ignoring payouts and pending transactions)
62
26
  const totalBalanceChange = balanceTransactions.reduce((total, transaction) => {
63
- // Skip payout, stripe_fee and pending transactions
64
27
  if (transaction.type === 'payout' || transaction.type === 'stripe_fee' || transaction.status === 'pending') {
65
28
  return total;
66
29
  }
@@ -75,53 +38,36 @@ export default async function handler(req, res) {
75
38
  }
76
39
  });
77
40
  } catch (error) {
78
- console.error('Error fetching balance transactions:', error);
79
- return res.status(500).json({
80
- success: false,
81
- message: 'Failed to fetch balance transactions',
82
- error: error.message
83
- });
41
+ return sendError(res, 500, 'Failed to fetch balance transactions', error);
84
42
  }
85
43
  }
86
44
 
87
45
  /**
88
46
  * Fetches all pages of Stripe balance transactions for a given date range
89
- * @param {Object} timeRange - Stripe-compatible date range
47
+ * @param {Object} timeRange - Stripe-compatible date range (Unix seconds)
90
48
  * @returns {Promise<Array>} Array of all fetched balance transactions
91
49
  */
92
50
  async function fetchAllBalanceTransactions(timeRange) {
93
- const fetchOptions = {
94
- created: timeRange,
95
- limit: 100,
96
- };
97
-
98
51
  let allTransactions = [];
99
52
  let hasMore = true;
100
53
  let lastId = null;
101
54
  let pageCount = 0;
102
55
 
103
56
  while (hasMore) {
104
- try {
105
- const fetchParams = { ...fetchOptions };
106
- if (lastId) {
107
- fetchParams.starting_after = lastId;
108
- }
57
+ const fetchParams = { created: timeRange, limit: 100 };
58
+ if (lastId) fetchParams.starting_after = lastId;
109
59
 
110
- console.log(`Fetching balance transaction page ${pageCount + 1}...`);
111
- const response = await stripe.balanceTransactions.list(fetchParams);
60
+ const response = await stripe.balanceTransactions.list(fetchParams);
112
61
 
113
- if (response.data.length > 0) {
114
- allTransactions = allTransactions.concat(response.data);
115
- lastId = response.data[response.data.length - 1].id;
116
- hasMore = response.has_more;
117
- pageCount++;
118
- } else {
119
- hasMore = false;
120
- }
121
- } catch (error) {
122
- console.error('Error fetching balance transaction page:', error);
123
- throw new Error(`Failed to fetch balance transaction page ${pageCount + 1}: ${error.message}`);
62
+ if (response.data.length > 0) {
63
+ allTransactions = allTransactions.concat(response.data);
64
+ lastId = response.data[response.data.length - 1].id;
65
+ hasMore = response.has_more;
66
+ pageCount++;
67
+ } else {
68
+ hasMore = false;
124
69
  }
125
70
  }
71
+
126
72
  return allTransactions;
127
73
  }
@@ -1,36 +1,10 @@
1
1
  // API endpoint for retrieving designer information and authentication
2
+ import { sanityClient } from './utils/clients';
3
+ import { sendError, requirePost } from './utils/apiResponse';
2
4
 
3
- // Configure extended timeout for Vercel serverless function
4
5
  export const config = { maxDuration: 300 };
5
6
 
6
- // Import required dependencies
7
- const { createClient } = require('@sanity/client');
8
- const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, {
9
- maxNetworkRetries: 3,
10
- });
11
-
12
- /**
13
- * Sanity client configuration for CMS interactions
14
- * Uses environment variables for secure configuration
15
- */
16
- const client = createClient({
17
- projectId: process.env.SANITY_STUDIO_PROJECT_ID,
18
- dataset: process.env.SANITY_STUDIO_DATASET,
19
- apiVersion: process.env.SANITY_STUDIO_API_VERSION || '2022-04-01',
20
- token: process.env.SANITY_STUDIO_TOKEN,
21
- useCdn: false, // Ensures fresh data by bypassing CDN
22
- });
23
-
24
- /**
25
- * API route handler for designer information
26
- * Handles authentication and retrieval of designer data
27
- * @param {Object} req - HTTP request object containing user credentials
28
- * @param {Object} res - HTTP response object
29
- */
30
7
  export default async function handler(req, res) {
31
- const { method } = req;
32
-
33
- // Check if sales portal is enabled
34
8
  if (process.env.SALES_PORTAL_ENABLED === 'false') {
35
9
  return res.status(503).json({
36
10
  success: false,
@@ -39,63 +13,59 @@ export default async function handler(req, res) {
39
13
  });
40
14
  }
41
15
 
42
- // Only handle POST requests
43
- if (method === 'POST') {
44
- const { user, password } = req.body;
16
+ if (!requirePost(req, res)) return;
17
+
18
+ const { user, password } = req.body;
45
19
 
46
- // Authenticate designer using Sanity
47
- const designer = await client.fetch(
20
+ if (!user || !password) {
21
+ return sendError(res, 400, 'Email and password are required');
22
+ }
23
+
24
+ try {
25
+ const designer = await sanityClient.fetch(
48
26
  `*[_type == "account" && email == $user && password == $password && isDesigner][0]`,
49
27
  { user, password }
50
28
  );
51
29
 
52
- if (designer) {
53
- // Handle admin user case
54
- if (designer.isAdmin) {
55
- // Fetch all designers for admin view
56
- const designers = await client.fetch(`*[_type == "account" && isDesigner]`);
57
-
58
- if (designers) {
59
- res.status(200).json({
60
- success: true,
61
- admin: true,
62
- data: designers.map(designer => ({
63
- _id: designer._id,
64
- firstName: designer?.firstName || (designer?.name || 'Nameless'),
65
- lastName: designer?.lastName || '',
66
- user: designer.email,
67
- password: designer.password,
68
- })).sort((a, b) =>
69
- a?.firstName
70
- ? a.firstName.localeCompare(b.firstName)
71
- : a.name.localeCompare(b.name)
72
- ),
73
- });
74
- } else {
75
- res.status(200).json({
76
- success: false,
77
- message: 'No designers... thats weird.'
78
- });
79
- }
80
- } else {
81
- // Handle regular designer case
82
- res.status(200).json({
83
- success: true,
84
- data: [{
85
- _id: designer._id,
86
- firstName: designer?.firstName || (designer?.name || 'Nameless'),
87
- lastName: designer?.lastName || '',
88
- user: designer.email,
89
- password: designer.password
90
- }],
91
- });
30
+ if (!designer) {
31
+ return sendError(res, 401, 'Incorrect email or password.');
32
+ }
33
+
34
+ // Admin user return all designers
35
+ if (designer.isAdmin) {
36
+ const designers = await sanityClient.fetch(`*[_type == "account" && isDesigner]`);
37
+
38
+ if (!designers) {
39
+ return sendError(res, 500, 'Failed to fetch designer accounts');
92
40
  }
93
- } else {
94
- // Handle authentication failure
95
- res.status(200).json({
96
- success: false,
97
- message: 'Incorrect email or password.'
41
+
42
+ return res.status(200).json({
43
+ success: true,
44
+ admin: true,
45
+ data: designers.map(d => ({
46
+ _id: d._id,
47
+ firstName: d?.firstName || (d?.name || 'Nameless'),
48
+ lastName: d?.lastName || '',
49
+ user: d.email,
50
+ password: d.password,
51
+ })).sort((a, b) =>
52
+ (a?.firstName || '').localeCompare(b?.firstName || '')
53
+ ),
98
54
  });
99
55
  }
56
+
57
+ // Regular designer
58
+ return res.status(200).json({
59
+ success: true,
60
+ data: [{
61
+ _id: designer._id,
62
+ firstName: designer?.firstName || (designer?.name || 'Nameless'),
63
+ lastName: designer?.lastName || '',
64
+ user: designer.email,
65
+ password: designer.password
66
+ }],
67
+ });
68
+ } catch (error) {
69
+ return sendError(res, 500, 'Authentication service error', error);
100
70
  }
101
71
  }
@@ -1,63 +1,31 @@
1
1
  // API endpoint to fetch designer accounts from Sanity CMS
2
+ import { sanityCdnClient } from './utils/clients';
3
+ import { sendSuccess, sendError } from './utils/apiResponse';
2
4
 
3
- const { createClient } = require('@sanity/client');
4
-
5
- /**
6
- * Configure Next.js API route to allow for longer execution time
7
- * @constant {Object} config
8
- */
9
5
  export const config = { maxDuration: 300 };
10
6
 
11
- /**
12
- * Sanity client configuration
13
- * @constant {Object} client
14
- */
15
- const client = createClient({
16
- projectId: process.env.SANITY_STUDIO_PROJECT_ID,
17
- dataset: process.env.SANITY_STUDIO_DATASET,
18
- apiVersion: '2022-04-01',
19
- token: process.env.SANITY_STUDIO_TOKEN,
20
- useCdn: true,
21
- });
22
-
23
- /**
24
- * GET handler to fetch designer accounts
25
- * @param {Object} req - Next.js API request object
26
- * @param {Object} res - Next.js API response object
27
- * @returns {Promise<void>}
28
- * @response {Object} success - Indicates if the request was successful
29
- * @response {boolean} admin - Always true for this endpoint
30
- * @response {Array<Object>} data - Array of designer objects with firstName, lastName, and _id
31
- */
32
7
  export default async function handler(req, res) {
33
- const { method } = req;
8
+ if (req.method !== 'GET') {
9
+ return sendError(res, 405, 'Method not allowed');
10
+ }
34
11
 
35
- if (method === 'GET') {
36
- const designers = await client.fetch(`*[_type == "account" && isDesigner]`);
37
-
38
- if (designers) {
39
- let designerData = designers
40
- .map(designer => ({
41
- firstName: designer?.firstName || (designer?.name || 'Nameless'),
42
- lastName: designer?.lastName || '',
43
- _id: designer?._id,
44
- }))
45
- .sort((a, b) =>
46
- a?.firstName
47
- ? a.firstName.localeCompare(b.firstName)
48
- : a.name.localeCompare(b.name)
49
- );
12
+ try {
13
+ const designers = await sanityCdnClient.fetch(`*[_type == "account" && isDesigner]`);
50
14
 
51
- res.status(200).json({
52
- success: true,
53
- admin: true,
54
- data: designerData,
55
- });
56
- } else {
57
- res.status(200).json({
58
- success: false,
59
- message: 'No designers... thats weird.'
60
- });
15
+ if (!designers) {
16
+ return sendError(res, 500, 'No designers found');
61
17
  }
18
+
19
+ return sendSuccess(res,
20
+ designers.map(designer => ({
21
+ firstName: designer?.firstName || (designer?.name || 'Nameless'),
22
+ lastName: designer?.lastName || '',
23
+ _id: designer?._id,
24
+ })).sort((a, b) =>
25
+ (a?.firstName || '').localeCompare(b?.firstName || '')
26
+ )
27
+ );
28
+ } catch (error) {
29
+ return sendError(res, 500, 'Failed to fetch designers', error);
62
30
  }
63
31
  }
package/api/getSales.js CHANGED
@@ -1,50 +1,27 @@
1
- /**
2
- * API endpoint for retrieving sales data from Sanity and Stripe for authenticated designers
3
- * Combines order data from both platforms and handles refunds, shipping, and tax calculations
4
- */
5
-
1
+ // API endpoint for retrieving sales data from Sanity and Stripe
6
2
  import { authenticateDesigner, processSalesData } from './utils/salesDataProcessor';
3
+ import { sendError, requirePost } from './utils/apiResponse';
7
4
 
8
- // API config for extended processing time
9
5
  export const config = { maxDuration: 300 };
10
6
 
11
- /**
12
- * API route handler for sales data retrieval
13
- * @param {Object} req - Next.js API request object
14
- * @param {Object} res - Next.js API response object
15
- */
16
7
  export default async function handler(req, res) {
17
- if (req.method !== 'POST') {
18
- return res.status(405).json({ success: false, message: 'Method not allowed' });
19
- }
8
+ if (!requirePost(req, res)) return;
20
9
 
21
10
  try {
22
- // Extract request parameters
23
11
  const { user, password, date, dateRange, admin } = req.body;
24
12
 
25
- // Authenticate designer
26
13
  const designer = await authenticateDesigner(user, password);
27
14
  if (!designer) {
28
- return res.status(200).json({
29
- success: false,
30
- message: 'Looks like there was an issue finding the account.'
31
- });
15
+ return sendError(res, 401, 'Looks like there was an issue finding the account.');
32
16
  }
33
17
 
34
- // Process sales data
35
18
  const sales = await processSalesData({ date, dateRange, designer, admin });
36
19
 
37
- // Return processed data
38
20
  res.status(200).json({
39
21
  success: true,
40
22
  data: sales,
41
23
  });
42
-
43
24
  } catch (error) {
44
- console.error('Error in sales data API:', error);
45
- res.status(500).json({
46
- success: false,
47
- message: 'An error occurred while processing sales data.'
48
- });
25
+ return sendError(res, 500, 'An error occurred while processing sales data.', error);
49
26
  }
50
27
  }
@@ -0,0 +1,41 @@
1
+ // Standardized API response helpers for consistent error reporting
2
+
3
+ /**
4
+ * Send a success response
5
+ * @param {Object} res - Next.js response object
6
+ * @param {*} data - Response data
7
+ * @param {Object} [meta] - Optional metadata
8
+ */
9
+ export function sendSuccess(res, data, meta = null) {
10
+ const body = { success: true, data };
11
+ if (meta) body.metadata = meta;
12
+ return res.status(200).json(body);
13
+ }
14
+
15
+ /**
16
+ * Send an error response with consistent formatting
17
+ * @param {Object} res - Next.js response object
18
+ * @param {number} status - HTTP status code
19
+ * @param {string} message - User-facing error message
20
+ * @param {Error} [error] - Original error for server-side logging
21
+ */
22
+ export function sendError(res, status, message, error = null) {
23
+ if (error) {
24
+ console.error(`[${status}] ${message}:`, error.message || error);
25
+ }
26
+ return res.status(status).json({ success: false, message });
27
+ }
28
+
29
+ /**
30
+ * Require POST method or send 405
31
+ * @param {Object} req - Next.js request object
32
+ * @param {Object} res - Next.js response object
33
+ * @returns {boolean} true if method is POST
34
+ */
35
+ export function requirePost(req, res) {
36
+ if (req.method !== 'POST') {
37
+ sendError(res, 405, 'Method not allowed');
38
+ return false;
39
+ }
40
+ return true;
41
+ }
@@ -1,27 +1,12 @@
1
- /**
2
- * Authentication middleware for sales portal API endpoints
3
- * Verifies user credentials against Sanity database and returns designer information
4
- */
5
-
6
- import { createClient } from '@sanity/client';
7
-
8
- // Initialize Sanity client
9
- const client = createClient({
10
- projectId: process.env.SANITY_STUDIO_PROJECT_ID,
11
- dataset: process.env.SANITY_STUDIO_DATASET,
12
- apiVersion: '2022-04-01',
13
- token: process.env.SANITY_STUDIO_TOKEN,
14
- useCdn: true,
15
- });
1
+ // Authentication middleware for sales portal API endpoints
2
+ import { sanityClient } from './clients';
16
3
 
17
4
  /**
18
5
  * Middleware to authenticate API requests
19
- * Validates credentials and returns designer data
20
6
  * @param {Object} req - HTTP request object with user credentials
21
7
  * @returns {Object} Authentication result with authorized flag, designer data, and error message
22
8
  */
23
9
  export async function authMiddleware(req) {
24
- // Check if sales portal is enabled via environment variable
25
10
  if (process.env.SALES_PORTAL_ENABLED === 'false') {
26
11
  return {
27
12
  authorized: false,
@@ -30,10 +15,8 @@ export async function authMiddleware(req) {
30
15
  };
31
16
  }
32
17
 
33
- // Extract credentials from request body
34
18
  const { user, password, admin = false } = req.body;
35
19
 
36
- // Check if credentials are provided
37
20
  if (!user || !password) {
38
21
  return {
39
22
  authorized: false,
@@ -43,13 +26,11 @@ export async function authMiddleware(req) {
43
26
  }
44
27
 
45
28
  try {
46
- // Query Sanity for designer account
47
- const designer = await client.fetch(
29
+ const designer = await sanityClient.fetch(
48
30
  `*[_type == "account" && email == $email && password == $password && isDesigner][0]`,
49
31
  { email: user, password }
50
32
  );
51
33
 
52
- // Verify designer account exists
53
34
  if (!designer) {
54
35
  return {
55
36
  authorized: false,
@@ -58,7 +39,6 @@ export async function authMiddleware(req) {
58
39
  };
59
40
  }
60
41
 
61
- // Verify admin access if requested
62
42
  if (admin && !designer.isAdmin) {
63
43
  return {
64
44
  authorized: false,
@@ -67,7 +47,6 @@ export async function authMiddleware(req) {
67
47
  };
68
48
  }
69
49
 
70
- // Authentication successful
71
50
  return {
72
51
  authorized: true,
73
52
  designer,
@@ -0,0 +1,44 @@
1
+ // Shared client instances for Sanity CMS and Stripe API
2
+ import { createClient } from '@sanity/client';
3
+
4
+ const API_VERSION = '2022-04-01';
5
+
6
+ // Sanity client for fresh data (auth, writes)
7
+ export const sanityClient = createClient({
8
+ projectId: process.env.SANITY_STUDIO_PROJECT_ID,
9
+ dataset: process.env.SANITY_STUDIO_DATASET,
10
+ apiVersion: process.env.SANITY_STUDIO_API_VERSION || API_VERSION,
11
+ token: process.env.SANITY_STUDIO_TOKEN,
12
+ useCdn: false,
13
+ });
14
+
15
+ // Sanity client with CDN for read-heavy queries
16
+ export const sanityCdnClient = createClient({
17
+ projectId: process.env.SANITY_STUDIO_PROJECT_ID,
18
+ dataset: process.env.SANITY_STUDIO_DATASET,
19
+ apiVersion: API_VERSION,
20
+ token: process.env.SANITY_STUDIO_TOKEN,
21
+ useCdn: true,
22
+ });
23
+
24
+ // Stripe client with automatic retries for rate limiting
25
+ export const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, {
26
+ maxNetworkRetries: 3,
27
+ });
28
+
29
+ // Test accounts excluded from production sales data
30
+ export const TEST_ACCOUNTS = [
31
+ "colby@liiift.studio",
32
+ "quinn@liiift.studio",
33
+ "qkeave@gmail.com",
34
+ "quinn@quitetype.com"
35
+ ];
36
+
37
+ /**
38
+ * Checks if a sale involves a test account
39
+ * @param {Object} params - Objects to check for test email addresses
40
+ * @returns {boolean} Whether any email matches a test account
41
+ */
42
+ export function isTestSale(...emailSources) {
43
+ return emailSources.some(email => email && TEST_ACCOUNTS.includes(email));
44
+ }
@@ -1,42 +1,33 @@
1
1
  /**
2
2
  * Utility functions for handling dates and time operations in sales data processing
3
+ * All date operations use UTC to ensure consistent behavior regardless of server timezone
3
4
  */
4
5
 
5
- /**
6
- * Adjusts a date to UTC, handling timezone offsets
7
- * @param {Date} date - The date to adjust
8
- * @returns {Date} UTC-adjusted date
9
- */
10
- export function adjustToUTC(date) {
11
- const newDate = new Date(date);
12
- newDate.setMinutes(newDate.getMinutes() - newDate.getTimezoneOffset());
13
- return newDate;
14
- }
15
-
16
6
  /**
17
7
  * Creates a date range for a given month or specific range
18
8
  * @param {Object} params - Date parameters
19
9
  * @param {string|Date} params.date - Single date for month range
20
10
  * @param {Object} [params.dateRange] - Specific date range
21
- * @param {string|Date} params.dateRange.start - Start date
22
- * @param {string|Date} params.dateRange.end - End date
23
- * @returns {Object} Start and end dates
11
+ * @param {number|string|Date} params.dateRange.start - Start date (ms timestamp, ISO string, or Date)
12
+ * @param {number|string|Date} params.dateRange.end - End date (ms timestamp, ISO string, or Date)
13
+ * @returns {Object} Start and end dates in UTC
24
14
  */
25
15
  export function createDateRange({ date, dateRange }) {
26
16
  let startDate, endDate;
27
17
 
28
18
  if (dateRange) {
29
- startDate = adjustToUTC(new Date(dateRange.start));
30
- endDate = adjustToUTC(new Date(dateRange.end));
19
+ startDate = new Date(dateRange.start);
20
+ endDate = new Date(dateRange.end);
31
21
  } else {
32
22
  const dateObject = new Date(date);
33
- startDate = new Date(dateObject.getUTCFullYear(), dateObject.getUTCMonth(), 1);
34
- // Get the last day of the current month by finding the last day that still belongs to this month
35
- const lastDay = new Date(dateObject.getUTCFullYear(), dateObject.getUTCMonth() + 1, 0).getUTCDate();
36
- endDate = new Date(dateObject.getUTCFullYear(), dateObject.getUTCMonth(), lastDay);
23
+ const year = dateObject.getUTCFullYear();
24
+ const month = dateObject.getUTCMonth();
25
+ startDate = new Date(Date.UTC(year, month, 1));
26
+ const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
27
+ endDate = new Date(Date.UTC(year, month, lastDay));
37
28
  }
38
29
 
39
- // Set precise start/end times
30
+ // Set precise start/end times in UTC
40
31
  startDate.setUTCHours(0, 0, 0, 0);
41
32
  endDate.setUTCHours(23, 59, 59, 999);
42
33
 
@@ -47,18 +38,18 @@ export function createDateRange({ date, dateRange }) {
47
38
  * Creates a date range suitable for Stripe API queries
48
39
  * @param {Date} startDate - Range start date
49
40
  * @param {Date} endDate - Range end date
50
- * @returns {Object} Stripe-compatible date range
41
+ * @returns {Object} Stripe-compatible date range (Unix seconds)
51
42
  */
52
43
  export function createStripeTimeRange(startDate, endDate) {
53
44
  return {
54
- gte: Math.floor(new Date(startDate.getTime() - 24 * 60 * 60 * 1000).getTime() / 1000),
55
- lte: Math.floor(new Date(endDate.getTime() + 24 * 60 * 60 * 1000).getTime() / 1000)
45
+ gte: Math.floor(startDate.getTime() / 1000),
46
+ lte: Math.floor(endDate.getTime() / 1000)
56
47
  };
57
48
  }
58
49
 
59
50
  /**
60
- * Checks if a timestamp falls within a date range
61
- * @param {number} timestamp - Unix timestamp to check
51
+ * Checks if a Unix timestamp falls within a date range
52
+ * @param {number} timestamp - Unix timestamp in seconds
62
53
  * @param {Date} startDate - Range start date
63
54
  * @param {Date} endDate - Range end date
64
55
  * @returns {boolean} Whether timestamp is in range