@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,63 @@
|
|
|
1
|
+
// API endpoint to fetch designer accounts from Sanity CMS
|
|
2
|
+
|
|
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
|
+
export const config = { maxDuration: 300 };
|
|
10
|
+
|
|
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
|
+
export default async function handler(req, res) {
|
|
33
|
+
const { method } = req;
|
|
34
|
+
|
|
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
|
+
);
|
|
50
|
+
|
|
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
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Type definitions for getPreviousSales API handler
|
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
3
|
+
import { SalesAPIResponse } from './getSales';
|
|
4
|
+
|
|
5
|
+
export interface GetPreviousSalesRequest extends NextApiRequest {
|
|
6
|
+
query: {
|
|
7
|
+
month: string;
|
|
8
|
+
year: string;
|
|
9
|
+
user?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const config: {
|
|
15
|
+
maxDuration: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
declare function handler(
|
|
19
|
+
req: GetPreviousSalesRequest,
|
|
20
|
+
res: NextApiResponse<SalesAPIResponse>
|
|
21
|
+
): Promise<void>;
|
|
22
|
+
|
|
23
|
+
export default handler;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// API route for fetching sales data from a previous period
|
|
2
|
+
import { authMiddleware } from './utils/authMiddleware';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* API handler for fetching sales data from a previous period
|
|
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 using middleware
|
|
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 { date, comparisonType = 'MoM', admin } = req.body;
|
|
25
|
+
|
|
26
|
+
if (!date) {
|
|
27
|
+
return res.status(400).json({ success: false, message: 'Date is required' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Calculate previous period date based on comparison type
|
|
31
|
+
const currentDate = new Date(date);
|
|
32
|
+
let previousDate = new Date(currentDate);
|
|
33
|
+
|
|
34
|
+
if (comparisonType === 'MoM') {
|
|
35
|
+
// Month over Month - go back 1 month
|
|
36
|
+
previousDate.setUTCMonth(previousDate.getUTCMonth() - 1);
|
|
37
|
+
} else if (comparisonType === 'YoY') {
|
|
38
|
+
// Year over Year - go back 1 year
|
|
39
|
+
previousDate.setUTCFullYear(previousDate.getUTCFullYear() - 1);
|
|
40
|
+
} else {
|
|
41
|
+
return res.status(400).json({ success: false, message: 'Invalid comparison type' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Use the existing getSales logic to fetch sales data for the previous period
|
|
45
|
+
// This would typically involve database queries or API calls to your sales data source
|
|
46
|
+
|
|
47
|
+
// Call your existing sales data retrieval function
|
|
48
|
+
// Example:
|
|
49
|
+
const salesData = await fetchSalesData(previousDate, designer, admin);
|
|
50
|
+
|
|
51
|
+
// Return the sales data
|
|
52
|
+
return res.status(200).json({
|
|
53
|
+
success: true,
|
|
54
|
+
data: salesData,
|
|
55
|
+
metadata: {
|
|
56
|
+
currentPeriod: currentDate.toISOString(),
|
|
57
|
+
previousPeriod: previousDate.toISOString(),
|
|
58
|
+
comparisonType
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error fetching previous sales data:', error);
|
|
64
|
+
return res.status(500).json({ success: false, message: 'Internal server error' });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fetch sales data from the database for a given period
|
|
70
|
+
* @param {Date} date - Date to fetch sales for
|
|
71
|
+
* @param {Object} designer - Designer information
|
|
72
|
+
* @param {boolean} admin - Whether the user is an admin
|
|
73
|
+
* @returns {Promise<Array>} Sales data
|
|
74
|
+
*/
|
|
75
|
+
async function fetchSalesData(date, designer, admin) {
|
|
76
|
+
// Implement your sales data retrieval logic here
|
|
77
|
+
// This would typically involve database queries
|
|
78
|
+
|
|
79
|
+
// For now, import and use the same logic from the getSales API endpoint
|
|
80
|
+
const { getSalesData } = require('./getSales');
|
|
81
|
+
return await getSalesData(date, designer, admin);
|
|
82
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Type definitions for getSales API handler
|
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
3
|
+
|
|
4
|
+
export interface GetSalesRequest extends NextApiRequest {
|
|
5
|
+
query: {
|
|
6
|
+
month: string;
|
|
7
|
+
year: string;
|
|
8
|
+
user?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SalesAPIResponse {
|
|
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: GetSalesRequest,
|
|
26
|
+
res: NextApiResponse<SalesAPIResponse>
|
|
27
|
+
): Promise<void>;
|
|
28
|
+
|
|
29
|
+
export default handler;
|
package/api/getSales.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
|
|
6
|
+
import { authenticateDesigner, processSalesData } from './utils/salesDataProcessor';
|
|
7
|
+
|
|
8
|
+
// API config for extended processing time
|
|
9
|
+
export const config = { maxDuration: 300 };
|
|
10
|
+
|
|
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
|
+
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
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Extract request parameters
|
|
23
|
+
const { user, password, date, dateRange, admin } = req.body;
|
|
24
|
+
|
|
25
|
+
// Authenticate designer
|
|
26
|
+
const designer = await authenticateDesigner(user, password);
|
|
27
|
+
if (!designer) {
|
|
28
|
+
return res.status(200).json({
|
|
29
|
+
success: false,
|
|
30
|
+
message: 'Looks like there was an issue finding the account.'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Process sales data
|
|
35
|
+
const sales = await processSalesData({ date, dateRange, designer, admin });
|
|
36
|
+
|
|
37
|
+
// Return processed data
|
|
38
|
+
res.status(200).json({
|
|
39
|
+
success: true,
|
|
40
|
+
data: sales,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
} 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
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Type definitions for getSalesRange API handler
|
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
3
|
+
import { SalesAPIResponse } from './getSales';
|
|
4
|
+
|
|
5
|
+
export interface GetSalesRangeRequest extends NextApiRequest {
|
|
6
|
+
query: {
|
|
7
|
+
startDate: string;
|
|
8
|
+
endDate: string;
|
|
9
|
+
user?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const config: {
|
|
15
|
+
maxDuration: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
declare function handler(
|
|
19
|
+
req: GetSalesRangeRequest,
|
|
20
|
+
res: NextApiResponse<SalesAPIResponse>
|
|
21
|
+
): Promise<void>;
|
|
22
|
+
|
|
23
|
+
export default handler;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// API endpoint for retrieving sales data within a specified date range
|
|
2
|
+
// File summary: Handles POST requests to retrieve sales data within a specified date range
|
|
3
|
+
import { createClient } from '@sanity/client';
|
|
4
|
+
|
|
5
|
+
// Initialize Sanity client
|
|
6
|
+
const client = createClient({
|
|
7
|
+
projectId: process.env.SANITY_STUDIO_PROJECT_ID,
|
|
8
|
+
dataset: process.env.SANITY_STUDIO_DATASET,
|
|
9
|
+
apiVersion: '2022-04-01',
|
|
10
|
+
token: process.env.SANITY_STUDIO_TOKEN,
|
|
11
|
+
useCdn: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export default async function handler(req, res) {
|
|
15
|
+
if (req.method !== 'POST') {
|
|
16
|
+
return res.status(405).json({ success: false, message: 'Method not allowed' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { user, password, startDate, endDate, admin } = req.body;
|
|
20
|
+
|
|
21
|
+
if (!user || !password || !startDate || !endDate) {
|
|
22
|
+
return res.status(400).json({ success: false, message: 'Missing required fields' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Verify user credentials
|
|
27
|
+
const designer = await client.fetch(
|
|
28
|
+
`*[_type == "designer" && _id == $user && password == $password][0]{
|
|
29
|
+
_id,
|
|
30
|
+
firstName,
|
|
31
|
+
lastName,
|
|
32
|
+
admin,
|
|
33
|
+
password
|
|
34
|
+
}`,
|
|
35
|
+
{ user, password }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!designer) {
|
|
39
|
+
return res.status(401).json({ success: false, message: 'Invalid credentials' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fetch sales data within date range
|
|
43
|
+
const query = admin ?
|
|
44
|
+
`*[_type == "sale" && dateTime >= $startDate && dateTime <= $endDate] | order(dateTime asc)` :
|
|
45
|
+
`*[_type == "sale" && dateTime >= $startDate && dateTime <= $endDate && references($user)] | order(dateTime asc)`;
|
|
46
|
+
|
|
47
|
+
const sales = await client.fetch(query, {
|
|
48
|
+
user,
|
|
49
|
+
startDate: new Date(startDate).toISOString(),
|
|
50
|
+
endDate: new Date(endDate).toISOString()
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return res.status(200).json({ success: true, data: sales });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Error fetching sales range:', error);
|
|
56
|
+
return res.status(500).json({ success: false, message: 'Internal server error' });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Middleware to authenticate API requests
|
|
19
|
+
* Validates credentials and returns designer data
|
|
20
|
+
* @param {Object} req - HTTP request object with user credentials
|
|
21
|
+
* @returns {Object} Authentication result with authorized flag, designer data, and error message
|
|
22
|
+
*/
|
|
23
|
+
export async function authMiddleware(req) {
|
|
24
|
+
// Check if sales portal is enabled via environment variable
|
|
25
|
+
if (process.env.SALES_PORTAL_ENABLED === 'false') {
|
|
26
|
+
return {
|
|
27
|
+
authorized: false,
|
|
28
|
+
designer: null,
|
|
29
|
+
error: 'Sales portal is currently disabled'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extract credentials from request body
|
|
34
|
+
const { user, password, admin = false } = req.body;
|
|
35
|
+
|
|
36
|
+
// Check if credentials are provided
|
|
37
|
+
if (!user || !password) {
|
|
38
|
+
return {
|
|
39
|
+
authorized: false,
|
|
40
|
+
designer: null,
|
|
41
|
+
error: 'Email and password are required'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Query Sanity for designer account
|
|
47
|
+
const designer = await client.fetch(
|
|
48
|
+
`*[_type == "account" && email == $email && password == $password && isDesigner][0]`,
|
|
49
|
+
{ email: user, password }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Verify designer account exists
|
|
53
|
+
if (!designer) {
|
|
54
|
+
return {
|
|
55
|
+
authorized: false,
|
|
56
|
+
designer: null,
|
|
57
|
+
error: 'Invalid credentials'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Verify admin access if requested
|
|
62
|
+
if (admin && !designer.isAdmin) {
|
|
63
|
+
return {
|
|
64
|
+
authorized: false,
|
|
65
|
+
designer: null,
|
|
66
|
+
error: 'Admin privileges required'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Authentication successful
|
|
71
|
+
return {
|
|
72
|
+
authorized: true,
|
|
73
|
+
designer,
|
|
74
|
+
error: null
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Authentication error:', error);
|
|
78
|
+
return {
|
|
79
|
+
authorized: false,
|
|
80
|
+
designer: null,
|
|
81
|
+
error: 'Authentication service error'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for handling dates and time operations in sales data processing
|
|
3
|
+
*/
|
|
4
|
+
|
|
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
|
+
/**
|
|
17
|
+
* Creates a date range for a given month or specific range
|
|
18
|
+
* @param {Object} params - Date parameters
|
|
19
|
+
* @param {string|Date} params.date - Single date for month range
|
|
20
|
+
* @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
|
|
24
|
+
*/
|
|
25
|
+
export function createDateRange({ date, dateRange }) {
|
|
26
|
+
let startDate, endDate;
|
|
27
|
+
|
|
28
|
+
if (dateRange) {
|
|
29
|
+
startDate = adjustToUTC(new Date(dateRange.start));
|
|
30
|
+
endDate = adjustToUTC(new Date(dateRange.end));
|
|
31
|
+
} else {
|
|
32
|
+
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);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Set precise start/end times
|
|
40
|
+
startDate.setUTCHours(0, 0, 0, 0);
|
|
41
|
+
endDate.setUTCHours(23, 59, 59, 999);
|
|
42
|
+
|
|
43
|
+
return { startDate, endDate };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a date range suitable for Stripe API queries
|
|
48
|
+
* @param {Date} startDate - Range start date
|
|
49
|
+
* @param {Date} endDate - Range end date
|
|
50
|
+
* @returns {Object} Stripe-compatible date range
|
|
51
|
+
*/
|
|
52
|
+
export function createStripeTimeRange(startDate, endDate) {
|
|
53
|
+
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)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks if a timestamp falls within a date range
|
|
61
|
+
* @param {number} timestamp - Unix timestamp to check
|
|
62
|
+
* @param {Date} startDate - Range start date
|
|
63
|
+
* @param {Date} endDate - Range end date
|
|
64
|
+
* @returns {boolean} Whether timestamp is in range
|
|
65
|
+
*/
|
|
66
|
+
export function isInDateRange(timestamp, startDate, endDate) {
|
|
67
|
+
const date = timestamp * 1000; // Convert to milliseconds
|
|
68
|
+
return date >= startDate.getTime() && date <= endDate.getTime();
|
|
69
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for fee calculations
|
|
3
|
+
* Fixed to handle transaction fees correctly for multiple line items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculates total transaction fees for a charge (first pass)
|
|
8
|
+
* @param {Object} charge - Stripe charge object
|
|
9
|
+
* @param {boolean} isRefund - Whether this is a refund transaction
|
|
10
|
+
* @returns {number} Total transaction fees
|
|
11
|
+
*/
|
|
12
|
+
function calculateTotalTransactionFees(charge, isRefund) {
|
|
13
|
+
// Don't calculate transaction fees for refunds
|
|
14
|
+
if (isRefund) return 0;
|
|
15
|
+
|
|
16
|
+
// Calculate regular Stripe fees
|
|
17
|
+
let stripeFees = 0;
|
|
18
|
+
if (charge?.balance_transaction?.fee_details) {
|
|
19
|
+
stripeFees = charge.balance_transaction.fee_details.reduce((total, fee) => {
|
|
20
|
+
if (fee.type === 'stripe_fee') {
|
|
21
|
+
return total + fee.amount;
|
|
22
|
+
}
|
|
23
|
+
return total;
|
|
24
|
+
}, 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return stripeFees;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculates all fees for a charge
|
|
32
|
+
* @param {Object} params - Calculation parameters
|
|
33
|
+
* @param {Object} params.charge - Stripe charge object
|
|
34
|
+
* @param {number} params.amount - Amount for this item
|
|
35
|
+
* @param {number} params.total - Total amount of the charge
|
|
36
|
+
* @param {boolean} params.isPrimaryItem - Whether this is the primary/first item that should calculate the total fee
|
|
37
|
+
* @param {boolean} params.isRefund - Whether this item is a refund
|
|
38
|
+
* @param {number} [params.totalFee] - Pre-calculated total fee (provided for non-primary items)
|
|
39
|
+
* @returns {Object} Calculated fees and amounts
|
|
40
|
+
*/
|
|
41
|
+
export function calculateFees({ charge, amount, total, isPrimaryItem = false, isRefund = false, totalFee = null }) {
|
|
42
|
+
// For invoice items, amount often includes the raw amount without considering discounts
|
|
43
|
+
// Additionally, the total might not account for all discounts applied
|
|
44
|
+
// This is especially problematic for refunded items where the ratio calculation is critical
|
|
45
|
+
|
|
46
|
+
// Calculate the effective amount and total that properly accounts for discounts
|
|
47
|
+
// For refunded items, we need to ensure we're using the pre-discount values for consistent ratio calculation
|
|
48
|
+
const effectiveAmount = amount;
|
|
49
|
+
const effectiveTotal = total;
|
|
50
|
+
const ratio = effectiveTotal !== 0 ? effectiveAmount / effectiveTotal : 0;
|
|
51
|
+
const dispute = charge?.dispute;
|
|
52
|
+
|
|
53
|
+
// Calculate dispute amounts
|
|
54
|
+
const disputeAmount = (dispute?.amount || 0) * ratio;
|
|
55
|
+
|
|
56
|
+
// Get dispute fees from balance transactions
|
|
57
|
+
let disputeFees = 0;
|
|
58
|
+
|
|
59
|
+
// Get fees from dispute balance transactions - only if there's an actual dispute
|
|
60
|
+
if (dispute?.balance_transactions?.length) {
|
|
61
|
+
// For disputes, we still calculate the full fee amount
|
|
62
|
+
// because these are charged per-dispute, not per-transaction
|
|
63
|
+
disputeFees = dispute.balance_transactions.reduce((total, transaction) => {
|
|
64
|
+
// The dispute fee is directly in the fee field
|
|
65
|
+
return total + (transaction.fee || 0);
|
|
66
|
+
}, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Calculate or retrieve transaction fees
|
|
70
|
+
let stripeFees = 0;
|
|
71
|
+
|
|
72
|
+
if (isRefund) {
|
|
73
|
+
// No transaction fees for refunds
|
|
74
|
+
stripeFees = 0;
|
|
75
|
+
} else if (isPrimaryItem) {
|
|
76
|
+
// For primary item, calculate the total fee but only assign its proportional part
|
|
77
|
+
const totalStripeFee = calculateTotalTransactionFees(charge, isRefund);
|
|
78
|
+
// Apply only this item's proportion of the fee
|
|
79
|
+
stripeFees = Math.round(totalStripeFee * ratio);
|
|
80
|
+
// Return the full fee as well for distribution to other items
|
|
81
|
+
return {
|
|
82
|
+
disputed: !!charge?.disputed,
|
|
83
|
+
disputeAmount,
|
|
84
|
+
disputeFees,
|
|
85
|
+
stripeFees, // This item's portion
|
|
86
|
+
totalFees: stripeFees, // This item's portion of fees
|
|
87
|
+
totalTransactionFee: totalStripeFee, // The full transaction fee to distribute
|
|
88
|
+
disputeDetails: dispute ? {
|
|
89
|
+
amount: disputeAmount,
|
|
90
|
+
fees: disputeFees,
|
|
91
|
+
status: dispute.status,
|
|
92
|
+
reason: dispute.reason,
|
|
93
|
+
evidence: dispute.evidence,
|
|
94
|
+
created: dispute.created,
|
|
95
|
+
balanceTransactions: dispute.balance_transactions,
|
|
96
|
+
charge: dispute.charge
|
|
97
|
+
} : null
|
|
98
|
+
};
|
|
99
|
+
} else if (totalFee !== null) {
|
|
100
|
+
// For non-primary items, distribute the fee proportionally
|
|
101
|
+
stripeFees = Math.round(totalFee * ratio);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// For disputed charges, add dispute fees only
|
|
105
|
+
// Regular transaction fees are distributed proportionally for non-refund items
|
|
106
|
+
const totalFees = dispute ? disputeFees : stripeFees;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
disputed: !!charge?.disputed,
|
|
110
|
+
disputeAmount,
|
|
111
|
+
disputeFees,
|
|
112
|
+
stripeFees,
|
|
113
|
+
totalFees,
|
|
114
|
+
disputeDetails: dispute ? {
|
|
115
|
+
amount: disputeAmount,
|
|
116
|
+
fees: disputeFees,
|
|
117
|
+
status: dispute.status,
|
|
118
|
+
reason: dispute.reason,
|
|
119
|
+
evidence: dispute.evidence,
|
|
120
|
+
created: dispute.created,
|
|
121
|
+
balanceTransactions: dispute.balance_transactions,
|
|
122
|
+
charge: dispute.charge
|
|
123
|
+
} : null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Maps refund data to a consistent format
|
|
129
|
+
* @param {Array} refunds - Array of Stripe refund objects
|
|
130
|
+
* @param {number} amount - Amount for this item
|
|
131
|
+
* @param {number} total - Total amount
|
|
132
|
+
* @returns {Array} Formatted refund data
|
|
133
|
+
*/
|
|
134
|
+
export function formatRefunds(refunds, amount, total) {
|
|
135
|
+
if (!refunds?.length) return [];
|
|
136
|
+
|
|
137
|
+
const ratio = amount / total;
|
|
138
|
+
return refunds.map(refund => ({
|
|
139
|
+
total: refund.amount,
|
|
140
|
+
created: refund.created,
|
|
141
|
+
id: refund.id,
|
|
142
|
+
description: refund.reason,
|
|
143
|
+
status: refund.status,
|
|
144
|
+
percentOf_total: ratio,
|
|
145
|
+
adjustedTotal: refund.amount * ratio,
|
|
146
|
+
balance_transaction: refund.balance_transaction
|
|
147
|
+
}));
|
|
148
|
+
}
|