@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.
- package/api/getBalanceTransactions.js +29 -83
- package/api/getDesignerInfo.js +48 -78
- package/api/getDesigners.js +21 -53
- package/api/getSales.js +5 -28
- package/api/utils/apiResponse.js +41 -0
- package/api/utils/authMiddleware.js +3 -24
- package/api/utils/clients.js +44 -0
- package/api/utils/dateUtils.js +17 -26
- package/api/utils/processors/invoiceProcessor.js +40 -38
- package/api/utils/processors/paymentProcessor.js +29 -28
- package/api/utils/salesDataProcessor.js +19 -37
- package/api/utils/stripeFetcher.js +7 -15
- package/components/LoginForm.js +44 -33
- package/components/Sales.js +118 -34
- package/components/SalesPortalPage.js +15 -3
- package/package.json +1 -1
- package/api/getAnalytics.js +0 -346
- package/api/getPreviousSales.js +0 -82
- package/api/getSalesRange.js +0 -58
|
@@ -1,66 +1,29 @@
|
|
|
1
1
|
// API endpoint to fetch Stripe balance transactions for reconciliation
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
111
|
-
const response = await stripe.balanceTransactions.list(fetchParams);
|
|
60
|
+
const response = await stripe.balanceTransactions.list(fetchParams);
|
|
112
61
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
}
|
package/api/getDesignerInfo.js
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
16
|
+
if (!requirePost(req, res)) return;
|
|
17
|
+
|
|
18
|
+
const { user, password } = req.body;
|
|
45
19
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
}
|
package/api/getDesigners.js
CHANGED
|
@@ -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
|
-
|
|
8
|
+
if (req.method !== 'GET') {
|
|
9
|
+
return sendError(res, 405, 'Method not allowed');
|
|
10
|
+
}
|
|
34
11
|
|
|
35
|
-
|
|
36
|
-
const designers = await
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/api/utils/dateUtils.js
CHANGED
|
@@ -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 =
|
|
30
|
-
endDate =
|
|
19
|
+
startDate = new Date(dateRange.start);
|
|
20
|
+
endDate = new Date(dateRange.end);
|
|
31
21
|
} else {
|
|
32
22
|
const dateObject = new Date(date);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
55
|
-
lte: Math.floor(
|
|
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
|
|
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
|