@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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main processor for sales data, coordinating Stripe and Sanity data processing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createClient } from '@sanity/client';
|
|
6
|
+
import { createDateRange, createStripeTimeRange, isInDateRange } from './dateUtils';
|
|
7
|
+
import { fetchStripeData } from './stripeFetcher';
|
|
8
|
+
import { processInvoice } from './processors/invoiceProcessor';
|
|
9
|
+
import { processPaymentIntent } from './processors/paymentProcessor';
|
|
10
|
+
|
|
11
|
+
// Initialize Sanity client
|
|
12
|
+
const client = createClient({
|
|
13
|
+
projectId: process.env.SANITY_STUDIO_PROJECT_ID,
|
|
14
|
+
dataset: process.env.SANITY_STUDIO_DATASET,
|
|
15
|
+
apiVersion: '2022-04-01',
|
|
16
|
+
token: process.env.SANITY_STUDIO_TOKEN,
|
|
17
|
+
useCdn: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Authenticates a designer using Sanity credentials
|
|
22
|
+
* @param {string} email - Designer's email
|
|
23
|
+
* @param {string} password - Designer's password
|
|
24
|
+
* @returns {Promise<Object>} Authenticated designer data
|
|
25
|
+
*/
|
|
26
|
+
export async function authenticateDesigner(email, password) {
|
|
27
|
+
try {
|
|
28
|
+
const designer = await client.fetch(
|
|
29
|
+
`*[_type == "account" && email == $email && password == $password && isDesigner][0]`,
|
|
30
|
+
{ email, password }
|
|
31
|
+
);
|
|
32
|
+
return designer || null;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Authentication error:', error);
|
|
35
|
+
throw new Error('Failed to authenticate designer');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetches Sanity orders for a date range
|
|
41
|
+
* @param {Date} startDate - Range start date
|
|
42
|
+
* @param {Date} endDate - Range end date
|
|
43
|
+
* @returns {Promise<Array>} Array of Sanity orders
|
|
44
|
+
*/
|
|
45
|
+
async function fetchSanityOrders(startDate, endDate) {
|
|
46
|
+
try {
|
|
47
|
+
return await client.fetch(
|
|
48
|
+
`*[_type == "order" && _createdAt >= $startOfMonth && _createdAt <= $endOfMonth]{
|
|
49
|
+
...,
|
|
50
|
+
discount->,
|
|
51
|
+
typefaces[]{
|
|
52
|
+
...,
|
|
53
|
+
typeface->{
|
|
54
|
+
_id,
|
|
55
|
+
title,
|
|
56
|
+
author->{
|
|
57
|
+
_id,
|
|
58
|
+
firstName,
|
|
59
|
+
lastName,
|
|
60
|
+
email
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}`,
|
|
65
|
+
{
|
|
66
|
+
startOfMonth: startDate.toISOString(),
|
|
67
|
+
endOfMonth: endDate.toISOString()
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error fetching Sanity orders:', error);
|
|
72
|
+
throw new Error('Failed to fetch Sanity orders');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Processes all sales data for a given date range and designer
|
|
78
|
+
* @param {Object} params - Processing parameters
|
|
79
|
+
* @param {string} params.date - Single date for month range
|
|
80
|
+
* @param {Object} [params.dateRange] - Specific date range
|
|
81
|
+
* @param {Object} params.designer - Authenticated designer data
|
|
82
|
+
* @param {boolean} [params.admin] - Whether to include all sales (admin only)
|
|
83
|
+
* @returns {Promise<Array>} Processed sales data
|
|
84
|
+
*/
|
|
85
|
+
export async function processSalesData({ date, dateRange, designer, admin = false }) {
|
|
86
|
+
try {
|
|
87
|
+
// Create date ranges
|
|
88
|
+
const { startDate, endDate } = createDateRange({ date, dateRange });
|
|
89
|
+
const stripeTimeRange = createStripeTimeRange(startDate, endDate);
|
|
90
|
+
|
|
91
|
+
// Fetch data from both sources
|
|
92
|
+
const [sanitySales, stripeData] = await Promise.all([
|
|
93
|
+
fetchSanityOrders(startDate, endDate),
|
|
94
|
+
fetchStripeData(stripeTimeRange, {
|
|
95
|
+
filterByDesigner: !designer.isAdmin || !admin,
|
|
96
|
+
designerId: designer._id
|
|
97
|
+
})
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// Process all sales data
|
|
101
|
+
const processedPaymentIntents = new Set();
|
|
102
|
+
const allSales = [];
|
|
103
|
+
|
|
104
|
+
// Process invoices
|
|
105
|
+
console.log("Processing invoices...");
|
|
106
|
+
stripeData.invoices.forEach(invoice => {
|
|
107
|
+
if (
|
|
108
|
+
(!designer.isAdmin || !admin) &&
|
|
109
|
+
!invoice?.metadata?.authors?.includes(designer._id)
|
|
110
|
+
) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Log payment intent before processing
|
|
115
|
+
if (invoice.payment_intent) {
|
|
116
|
+
console.log(`Invoice ${invoice.id} has payment_intent: ${typeof invoice.payment_intent === 'string' ? invoice.payment_intent : 'Object'}`);
|
|
117
|
+
|
|
118
|
+
// Check if it's an object or string
|
|
119
|
+
if (typeof invoice.payment_intent !== 'string') {
|
|
120
|
+
console.log(`Payment intent is an object: ${JSON.stringify(invoice.payment_intent)}`);
|
|
121
|
+
// If it's an object, ensure we're using the ID for tracking
|
|
122
|
+
if (invoice.payment_intent.id) {
|
|
123
|
+
invoice.payment_method = invoice.payment_intent.payment_method;
|
|
124
|
+
invoice.payment_intent = invoice.payment_intent.id;
|
|
125
|
+
console.log(`Updated to use payment intent ID: ${invoice.payment_intent}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const sales = processInvoice({
|
|
131
|
+
invoice,
|
|
132
|
+
sanitySales,
|
|
133
|
+
processedPaymentIntents
|
|
134
|
+
});
|
|
135
|
+
allSales.push(...sales);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(`After processing invoices, tracked payment intents: ${Array.from(processedPaymentIntents).join(', ')}`);
|
|
139
|
+
|
|
140
|
+
// Process payment intents that don't have associated invoices
|
|
141
|
+
console.log("Processing payment intents...");
|
|
142
|
+
stripeData.payments.forEach(payment => {
|
|
143
|
+
console.log(`Checking payment intent: ${payment.id}`);
|
|
144
|
+
|
|
145
|
+
if (processedPaymentIntents.has(payment.id)) {
|
|
146
|
+
console.log(`Skipping payment intent ${payment.id} - already processed via invoice`);
|
|
147
|
+
return; // Skip if already processed via invoice
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
(!designer.isAdmin || !admin) &&
|
|
152
|
+
!payment?.metadata?.authors?.includes(designer._id)
|
|
153
|
+
) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const sales = processPaymentIntent({
|
|
158
|
+
payment,
|
|
159
|
+
sanitySales
|
|
160
|
+
});
|
|
161
|
+
allSales.push(...sales);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Filter by exact date range
|
|
165
|
+
const filteredByDate = allSales.filter(sale =>
|
|
166
|
+
isInDateRange(sale.created, startDate, endDate)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Filter out duplicate transactions with the same Stripe data within an hour
|
|
170
|
+
const seen = new Map(); // Map<key, Array<timestamp>>
|
|
171
|
+
const filteredSales = filteredByDate.filter(sale => {
|
|
172
|
+
// Always keep sales over $0, refunds and shipping provisions
|
|
173
|
+
if ((sale.refunds && sale.refunds.length > 0) || sale.shippingProvision || sale.total > 0) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Create a comprehensive key from the sale data
|
|
178
|
+
const key = JSON.stringify({
|
|
179
|
+
customerEmail: sale.customer?.email,
|
|
180
|
+
description: sale.description,
|
|
181
|
+
total: sale.total,
|
|
182
|
+
orderNumber: sale.orderNumber,
|
|
183
|
+
// Include more fields for better duplicate detection
|
|
184
|
+
taxAmount: sale.taxAmounts?.reduce((sum, tax) => sum + (tax.amount || 0), 0),
|
|
185
|
+
discountAmount: sale.discountAmounts?.reduce((sum, discount) => sum + (discount.amount || 0), 0),
|
|
186
|
+
// Additional deduplication for payment methods
|
|
187
|
+
saleType: sale.saleType, // 'invoice' or 'paymentIntent'
|
|
188
|
+
id: sale.id // The Stripe ID (either invoice ID or payment intent ID)
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Get the current timestamp
|
|
192
|
+
const timestamp = sale.created;
|
|
193
|
+
|
|
194
|
+
// Check if we've seen this key before
|
|
195
|
+
if (seen.has(key)) {
|
|
196
|
+
|
|
197
|
+
const timestamps = seen.get(key);
|
|
198
|
+
|
|
199
|
+
// Use a time window (24 hours)
|
|
200
|
+
const timeWindowInSeconds = 24 * 60 * 60;
|
|
201
|
+
const isDuplicate = timestamps.some(ts =>
|
|
202
|
+
Math.abs(ts - timestamp) < timeWindowInSeconds
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (isDuplicate) return false; // Skip this duplicate
|
|
206
|
+
|
|
207
|
+
// Not a duplicate within an hour, add this timestamp to the array
|
|
208
|
+
timestamps.push(timestamp);
|
|
209
|
+
} else {
|
|
210
|
+
// First time seeing this key, initialize with this timestamp
|
|
211
|
+
seen.set(key, [timestamp]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Sort by creation date (newest first)
|
|
218
|
+
return filteredSales.sort((a, b) => b.created - a.created);
|
|
219
|
+
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error('Error processing sales data:', error);
|
|
222
|
+
throw new Error(`Failed to process sales data: ${error.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for fetching data from Stripe API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fetches dispute details including all balance transactions
|
|
9
|
+
* @param {Object} dispute - Stripe dispute object
|
|
10
|
+
* @returns {Promise<Object>} Enhanced dispute object with all transactions
|
|
11
|
+
*/
|
|
12
|
+
async function fetchDisputeDetails(dispute) {
|
|
13
|
+
if (!dispute) return null;
|
|
14
|
+
|
|
15
|
+
// Get dispute with balance transactions
|
|
16
|
+
const fullDispute = await stripe.disputes.retrieve(dispute.id, {
|
|
17
|
+
expand: ['balance_transactions']
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// If there's a dispute charge, get its balance transaction
|
|
21
|
+
if (fullDispute.charge) {
|
|
22
|
+
const disputeCharge = await stripe.charges.retrieve(fullDispute.charge, {
|
|
23
|
+
expand: ['balance_transaction']
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Get all balance transactions for this dispute
|
|
27
|
+
const balanceTransactions = await stripe.balanceTransactions.list({
|
|
28
|
+
type: 'dispute_fee',
|
|
29
|
+
source: disputeCharge.id
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Add balance transactions to the dispute object
|
|
33
|
+
fullDispute.charge = disputeCharge;
|
|
34
|
+
fullDispute.balance_transactions = [
|
|
35
|
+
...(fullDispute.balance_transactions || []),
|
|
36
|
+
...balanceTransactions.data
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return fullDispute;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetches all pages of Stripe invoices for a given date range
|
|
45
|
+
* @param {Object} timeRange - Stripe-compatible date range
|
|
46
|
+
* @param {Object} options - Additional fetch options
|
|
47
|
+
* @param {boolean} [options.filterByDesigner] - Whether to filter by designer
|
|
48
|
+
* @param {string} [options.designerId] - Designer ID to filter by
|
|
49
|
+
* @returns {Promise<Array>} Array of all fetched invoices
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchAllInvoices(timeRange, options = {}) {
|
|
52
|
+
const fetchOptions = {
|
|
53
|
+
created: timeRange,
|
|
54
|
+
status: 'paid',
|
|
55
|
+
limit: 100,
|
|
56
|
+
expand: [
|
|
57
|
+
'data.charge',
|
|
58
|
+
'data.charge.refunds',
|
|
59
|
+
'data.charge.balance_transaction',
|
|
60
|
+
'data.charge.dispute',
|
|
61
|
+
'data.customer',
|
|
62
|
+
'data.payment_intent',
|
|
63
|
+
'data.payment_intent.payment_method'
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let allInvoices = [];
|
|
68
|
+
let hasMore = true;
|
|
69
|
+
let lastId = null;
|
|
70
|
+
let pageCount = 0;
|
|
71
|
+
|
|
72
|
+
while (hasMore) {
|
|
73
|
+
try {
|
|
74
|
+
const fetchParams = { ...fetchOptions };
|
|
75
|
+
if (lastId) {
|
|
76
|
+
fetchParams.starting_after = lastId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(`Fetching invoice page ${pageCount + 1}...`);
|
|
80
|
+
const response = await stripe.invoices.list(fetchParams);
|
|
81
|
+
|
|
82
|
+
if (response.data.length > 0) {
|
|
83
|
+
// Filter by designer if needed
|
|
84
|
+
const invoices = options.filterByDesigner && options.designerId
|
|
85
|
+
? response.data.filter(invoice => invoice?.metadata?.authors?.includes(options.designerId))
|
|
86
|
+
: response.data;
|
|
87
|
+
|
|
88
|
+
// Log the expansion of payment_intent for debugging
|
|
89
|
+
if (response.data.length > 0) {
|
|
90
|
+
const sampleInvoice = response.data[0];
|
|
91
|
+
console.log('Sample invoice payment_intent type:', typeof sampleInvoice.payment_intent);
|
|
92
|
+
console.log('Sample invoice payment_intent:', sampleInvoice.payment_intent );
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// For each invoice with a dispute, fetch additional details
|
|
96
|
+
const processedInvoices = await Promise.all(invoices.map(async invoice => {
|
|
97
|
+
if (invoice.charge?.disputed) {
|
|
98
|
+
invoice.charge.dispute = await fetchDisputeDetails(invoice.charge.dispute);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// For each refund, fetch its balance transaction
|
|
102
|
+
if (invoice.charge?.refunds?.data?.length) {
|
|
103
|
+
const refundsWithTransactions = await Promise.all(
|
|
104
|
+
invoice.charge.refunds.data.map(async refund => {
|
|
105
|
+
const fullRefund = await stripe.refunds.retrieve(refund.id, {
|
|
106
|
+
expand: ['balance_transaction']
|
|
107
|
+
});
|
|
108
|
+
return fullRefund;
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
invoice.charge.refunds.data = refundsWithTransactions;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If payment_intent is a string, try to fetch the full object
|
|
115
|
+
if (invoice.payment_intent && typeof invoice.payment_intent === 'string') {
|
|
116
|
+
try {
|
|
117
|
+
const paymentIntent = await stripe.paymentIntents.retrieve(invoice.payment_intent, {
|
|
118
|
+
expand: ['payment_method']
|
|
119
|
+
});
|
|
120
|
+
invoice.payment_intent = paymentIntent;
|
|
121
|
+
console.log(`Expanded payment intent ${paymentIntent.id} for invoice ${invoice.id}`);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.warn(`Could not expand payment intent for invoice ${invoice.id}:`, error.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return invoice;
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
allInvoices = allInvoices.concat(processedInvoices);
|
|
131
|
+
lastId = response.data[response.data.length - 1].id;
|
|
132
|
+
hasMore = response.has_more;
|
|
133
|
+
pageCount++;
|
|
134
|
+
|
|
135
|
+
} else {
|
|
136
|
+
hasMore = false;
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('Error fetching invoice page:', error);
|
|
140
|
+
throw new Error(`Failed to fetch invoice page ${pageCount + 1}: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return allInvoices;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fetches all pages of Stripe payment intents for a given date range
|
|
149
|
+
* @param {Object} timeRange - Stripe-compatible date range
|
|
150
|
+
* @param {Object} options - Additional fetch options
|
|
151
|
+
* @param {boolean} [options.filterByDesigner] - Whether to filter by designer
|
|
152
|
+
* @param {string} [options.designerId] - Designer ID to filter by
|
|
153
|
+
* @returns {Promise<Array>} Array of all fetched payment intents
|
|
154
|
+
*/
|
|
155
|
+
export async function fetchAllPaymentIntents(timeRange, options = {}) {
|
|
156
|
+
const fetchOptions = {
|
|
157
|
+
created: timeRange,
|
|
158
|
+
limit: 100,
|
|
159
|
+
expand: [
|
|
160
|
+
'data.latest_charge',
|
|
161
|
+
'data.latest_charge.balance_transaction',
|
|
162
|
+
'data.latest_charge.dispute',
|
|
163
|
+
'data.latest_charge.refunds',
|
|
164
|
+
'data.customer',
|
|
165
|
+
'data.payment_method'
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
let allPayments = [];
|
|
170
|
+
let hasMore = true;
|
|
171
|
+
let lastId = null;
|
|
172
|
+
let pageCount = 0;
|
|
173
|
+
|
|
174
|
+
while (hasMore) {
|
|
175
|
+
try {
|
|
176
|
+
const fetchParams = { ...fetchOptions };
|
|
177
|
+
if (lastId) {
|
|
178
|
+
fetchParams.starting_after = lastId;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(`Fetching payment intent page ${pageCount + 1}...`);
|
|
182
|
+
const response = await stripe.paymentIntents.list(fetchParams);
|
|
183
|
+
|
|
184
|
+
if (response.data.length > 0) {
|
|
185
|
+
// Filter succeeded payments and by designer if needed
|
|
186
|
+
const payments = response.data
|
|
187
|
+
.filter(pi => pi.status === 'succeeded')
|
|
188
|
+
.filter(pi => !options.filterByDesigner || !options.designerId ||
|
|
189
|
+
pi?.metadata?.authors?.includes(options.designerId));
|
|
190
|
+
|
|
191
|
+
// For each payment with a dispute, fetch additional details
|
|
192
|
+
const processedPayments = await Promise.all(payments.map(async payment => {
|
|
193
|
+
if (payment.latest_charge?.disputed) {
|
|
194
|
+
payment.latest_charge.dispute = await fetchDisputeDetails(payment.latest_charge.dispute);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// For each refund, fetch its balance transaction
|
|
198
|
+
if (payment.latest_charge?.refunds?.data?.length) {
|
|
199
|
+
const refundsWithTransactions = await Promise.all(
|
|
200
|
+
payment.latest_charge.refunds.data.map(async refund => {
|
|
201
|
+
const fullRefund = await stripe.refunds.retrieve(refund.id, {
|
|
202
|
+
expand: ['balance_transaction']
|
|
203
|
+
});
|
|
204
|
+
return fullRefund;
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
payment.latest_charge.refunds.data = refundsWithTransactions;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return payment;
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
allPayments = allPayments.concat(processedPayments);
|
|
214
|
+
lastId = response.data[response.data.length - 1].id;
|
|
215
|
+
hasMore = response.has_more;
|
|
216
|
+
pageCount++;
|
|
217
|
+
|
|
218
|
+
} else {
|
|
219
|
+
hasMore = false;
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('Error fetching payment intent page:', error);
|
|
223
|
+
throw new Error(`Failed to fetch payment intent page ${pageCount + 1}: ${error.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return allPayments;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Fetches both invoices and payment intents for a date range
|
|
232
|
+
* @param {Object} timeRange - Stripe-compatible date range
|
|
233
|
+
* @param {Object} options - Additional fetch options
|
|
234
|
+
* @returns {Promise<Object>} Object containing both invoices and payment intents
|
|
235
|
+
*/
|
|
236
|
+
export async function fetchStripeData(timeRange, options = {}) {
|
|
237
|
+
try {
|
|
238
|
+
const [invoices, payments] = await Promise.all([
|
|
239
|
+
fetchAllInvoices(timeRange, options),
|
|
240
|
+
fetchAllPaymentIntents(timeRange, options)
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
return { invoices, payments };
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('Error fetching Stripe data:', error);
|
|
246
|
+
throw new Error(`Failed to fetch Stripe data: ${error.message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|