@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,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
+ }