@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.
Files changed (52) hide show
  1. package/README.md +461 -0
  2. package/SETUP.md +230 -0
  3. package/api/getAnalytics.d.ts +38 -0
  4. package/api/getAnalytics.js +346 -0
  5. package/api/getBalanceTransactions.d.ts +29 -0
  6. package/api/getBalanceTransactions.js +125 -0
  7. package/api/getDesignerInfo.d.ts +37 -0
  8. package/api/getDesignerInfo.js +98 -0
  9. package/api/getDesigners.d.ts +28 -0
  10. package/api/getDesigners.js +63 -0
  11. package/api/getPreviousSales.d.ts +23 -0
  12. package/api/getPreviousSales.js +82 -0
  13. package/api/getSales.d.ts +29 -0
  14. package/api/getSales.js +50 -0
  15. package/api/getSalesRange.d.ts +23 -0
  16. package/api/getSalesRange.js +58 -0
  17. package/api/utils/authMiddleware.js +84 -0
  18. package/api/utils/dateUtils.js +69 -0
  19. package/api/utils/feeCalculator.js +148 -0
  20. package/api/utils/processors/invoiceProcessor.js +337 -0
  21. package/api/utils/processors/paymentProcessor.js +462 -0
  22. package/api/utils/salesDataProcessing.js +596 -0
  23. package/api/utils/salesDataProcessor.js +224 -0
  24. package/api/utils/stripeFetcher.js +248 -0
  25. package/components/DateRangeSalesTable.js +1072 -0
  26. package/components/DebugValues.js +48 -0
  27. package/components/LicenseTypeList.js +193 -0
  28. package/components/LoginForm.js +219 -0
  29. package/components/PeriodComparison.js +501 -0
  30. package/components/Sales.js +773 -0
  31. package/components/SalesChart.js +307 -0
  32. package/components/SalesPortalPage.js +147 -0
  33. package/components/SalesTable.js +677 -0
  34. package/components/SummaryCards.js +345 -0
  35. package/components/TopPerformers.js +331 -0
  36. package/components/TypefaceList.js +154 -0
  37. package/components/table-columns.js +70 -0
  38. package/components/table-row-cells.js +295 -0
  39. package/data/countryCode.json +318 -0
  40. package/hooks/useSalesDateQuery.d.ts +20 -0
  41. package/hooks/useSalesDateQuery.js +71 -0
  42. package/index.d.ts +172 -0
  43. package/index.js +33 -0
  44. package/package.json +87 -0
  45. package/styles/sales-portal.module.scss +383 -0
  46. package/styles/sales-portal.theme.d.ts +5 -0
  47. package/styles/sales-portal.theme.js +799 -0
  48. package/utils/currencyUtils.d.ts +20 -0
  49. package/utils/currencyUtils.js +79 -0
  50. package/utils/salesDataProcessing.d.ts +44 -0
  51. package/utils/salesDataProcessing.js +596 -0
  52. 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;
@@ -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
+ }