@liiift-studio/sales-portal 1.2.1
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/README.md +461 -0
- package/SETUP.md +230 -0
- package/api/getAnalytics.d.ts +38 -0
- package/api/getAnalytics.js +346 -0
- package/api/getBalanceTransactions.d.ts +29 -0
- package/api/getBalanceTransactions.js +125 -0
- package/api/getDesignerInfo.d.ts +37 -0
- package/api/getDesignerInfo.js +98 -0
- package/api/getDesigners.d.ts +28 -0
- package/api/getDesigners.js +63 -0
- package/api/getPreviousSales.d.ts +23 -0
- package/api/getPreviousSales.js +82 -0
- package/api/getSales.d.ts +29 -0
- package/api/getSales.js +50 -0
- package/api/getSalesRange.d.ts +23 -0
- package/api/getSalesRange.js +58 -0
- package/api/utils/authMiddleware.js +84 -0
- package/api/utils/dateUtils.js +69 -0
- package/api/utils/feeCalculator.js +148 -0
- package/api/utils/processors/invoiceProcessor.js +337 -0
- package/api/utils/processors/paymentProcessor.js +462 -0
- package/api/utils/salesDataProcessing.js +596 -0
- package/api/utils/salesDataProcessor.js +224 -0
- package/api/utils/stripeFetcher.js +248 -0
- package/components/DateRangeSalesTable.js +1072 -0
- package/components/DebugValues.js +48 -0
- package/components/LicenseTypeList.js +193 -0
- package/components/LoginForm.js +219 -0
- package/components/PeriodComparison.js +501 -0
- package/components/Sales.js +773 -0
- package/components/SalesChart.js +307 -0
- package/components/SalesPortalPage.js +147 -0
- package/components/SalesTable.js +677 -0
- package/components/SummaryCards.js +345 -0
- package/components/TopPerformers.js +331 -0
- package/components/TypefaceList.js +154 -0
- package/components/table-columns.js +70 -0
- package/components/table-row-cells.js +295 -0
- package/data/countryCode.json +318 -0
- package/hooks/useSalesDateQuery.d.ts +20 -0
- package/hooks/useSalesDateQuery.js +71 -0
- package/index.d.ts +172 -0
- package/index.js +33 -0
- package/package.json +87 -0
- package/styles/sales-portal.module.scss +383 -0
- package/styles/sales-portal.theme.d.ts +5 -0
- package/styles/sales-portal.theme.js +799 -0
- package/utils/currencyUtils.d.ts +20 -0
- package/utils/currencyUtils.js +79 -0
- package/utils/salesDataProcessing.d.ts +44 -0
- package/utils/salesDataProcessing.js +596 -0
- package/utils/useSalesDateQuery.js +71 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Type definitions for getAnalytics API handler
|
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
3
|
+
|
|
4
|
+
export interface GetAnalyticsRequest extends NextApiRequest {
|
|
5
|
+
query: {
|
|
6
|
+
month: string;
|
|
7
|
+
year: string;
|
|
8
|
+
user?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AnalyticsData {
|
|
14
|
+
totalRevenue: number;
|
|
15
|
+
orderCount: number;
|
|
16
|
+
averageOrderValue: number;
|
|
17
|
+
topTypefaces?: any[];
|
|
18
|
+
topLicenseTypes?: any[];
|
|
19
|
+
geographicDistribution?: any[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AnalyticsResponse {
|
|
23
|
+
success: boolean;
|
|
24
|
+
data?: AnalyticsData;
|
|
25
|
+
error?: string;
|
|
26
|
+
message?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const config: {
|
|
30
|
+
maxDuration: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
declare function handler(
|
|
34
|
+
req: GetAnalyticsRequest,
|
|
35
|
+
res: NextApiResponse<AnalyticsResponse>
|
|
36
|
+
): Promise<void>;
|
|
37
|
+
|
|
38
|
+
export default handler;
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// API endpoint for fetching sales analytics data
|
|
2
|
+
import { authMiddleware } from './utils/authMiddleware';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* API handler for generating analytics from sales data
|
|
6
|
+
* @param {Object} req - HTTP request object
|
|
7
|
+
* @param {Object} res - HTTP response object
|
|
8
|
+
*/
|
|
9
|
+
export default async function handler(req, res) {
|
|
10
|
+
// Only allow POST requests
|
|
11
|
+
if (req.method !== 'POST') {
|
|
12
|
+
return res.status(405).json({ success: false, message: 'Method not allowed' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Authenticate request
|
|
17
|
+
const { authorized, designer, error } = await authMiddleware(req);
|
|
18
|
+
|
|
19
|
+
if (!authorized) {
|
|
20
|
+
return res.status(401).json({ success: false, message: error || 'Unauthorized' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Extract request parameters
|
|
24
|
+
const {
|
|
25
|
+
date,
|
|
26
|
+
compareWith = 'previous_month', // 'previous_month', 'previous_year', 'custom'
|
|
27
|
+
customStartDate,
|
|
28
|
+
customEndDate,
|
|
29
|
+
admin = false
|
|
30
|
+
} = req.body;
|
|
31
|
+
|
|
32
|
+
if (!date) {
|
|
33
|
+
return res.status(400).json({ success: false, message: 'Date is required' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate analytics based on current sales data
|
|
37
|
+
try {
|
|
38
|
+
// Call your existing sales data retrieval function to get current period data
|
|
39
|
+
const { getSalesData } = require('./getSales');
|
|
40
|
+
const currentPeriodSales = await getSalesData(date, designer, admin);
|
|
41
|
+
|
|
42
|
+
// Get comparison period data
|
|
43
|
+
let comparisonPeriodDate;
|
|
44
|
+
let comparisonLabel;
|
|
45
|
+
|
|
46
|
+
if (compareWith === 'previous_month') {
|
|
47
|
+
comparisonPeriodDate = new Date(date);
|
|
48
|
+
comparisonPeriodDate.setUTCMonth(comparisonPeriodDate.getUTCMonth() - 1);
|
|
49
|
+
comparisonLabel = 'Previous Month';
|
|
50
|
+
} else if (compareWith === 'previous_year') {
|
|
51
|
+
comparisonPeriodDate = new Date(date);
|
|
52
|
+
comparisonPeriodDate.setUTCFullYear(comparisonPeriodDate.getUTCFullYear() - 1);
|
|
53
|
+
comparisonLabel = 'Previous Year';
|
|
54
|
+
} else if (compareWith === 'custom' && customStartDate && customEndDate) {
|
|
55
|
+
// Handle custom date range
|
|
56
|
+
comparisonPeriodDate = {
|
|
57
|
+
start: new Date(customStartDate),
|
|
58
|
+
end: new Date(customEndDate)
|
|
59
|
+
};
|
|
60
|
+
comparisonLabel = 'Custom Range';
|
|
61
|
+
} else {
|
|
62
|
+
// Default to previous month
|
|
63
|
+
comparisonPeriodDate = new Date(date);
|
|
64
|
+
comparisonPeriodDate.setUTCMonth(comparisonPeriodDate.getUTCMonth() - 1);
|
|
65
|
+
comparisonLabel = 'Previous Month';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fetch comparison period data
|
|
69
|
+
const comparisonPeriodSales = await getSalesData(comparisonPeriodDate, designer, admin);
|
|
70
|
+
|
|
71
|
+
// Calculate analytics
|
|
72
|
+
const analytics = generateAnalytics(currentPeriodSales, comparisonPeriodSales);
|
|
73
|
+
|
|
74
|
+
// Return the analytics data
|
|
75
|
+
return res.status(200).json({
|
|
76
|
+
success: true,
|
|
77
|
+
data: analytics,
|
|
78
|
+
metadata: {
|
|
79
|
+
currentPeriod: new Date(date).toISOString(),
|
|
80
|
+
comparisonPeriod: comparisonPeriodDate instanceof Date
|
|
81
|
+
? comparisonPeriodDate.toISOString()
|
|
82
|
+
: {
|
|
83
|
+
start: comparisonPeriodDate.start.toISOString(),
|
|
84
|
+
end: comparisonPeriodDate.end.toISOString()
|
|
85
|
+
},
|
|
86
|
+
comparisonLabel
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Error generating analytics:', error);
|
|
91
|
+
return res.status(500).json({ success: false, message: 'Failed to generate analytics' });
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Error in getAnalytics API:', error);
|
|
95
|
+
return res.status(500).json({ success: false, message: 'Internal server error' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate analytics from sales data
|
|
101
|
+
* @param {Array} currentSales - Current period sales data
|
|
102
|
+
* @param {Array} comparisonSales - Comparison period sales data
|
|
103
|
+
* @returns {Object} Analytics data
|
|
104
|
+
*/
|
|
105
|
+
function generateAnalytics(currentSales, comparisonSales) {
|
|
106
|
+
// Initialize analytics object with proper documentation
|
|
107
|
+
/**
|
|
108
|
+
* Analytics data structure
|
|
109
|
+
* Note: All currency values are stored in dollars (not cents)
|
|
110
|
+
*/
|
|
111
|
+
const analytics = {
|
|
112
|
+
summary: {
|
|
113
|
+
totalRevenue: 0,
|
|
114
|
+
totalOrders: 0,
|
|
115
|
+
averageOrderValue: 0,
|
|
116
|
+
totalTax: 0,
|
|
117
|
+
totalShipping: 0,
|
|
118
|
+
totalDiscounts: 0,
|
|
119
|
+
totalRefunds: 0
|
|
120
|
+
},
|
|
121
|
+
comparison: {
|
|
122
|
+
revenueChange: 0,
|
|
123
|
+
revenueChangePercent: 0,
|
|
124
|
+
orderChange: 0,
|
|
125
|
+
orderChangePercent: 0,
|
|
126
|
+
aovChange: 0,
|
|
127
|
+
aovChangePercent: 0
|
|
128
|
+
},
|
|
129
|
+
typefaces: [],
|
|
130
|
+
bestSellingTypefaces: [],
|
|
131
|
+
worstSellingTypefaces: [],
|
|
132
|
+
designers: [],
|
|
133
|
+
topDesigners: [],
|
|
134
|
+
salesByDay: {},
|
|
135
|
+
salesByWeekday: {
|
|
136
|
+
"Sunday": 0,
|
|
137
|
+
"Monday": 0,
|
|
138
|
+
"Tuesday": 0,
|
|
139
|
+
"Wednesday": 0,
|
|
140
|
+
"Thursday": 0,
|
|
141
|
+
"Friday": 0,
|
|
142
|
+
"Saturday": 0
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Skip processing if no sales data
|
|
147
|
+
if (!currentSales || currentSales.length === 0) {
|
|
148
|
+
return analytics;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Process current period sales data
|
|
152
|
+
let totalRevenue = 0;
|
|
153
|
+
let totalTax = 0;
|
|
154
|
+
let totalShipping = 0;
|
|
155
|
+
let totalDiscounts = 0;
|
|
156
|
+
let totalRefunds = 0;
|
|
157
|
+
|
|
158
|
+
// Typeface and designer tracking
|
|
159
|
+
const typefaceMap = new Map();
|
|
160
|
+
const designerMap = new Map();
|
|
161
|
+
const dailySalesMap = new Map();
|
|
162
|
+
const weekdaySalesMap = new Map();
|
|
163
|
+
|
|
164
|
+
// Initialize weekday totals
|
|
165
|
+
for (let i = 0; i < 7; i++) {
|
|
166
|
+
weekdaySalesMap.set(i, 0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Process each sale
|
|
170
|
+
currentSales.forEach(sale => {
|
|
171
|
+
// Skip shipping provisions
|
|
172
|
+
if (sale.shippingProvision) {
|
|
173
|
+
totalShipping += sale.total || 0;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Basic metrics
|
|
178
|
+
totalRevenue += sale.total || 0;
|
|
179
|
+
|
|
180
|
+
// Tax totals
|
|
181
|
+
if (sale.taxAmounts && sale.taxAmounts.length) {
|
|
182
|
+
sale.taxAmounts.forEach(tax => {
|
|
183
|
+
totalTax += tax.amount || 0;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Discount totals
|
|
188
|
+
if (sale.discountAmounts && sale.discountAmounts.length) {
|
|
189
|
+
sale.discountAmounts.forEach(discount => {
|
|
190
|
+
totalDiscounts += discount.amount || 0;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Refund totals
|
|
195
|
+
if (sale.refunds && sale.refunds.length) {
|
|
196
|
+
sale.refunds.forEach(refund => {
|
|
197
|
+
totalRefunds += refund.adjustedTotal || 0;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Process by typeface
|
|
202
|
+
const typefaceTitle = getTypefaceTitle(sale);
|
|
203
|
+
if (typefaceTitle) {
|
|
204
|
+
const current = typefaceMap.get(typefaceTitle) || { revenue: 0, orders: 0, author: sale.author };
|
|
205
|
+
current.revenue += sale.total || 0;
|
|
206
|
+
current.orders += 1;
|
|
207
|
+
typefaceMap.set(typefaceTitle, current);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Process by designer
|
|
211
|
+
if (sale.author) {
|
|
212
|
+
const designerId = sale.author._id || sale.author;
|
|
213
|
+
const designerName = sale.author.firstName && sale.author.lastName
|
|
214
|
+
? `${sale.author.firstName} ${sale.author.lastName}`
|
|
215
|
+
: 'Unknown Designer';
|
|
216
|
+
|
|
217
|
+
const current = designerMap.get(designerId) || { id: designerId, name: designerName, revenue: 0, orders: 0 };
|
|
218
|
+
current.revenue += sale.total || 0;
|
|
219
|
+
current.orders += 1;
|
|
220
|
+
designerMap.set(designerId, current);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Process by day
|
|
224
|
+
const saleDate = new Date(sale.date || sale.created * 1000);
|
|
225
|
+
const dayKey = saleDate.toISOString().split('T')[0];
|
|
226
|
+
const weekdayIndex = saleDate.getUTCDay(); // 0 = Sunday, 6 = Saturday
|
|
227
|
+
|
|
228
|
+
const dailyTotal = dailySalesMap.get(dayKey) || 0;
|
|
229
|
+
dailySalesMap.set(dayKey, dailyTotal + (sale.total || 0));
|
|
230
|
+
|
|
231
|
+
const weekdayTotal = weekdaySalesMap.get(weekdayIndex) || 0;
|
|
232
|
+
weekdaySalesMap.set(weekdayIndex, weekdayTotal + (sale.total || 0));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Process comparison data if available
|
|
236
|
+
let comparisonRevenue = 0;
|
|
237
|
+
let comparisonOrders = 0;
|
|
238
|
+
|
|
239
|
+
if (comparisonSales && comparisonSales.length > 0) {
|
|
240
|
+
comparisonSales.forEach(sale => {
|
|
241
|
+
if (!sale.shippingProvision) {
|
|
242
|
+
comparisonRevenue += sale.total || 0;
|
|
243
|
+
comparisonOrders += 1;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Set summary data
|
|
249
|
+
analytics.summary.totalRevenue = totalRevenue;
|
|
250
|
+
analytics.summary.totalOrders = currentSales.filter(sale => !sale.shippingProvision).length;
|
|
251
|
+
analytics.summary.averageOrderValue = analytics.summary.totalOrders > 0
|
|
252
|
+
? totalRevenue / analytics.summary.totalOrders
|
|
253
|
+
: 0;
|
|
254
|
+
analytics.summary.totalTax = totalTax;
|
|
255
|
+
analytics.summary.totalShipping = totalShipping;
|
|
256
|
+
analytics.summary.totalDiscounts = totalDiscounts;
|
|
257
|
+
analytics.summary.totalRefunds = totalRefunds;
|
|
258
|
+
|
|
259
|
+
// Set comparison data
|
|
260
|
+
analytics.comparison.revenueChange = totalRevenue - comparisonRevenue;
|
|
261
|
+
analytics.comparison.revenueChangePercent = comparisonRevenue > 0
|
|
262
|
+
? (totalRevenue - comparisonRevenue) / comparisonRevenue * 100
|
|
263
|
+
: 0;
|
|
264
|
+
analytics.comparison.orderChange = analytics.summary.totalOrders - comparisonOrders;
|
|
265
|
+
analytics.comparison.orderChangePercent = comparisonOrders > 0
|
|
266
|
+
? (analytics.summary.totalOrders - comparisonOrders) / comparisonOrders * 100
|
|
267
|
+
: 0;
|
|
268
|
+
|
|
269
|
+
const comparisonAOV = comparisonOrders > 0 ? comparisonRevenue / comparisonOrders : 0;
|
|
270
|
+
analytics.comparison.aovChange = analytics.summary.averageOrderValue - comparisonAOV;
|
|
271
|
+
analytics.comparison.aovChangePercent = comparisonAOV > 0
|
|
272
|
+
? (analytics.summary.averageOrderValue - comparisonAOV) / comparisonAOV * 100
|
|
273
|
+
: 0;
|
|
274
|
+
|
|
275
|
+
// Set typeface data
|
|
276
|
+
analytics.typefaces = Array.from(typefaceMap.entries()).map(([title, data]) => ({
|
|
277
|
+
title,
|
|
278
|
+
revenue: data.revenue,
|
|
279
|
+
orders: data.orders,
|
|
280
|
+
author: data.author
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
// Best and worst selling typefaces
|
|
284
|
+
analytics.bestSellingTypefaces = [...analytics.typefaces]
|
|
285
|
+
.sort((a, b) => b.revenue - a.revenue)
|
|
286
|
+
.slice(0, 5);
|
|
287
|
+
|
|
288
|
+
analytics.worstSellingTypefaces = [...analytics.typefaces]
|
|
289
|
+
.sort((a, b) => a.revenue - b.revenue)
|
|
290
|
+
.slice(0, 5);
|
|
291
|
+
|
|
292
|
+
// Set designer data
|
|
293
|
+
analytics.designers = Array.from(designerMap.values());
|
|
294
|
+
|
|
295
|
+
// Top designers
|
|
296
|
+
analytics.topDesigners = [...analytics.designers]
|
|
297
|
+
.sort((a, b) => b.revenue - a.revenue)
|
|
298
|
+
.slice(0, 5);
|
|
299
|
+
|
|
300
|
+
// Sales by day
|
|
301
|
+
analytics.salesByDay = Object.fromEntries(dailySalesMap);
|
|
302
|
+
|
|
303
|
+
// Sales by weekday
|
|
304
|
+
analytics.salesByWeekday = {
|
|
305
|
+
"Sunday": weekdaySalesMap.get(0) || 0,
|
|
306
|
+
"Monday": weekdaySalesMap.get(1) || 0,
|
|
307
|
+
"Tuesday": weekdaySalesMap.get(2) || 0,
|
|
308
|
+
"Wednesday": weekdaySalesMap.get(3) || 0,
|
|
309
|
+
"Thursday": weekdaySalesMap.get(4) || 0,
|
|
310
|
+
"Friday": weekdaySalesMap.get(5) || 0,
|
|
311
|
+
"Saturday": weekdaySalesMap.get(6) || 0
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return analytics;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Extract typeface title from sale description
|
|
319
|
+
* @param {Object} sale - Sale data object
|
|
320
|
+
* @returns {string} Typeface title
|
|
321
|
+
*/
|
|
322
|
+
function getTypefaceTitle(sale) {
|
|
323
|
+
if (!sale || !sale.description) return 'Unknown Typeface';
|
|
324
|
+
|
|
325
|
+
let title = sale.description;
|
|
326
|
+
|
|
327
|
+
// Extract title before parentheses
|
|
328
|
+
const parenIndex = title.indexOf(' (');
|
|
329
|
+
if (parenIndex !== -1) {
|
|
330
|
+
title = title.substring(0, parenIndex);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Extract title before em dash
|
|
334
|
+
const dashIndex = title.indexOf(' —');
|
|
335
|
+
if (dashIndex !== -1) {
|
|
336
|
+
title = title.substring(0, dashIndex);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Extract title before period
|
|
340
|
+
const periodIndex = title.indexOf('.');
|
|
341
|
+
if (periodIndex !== -1) {
|
|
342
|
+
title = title.substring(0, periodIndex);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return title.trim() || 'Unknown Typeface';
|
|
346
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Type definitions for getBalanceTransactions API handler
|
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
3
|
+
|
|
4
|
+
export interface GetBalanceTransactionsRequest extends NextApiRequest {
|
|
5
|
+
query: {
|
|
6
|
+
month: string;
|
|
7
|
+
year: string;
|
|
8
|
+
user?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BalanceTransactionsResponse {
|
|
14
|
+
success: boolean;
|
|
15
|
+
data?: any[];
|
|
16
|
+
error?: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const config: {
|
|
21
|
+
maxDuration: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
declare function handler(
|
|
25
|
+
req: GetBalanceTransactionsRequest,
|
|
26
|
+
res: NextApiResponse<BalanceTransactionsResponse>
|
|
27
|
+
): Promise<void>;
|
|
28
|
+
|
|
29
|
+
export default handler;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// API endpoint to fetch Stripe balance transactions for reconciliation
|
|
2
|
+
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetches Stripe balance transactions for a specific month
|
|
6
|
+
* @param {Object} req - HTTP request object
|
|
7
|
+
* @param {Object} res - HTTP response object
|
|
8
|
+
*/
|
|
9
|
+
export default async function handler(req, res) {
|
|
10
|
+
// Only allow POST requests
|
|
11
|
+
if (req.method !== 'POST') {
|
|
12
|
+
return res.status(405).json({ success: false, message: 'Method not allowed' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Get date or dateRange from request body
|
|
17
|
+
const { date, dateRange, admin } = req.body;
|
|
18
|
+
|
|
19
|
+
let timeRange;
|
|
20
|
+
|
|
21
|
+
if (dateRange) {
|
|
22
|
+
// If dateRange is provided, use it directly
|
|
23
|
+
if (!dateRange.start || !dateRange.end) {
|
|
24
|
+
return res.status(400).json({ success: false, message: 'dateRange must include start and end timestamps' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convert millisecond timestamps to seconds for Stripe API
|
|
28
|
+
timeRange = {
|
|
29
|
+
gte: Math.floor(dateRange.start / 1000),
|
|
30
|
+
lte: Math.floor(dateRange.end / 1000)
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
console.log('Using date range:', {
|
|
34
|
+
start: new Date(dateRange.start).toISOString(),
|
|
35
|
+
end: new Date(dateRange.end).toISOString()
|
|
36
|
+
});
|
|
37
|
+
} else if (date) {
|
|
38
|
+
// If only date is provided, create a month range
|
|
39
|
+
const targetDate = new Date(date);
|
|
40
|
+
const startOfMonth = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth(), 1, 0, 0, 0));
|
|
41
|
+
const endOfMonth = new Date(Date.UTC(targetDate.getUTCFullYear(), targetDate.getUTCMonth() + 1, 0, 23, 59, 59));
|
|
42
|
+
|
|
43
|
+
timeRange = {
|
|
44
|
+
gte: Math.floor(startOfMonth.getTime() / 1000),
|
|
45
|
+
lte: Math.floor(endOfMonth.getTime() / 1000)
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
console.log('Using month range:', {
|
|
49
|
+
start: startOfMonth.toISOString(),
|
|
50
|
+
end: endOfMonth.toISOString()
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
return res.status(400).json({ success: false, message: 'Either date or dateRange is required' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fetch all balance transactions for the month
|
|
57
|
+
const balanceTransactions = await fetchAllBalanceTransactions(timeRange);
|
|
58
|
+
|
|
59
|
+
// Calculate total balance change for the month (ignoring payouts and pending transactions)
|
|
60
|
+
const totalBalanceChange = balanceTransactions.reduce((total, transaction) => {
|
|
61
|
+
// Skip payout, stripe_fee and pending transactions
|
|
62
|
+
if (transaction.type === 'payout' || transaction.type === 'stripe_fee' || transaction.status === 'pending') {
|
|
63
|
+
return total;
|
|
64
|
+
}
|
|
65
|
+
return total + transaction.amount;
|
|
66
|
+
}, 0);
|
|
67
|
+
|
|
68
|
+
return res.status(200).json({
|
|
69
|
+
success: true,
|
|
70
|
+
data: {
|
|
71
|
+
totalBalanceChange,
|
|
72
|
+
transactions: balanceTransactions
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Error fetching balance transactions:', error);
|
|
77
|
+
return res.status(500).json({
|
|
78
|
+
success: false,
|
|
79
|
+
message: 'Failed to fetch balance transactions',
|
|
80
|
+
error: error.message
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetches all pages of Stripe balance transactions for a given date range
|
|
87
|
+
* @param {Object} timeRange - Stripe-compatible date range
|
|
88
|
+
* @returns {Promise<Array>} Array of all fetched balance transactions
|
|
89
|
+
*/
|
|
90
|
+
async function fetchAllBalanceTransactions(timeRange) {
|
|
91
|
+
const fetchOptions = {
|
|
92
|
+
created: timeRange,
|
|
93
|
+
limit: 100,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let allTransactions = [];
|
|
97
|
+
let hasMore = true;
|
|
98
|
+
let lastId = null;
|
|
99
|
+
let pageCount = 0;
|
|
100
|
+
|
|
101
|
+
while (hasMore) {
|
|
102
|
+
try {
|
|
103
|
+
const fetchParams = { ...fetchOptions };
|
|
104
|
+
if (lastId) {
|
|
105
|
+
fetchParams.starting_after = lastId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`Fetching balance transaction page ${pageCount + 1}...`);
|
|
109
|
+
const response = await stripe.balanceTransactions.list(fetchParams);
|
|
110
|
+
|
|
111
|
+
if (response.data.length > 0) {
|
|
112
|
+
allTransactions = allTransactions.concat(response.data);
|
|
113
|
+
lastId = response.data[response.data.length - 1].id;
|
|
114
|
+
hasMore = response.has_more;
|
|
115
|
+
pageCount++;
|
|
116
|
+
} else {
|
|
117
|
+
hasMore = false;
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('Error fetching balance transaction page:', error);
|
|
121
|
+
throw new Error(`Failed to fetch balance transaction page ${pageCount + 1}: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return allTransactions;
|
|
125
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Type definitions for getDesignerInfo API handler
|
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
3
|
+
|
|
4
|
+
export interface Designer {
|
|
5
|
+
_id: string;
|
|
6
|
+
firstName: string;
|
|
7
|
+
lastName: string;
|
|
8
|
+
user: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GetDesignerInfoRequest extends NextApiRequest {
|
|
13
|
+
body: {
|
|
14
|
+
user: string;
|
|
15
|
+
password: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DesignerInfoResponse {
|
|
20
|
+
success: boolean;
|
|
21
|
+
designer?: Designer;
|
|
22
|
+
designers?: Designer[];
|
|
23
|
+
admin?: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const config: {
|
|
29
|
+
maxDuration: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
declare function handler(
|
|
33
|
+
req: GetDesignerInfoRequest,
|
|
34
|
+
res: NextApiResponse<DesignerInfoResponse>
|
|
35
|
+
): Promise<void>;
|
|
36
|
+
|
|
37
|
+
export default handler;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// API endpoint for retrieving designer information and authentication
|
|
2
|
+
|
|
3
|
+
// Configure extended timeout for Vercel serverless function
|
|
4
|
+
export const config = { maxDuration: 300 };
|
|
5
|
+
|
|
6
|
+
// Import required dependencies
|
|
7
|
+
const { createClient } = require('@sanity/client');
|
|
8
|
+
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sanity client configuration for CMS interactions
|
|
12
|
+
* Uses environment variables for secure configuration
|
|
13
|
+
*/
|
|
14
|
+
const client = createClient({
|
|
15
|
+
projectId: process.env.SANITY_STUDIO_PROJECT_ID,
|
|
16
|
+
dataset: process.env.SANITY_STUDIO_DATASET,
|
|
17
|
+
apiVersion: process.env.SANITY_STUDIO_API_VERSION,
|
|
18
|
+
token: process.env.SANITY_STUDIO_TOKEN,
|
|
19
|
+
useCdn: false, // Ensures fresh data by bypassing CDN
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* API route handler for designer information
|
|
24
|
+
* Handles authentication and retrieval of designer data
|
|
25
|
+
* @param {Object} req - HTTP request object containing user credentials
|
|
26
|
+
* @param {Object} res - HTTP response object
|
|
27
|
+
*/
|
|
28
|
+
export default async function handler(req, res) {
|
|
29
|
+
const { method } = req;
|
|
30
|
+
|
|
31
|
+
// Check if sales portal is enabled
|
|
32
|
+
if (process.env.SALES_PORTAL_ENABLED === 'false') {
|
|
33
|
+
return res.status(503).json({
|
|
34
|
+
success: false,
|
|
35
|
+
message: 'Sales portal is currently disabled',
|
|
36
|
+
disabled: true
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Only handle POST requests
|
|
41
|
+
if (method === 'POST') {
|
|
42
|
+
const { user, password } = req.body;
|
|
43
|
+
|
|
44
|
+
// Authenticate designer using Sanity
|
|
45
|
+
const designer = await client.fetch(
|
|
46
|
+
`*[_type == "account" && email == '${user}' && password == '${password}' && isDesigner][0]`
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (designer) {
|
|
50
|
+
// Handle admin user case
|
|
51
|
+
if (designer.isAdmin) {
|
|
52
|
+
// Fetch all designers for admin view
|
|
53
|
+
const designers = await client.fetch(`*[_type == "account" && isDesigner]`);
|
|
54
|
+
|
|
55
|
+
if (designers) {
|
|
56
|
+
res.status(200).json({
|
|
57
|
+
success: true,
|
|
58
|
+
admin: true,
|
|
59
|
+
data: designers.map(designer => ({
|
|
60
|
+
_id: designer._id,
|
|
61
|
+
firstName: designer?.firstName || (designer?.name || 'Nameless'),
|
|
62
|
+
lastName: designer?.lastName || '',
|
|
63
|
+
user: designer.email,
|
|
64
|
+
password: designer.password,
|
|
65
|
+
})).sort((a, b) =>
|
|
66
|
+
a?.firstName
|
|
67
|
+
? a.firstName.localeCompare(b.firstName)
|
|
68
|
+
: a.name.localeCompare(b.name)
|
|
69
|
+
),
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
res.status(200).json({
|
|
73
|
+
success: false,
|
|
74
|
+
message: 'No designers... thats weird.'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// Handle regular designer case
|
|
79
|
+
res.status(200).json({
|
|
80
|
+
success: true,
|
|
81
|
+
data: [{
|
|
82
|
+
_id: designer._id,
|
|
83
|
+
firstName: designer?.firstName || (designer?.name || 'Nameless'),
|
|
84
|
+
lastName: designer?.lastName || '',
|
|
85
|
+
user: designer.email,
|
|
86
|
+
password: designer.password
|
|
87
|
+
}],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// Handle authentication failure
|
|
92
|
+
res.status(200).json({
|
|
93
|
+
success: false,
|
|
94
|
+
message: 'Incorrect email or password.'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Type definitions for getDesigners API handler
|
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
3
|
+
import { Designer } from './getDesignerInfo';
|
|
4
|
+
|
|
5
|
+
export interface GetDesignersRequest extends NextApiRequest {
|
|
6
|
+
body: {
|
|
7
|
+
user: string;
|
|
8
|
+
password: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DesignersResponse {
|
|
13
|
+
success: boolean;
|
|
14
|
+
designers?: Designer[];
|
|
15
|
+
error?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const config: {
|
|
20
|
+
maxDuration: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
declare function handler(
|
|
24
|
+
req: GetDesignersRequest,
|
|
25
|
+
res: NextApiResponse<DesignersResponse>
|
|
26
|
+
): Promise<void>;
|
|
27
|
+
|
|
28
|
+
export default handler;
|