@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,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processor for Stripe invoice data
|
|
3
|
+
* Fixed to handle fee calculations correctly for items with discounts and refunds
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { calculateFees, formatRefunds } from '../feeCalculator';
|
|
7
|
+
|
|
8
|
+
const debuggingAccounts = ["colby@liiift.studio", "quinn@liiift.studio", "qkeave@gmail.com", "quinn@quitetype.com"];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Finds matching order and typeface for an invoice line
|
|
12
|
+
* @param {Object} params - Search parameters
|
|
13
|
+
* @param {Object} params.invoice - Invoice to match
|
|
14
|
+
* @param {Object} params.line - Invoice line item
|
|
15
|
+
* @param {Array} params.sanitySales - Sanity orders to search
|
|
16
|
+
* @returns {Object} Matching order and typeface
|
|
17
|
+
*/
|
|
18
|
+
function findMatches({ invoice, line = {}, sanitySales = [] }) {
|
|
19
|
+
// Find matching order
|
|
20
|
+
const associatedOrder =
|
|
21
|
+
(invoice?.id && sanitySales.find(order => order.orderStatus.invoiceId === invoice.id)) ||
|
|
22
|
+
(invoice?.payment_intent && sanitySales.find(order => order.orderStatus.paymentIntentId === invoice.payment_intent)) ||
|
|
23
|
+
(invoice?.customer?.email && sanitySales.find(order => {
|
|
24
|
+
const orderDate = new Date(order._createdAt);
|
|
25
|
+
const invoiceDate = new Date(invoice.created * 1000);
|
|
26
|
+
return (
|
|
27
|
+
order.title === invoice.customer.email &&
|
|
28
|
+
Math.abs(orderDate - invoiceDate) < (1000 * 60 * 60)
|
|
29
|
+
);
|
|
30
|
+
})) || null;
|
|
31
|
+
|
|
32
|
+
// Fix description for unknown items before finding typeface
|
|
33
|
+
if (line?.description === 'Unknown item') {
|
|
34
|
+
line.description = associatedOrder?.typefaces?.map(typeface =>
|
|
35
|
+
`${typeface?.typeface?.title?.toUpperCase()} (${(typeface?.fonts?.length || 0) + 1} styles)`
|
|
36
|
+
).join(', ') || line.description;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Find matching typeface
|
|
40
|
+
let associatedTypeface = null;
|
|
41
|
+
if (associatedOrder?.typefaces?.length) {
|
|
42
|
+
const sortedTypefaces = [...associatedOrder.typefaces]
|
|
43
|
+
.filter(t => t?.typeface?.title)
|
|
44
|
+
.sort((a, b) => b.typeface.title.length - a.typeface.title.length);
|
|
45
|
+
|
|
46
|
+
const matchingTypeface = sortedTypefaces.find(typeface =>
|
|
47
|
+
line?.description?.toLowerCase().includes(typeface.typeface.title.toLowerCase())
|
|
48
|
+
);
|
|
49
|
+
associatedTypeface = matchingTypeface?.typeface || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { associatedOrder, associatedTypeface };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Processes a single line item from an invoice
|
|
57
|
+
* @param {Object} params - Processing parameters
|
|
58
|
+
* @param {Object} params.line - Invoice line item
|
|
59
|
+
* @param {Object} params.invoice - Parent invoice
|
|
60
|
+
* @param {Object} params.associatedOrder - Matching Sanity order if found
|
|
61
|
+
* @param {Object} params.associatedTypeface - Matching typeface if found
|
|
62
|
+
* @param {boolean} params.isPrimaryItem - Whether this is the first/primary item in the invoice
|
|
63
|
+
* @param {number} [params.totalFee] - Total transaction fee for the invoice (for non-primary items)
|
|
64
|
+
* @returns {Object} Processed line item data
|
|
65
|
+
*/
|
|
66
|
+
function processLineItem({ line, invoice, associatedOrder, associatedTypeface, isPrimaryItem = false, totalFee = null, paymentMethodData = null }) {
|
|
67
|
+
// Check if this is a refund - we'll consider it a refund if it has a negative amount
|
|
68
|
+
const isRefund = line.amount < 0;
|
|
69
|
+
|
|
70
|
+
// Calculate discount-adjusted amounts
|
|
71
|
+
const lineTotalDiscounted = line.discount_amounts.reduce((acc, discount) => acc + discount.amount, 0);
|
|
72
|
+
const lineAmount = line.amount_excluding_tax - lineTotalDiscounted;
|
|
73
|
+
const invoiceTotal = invoice.total_excluding_tax;
|
|
74
|
+
|
|
75
|
+
// Calculate fees and dispute amounts using discount-adjusted values
|
|
76
|
+
const result = calculateFees({
|
|
77
|
+
charge: invoice.charge,
|
|
78
|
+
amount: lineAmount, // Use discount-adjusted amount
|
|
79
|
+
total: invoiceTotal, // Use discount-adjusted total
|
|
80
|
+
isPrimaryItem,
|
|
81
|
+
isRefund,
|
|
82
|
+
totalFee
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const { disputed, disputeAmount, totalFees, disputeDetails, totalTransactionFee } = result;
|
|
86
|
+
|
|
87
|
+
// Calculate final totals
|
|
88
|
+
const lineTotal = lineAmount; // Pre-tax total with discounts applied
|
|
89
|
+
const lineTotalWithTax = (line.amount || 0) - (disputeAmount || 0) - lineTotalDiscounted + line.tax_amounts.reduce((acc, tax) => acc + tax.amount, 0);
|
|
90
|
+
|
|
91
|
+
if (associatedOrder?.orderNumber === "201028") {
|
|
92
|
+
console.log("lineTotal", lineTotal);
|
|
93
|
+
console.log("lineTotalWithTax", lineTotalWithTax);
|
|
94
|
+
console.log("invoice.total_excluding_tax", invoice.total_excluding_tax);
|
|
95
|
+
console.log("line", line);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log("invoice?.payment_intent?.payment_method", invoice?.payment_intent?.payment_method);
|
|
99
|
+
return {
|
|
100
|
+
// Quantities
|
|
101
|
+
totalWithTax: lineTotalWithTax,
|
|
102
|
+
total: lineTotal, // Pre-tax total
|
|
103
|
+
invoiceTotal: invoice.total_excluding_tax,
|
|
104
|
+
invoiceTotalWithTax: invoice.total,
|
|
105
|
+
discountAmounts: line.discount_amounts,
|
|
106
|
+
taxAmounts: line.tax_amounts,
|
|
107
|
+
stripeFees: totalFees,
|
|
108
|
+
disputed,
|
|
109
|
+
disputeDetails,
|
|
110
|
+
|
|
111
|
+
// Refunds
|
|
112
|
+
refunds: formatRefunds(
|
|
113
|
+
invoice.charge?.refunds?.data,
|
|
114
|
+
lineTotal,
|
|
115
|
+
invoice.total_excluding_tax
|
|
116
|
+
),
|
|
117
|
+
|
|
118
|
+
// Other properties
|
|
119
|
+
description: line.description,
|
|
120
|
+
discounts: !!line?.discounts.length ? line?.discounts : invoice?.discounts,
|
|
121
|
+
lineId: line.id,
|
|
122
|
+
created: invoice.created,
|
|
123
|
+
id: invoice.id,
|
|
124
|
+
invoicePdf: invoice.invoice_pdf,
|
|
125
|
+
number: invoice.number,
|
|
126
|
+
customer: invoice?.customer,
|
|
127
|
+
testSale: debuggingAccounts.includes(invoice?.customer?.email) ||
|
|
128
|
+
debuggingAccounts.includes(invoice?.customer_shipping?.email) ||
|
|
129
|
+
debuggingAccounts.includes(invoice?.customer_address?.email) ||
|
|
130
|
+
debuggingAccounts.includes(associatedOrder?.billingAddress?.email) ||
|
|
131
|
+
debuggingAccounts.includes(associatedOrder?.shippingAddress?.email) ||
|
|
132
|
+
debuggingAccounts.includes(associatedOrder?.licenseeAddress?.email),
|
|
133
|
+
customerAddress: {
|
|
134
|
+
postal_code: invoice?.customer_address?.postal_code,
|
|
135
|
+
country: invoice?.customer_address?.country,
|
|
136
|
+
},
|
|
137
|
+
// Payment method data - use the invoice.payment_method (stored from payment_intent) if available
|
|
138
|
+
paymentMethod: {
|
|
139
|
+
type: paymentMethodData?.type || invoice?.payment_method?.type || invoice?.payment_intent?.payment_method?.type,
|
|
140
|
+
card: paymentMethodData?.card || invoice?.payment_method?.card || invoice?.payment_intent?.payment_method?.card,
|
|
141
|
+
origin: (paymentMethodData?.card?.country || invoice?.payment_method?.card?.country || invoice?.payment_intent?.payment_method?.card?.country) ? {
|
|
142
|
+
country: paymentMethodData?.card?.country || invoice?.payment_method?.card?.country || invoice?.payment_intent?.payment_method?.card?.country
|
|
143
|
+
} : null
|
|
144
|
+
},
|
|
145
|
+
order: associatedOrder || null,
|
|
146
|
+
orderNumber: associatedOrder?.orderNumber || null,
|
|
147
|
+
author: associatedTypeface?.author || null,
|
|
148
|
+
typeface: associatedTypeface || null,
|
|
149
|
+
saleType: "invoice"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Processes shipping costs from an invoice
|
|
155
|
+
* @param {Object} params - Processing parameters
|
|
156
|
+
* @param {Object} params.invoice - Parent invoice
|
|
157
|
+
* @param {Object} params.associatedOrder - Matching Sanity order if found
|
|
158
|
+
* @param {number} [params.totalFee] - Total transaction fee for the invoice (for proportional distribution)
|
|
159
|
+
* @returns {Object} Processed shipping data
|
|
160
|
+
*/
|
|
161
|
+
function processShippingCost({ invoice, associatedOrder, totalFee = null, paymentMethodData = null }) {
|
|
162
|
+
if (!invoice?.shipping_cost?.amount_total) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check if this is a refund shipping cost
|
|
167
|
+
const isRefund = invoice.shipping_cost.amount_total < 0;
|
|
168
|
+
|
|
169
|
+
// Calculate fees and dispute amounts - shipping should get proportional fee
|
|
170
|
+
const { disputed, disputeAmount, totalFees, disputeDetails } = calculateFees({
|
|
171
|
+
charge: invoice.charge,
|
|
172
|
+
amount: invoice.shipping_cost.amount_total,
|
|
173
|
+
total: invoice.total,
|
|
174
|
+
isPrimaryItem: false, // Shipping never gets the primary transaction fee
|
|
175
|
+
isRefund,
|
|
176
|
+
totalFee // Use the total fee for proportional distribution
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
// Quantities
|
|
181
|
+
totalWithTax: invoice.shipping_cost.amount_total - disputeAmount,
|
|
182
|
+
total: invoice.shipping_cost.amount_subtotal, // Pre-tax total
|
|
183
|
+
invoiceTotal: invoice.total_excluding_tax,
|
|
184
|
+
invoiceTotalWithTax: invoice.total,
|
|
185
|
+
discountAmounts: null,
|
|
186
|
+
stripeFees: totalFees,
|
|
187
|
+
disputed,
|
|
188
|
+
disputeDetails,
|
|
189
|
+
taxAmounts: [{
|
|
190
|
+
amount: invoice.shipping_cost.amount_tax,
|
|
191
|
+
inclusive: null,
|
|
192
|
+
tax_rate: null,
|
|
193
|
+
taxability_reason: null,
|
|
194
|
+
taxable_amount: invoice.shipping_cost.amount_subtotal
|
|
195
|
+
}],
|
|
196
|
+
|
|
197
|
+
// Refunds
|
|
198
|
+
refunds: formatRefunds(
|
|
199
|
+
invoice.charge?.refunds?.data,
|
|
200
|
+
invoice.shipping_cost.amount_subtotal,
|
|
201
|
+
invoice.total_excluding_tax
|
|
202
|
+
),
|
|
203
|
+
|
|
204
|
+
// Other properties
|
|
205
|
+
shippingProvision: true,
|
|
206
|
+
description: `Shipping for ${associatedOrder?.orderNumber || "?"} : ${invoice?.id}`,
|
|
207
|
+
discounts: null,
|
|
208
|
+
lineId: invoice.shipping_cost.shipping_rate,
|
|
209
|
+
created: invoice.created,
|
|
210
|
+
id: invoice.id,
|
|
211
|
+
invoicePdf: invoice.invoice_pdf,
|
|
212
|
+
number: invoice.number,
|
|
213
|
+
customer: invoice?.customer,
|
|
214
|
+
testSale: debuggingAccounts.includes(invoice?.customer?.email) ||
|
|
215
|
+
debuggingAccounts.includes(invoice?.customer_shipping?.email) ||
|
|
216
|
+
debuggingAccounts.includes(invoice?.customer_address?.email) ||
|
|
217
|
+
debuggingAccounts.includes(associatedOrder?.billingAddress?.email) ||
|
|
218
|
+
debuggingAccounts.includes(associatedOrder?.shippingAddress?.email) ||
|
|
219
|
+
debuggingAccounts.includes(associatedOrder?.licenseeAddress?.email),
|
|
220
|
+
customerAddress: {
|
|
221
|
+
postal_code: invoice?.customer_address?.postal_code,
|
|
222
|
+
country: invoice?.customer_address?.country,
|
|
223
|
+
},
|
|
224
|
+
// Payment method data - use the invoice.payment_method (stored from payment_intent) if available
|
|
225
|
+
paymentMethod: {
|
|
226
|
+
type: paymentMethodData?.type || invoice?.payment_method?.type || invoice?.payment_intent?.payment_method?.type,
|
|
227
|
+
card: paymentMethodData?.card || invoice?.payment_method?.card || invoice?.payment_intent?.payment_method?.card,
|
|
228
|
+
origin: (paymentMethodData?.card?.country || invoice?.payment_method?.card?.country || invoice?.payment_intent?.payment_method?.card?.country) ? {
|
|
229
|
+
country: paymentMethodData?.card?.country || invoice?.payment_method?.card?.country || invoice?.payment_intent?.payment_method?.card?.country
|
|
230
|
+
} : null
|
|
231
|
+
},
|
|
232
|
+
order: associatedOrder || null,
|
|
233
|
+
orderNumber: associatedOrder?.orderNumber || null,
|
|
234
|
+
author: null,
|
|
235
|
+
typeface: null,
|
|
236
|
+
saleType: "invoice"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Processes an invoice and its line items
|
|
242
|
+
* @param {Object} params - Processing parameters
|
|
243
|
+
* @param {Object} params.invoice - Invoice to process
|
|
244
|
+
* @param {Array} params.sanitySales - Sanity orders to match against
|
|
245
|
+
* @param {Set} params.processedPaymentIntents - Set of already processed payment intents
|
|
246
|
+
* @returns {Array} Processed sales data
|
|
247
|
+
*/
|
|
248
|
+
export function processInvoice({ invoice, sanitySales, processedPaymentIntents }) {
|
|
249
|
+
// Skip if payment intent already processed
|
|
250
|
+
let paymentIntentId = null;
|
|
251
|
+
|
|
252
|
+
// Handle payment_intent which could be a string ID or an object with an id property
|
|
253
|
+
let paymentMethodData = null;
|
|
254
|
+
|
|
255
|
+
if (invoice.payment_intent) {
|
|
256
|
+
if (typeof invoice.payment_intent === 'string') {
|
|
257
|
+
paymentIntentId = invoice.payment_intent;
|
|
258
|
+
} else if (invoice.payment_intent && invoice.payment_intent.id) {
|
|
259
|
+
// Extract payment method data before potentially using just the ID
|
|
260
|
+
if (invoice.payment_intent.payment_method) {
|
|
261
|
+
paymentMethodData = invoice.payment_intent.payment_method;
|
|
262
|
+
}
|
|
263
|
+
paymentIntentId = invoice.payment_intent.id;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Store payment method information for debugging
|
|
268
|
+
console.log(`Invoice ${invoice.id} payment method data:`, paymentMethodData ?
|
|
269
|
+
JSON.stringify({
|
|
270
|
+
type: paymentMethodData.type,
|
|
271
|
+
card_country: paymentMethodData.card?.country
|
|
272
|
+
}) : 'null');
|
|
273
|
+
|
|
274
|
+
// Skip if this payment intent was already processed
|
|
275
|
+
if (paymentIntentId && processedPaymentIntents.has(paymentIntentId)) {
|
|
276
|
+
console.log(`Invoice ${invoice.id} with payment intent ${paymentIntentId} already processed - skipping`);
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Track this payment intent
|
|
281
|
+
if (paymentIntentId) {
|
|
282
|
+
console.log(`Adding payment intent ${paymentIntentId} from invoice ${invoice.id} to tracked set`);
|
|
283
|
+
processedPaymentIntents.add(paymentIntentId);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const sales = [];
|
|
287
|
+
let totalStripeFee = null;
|
|
288
|
+
|
|
289
|
+
// Process each line item
|
|
290
|
+
invoice.lines.data.forEach((line, index) => {
|
|
291
|
+
const { associatedOrder, associatedTypeface } = findMatches({ invoice, line, sanitySales });
|
|
292
|
+
|
|
293
|
+
// Check if this is a refund item
|
|
294
|
+
const isRefund = line.amount < 0;
|
|
295
|
+
|
|
296
|
+
// Calculate parameters for fee calculation
|
|
297
|
+
const feeParams = {
|
|
298
|
+
line,
|
|
299
|
+
invoice,
|
|
300
|
+
associatedOrder,
|
|
301
|
+
associatedTypeface,
|
|
302
|
+
isPrimaryItem: index === 0, // First item calculates the total fee
|
|
303
|
+
isRefund,
|
|
304
|
+
paymentMethodData
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// For the first non-refund item, calculate the total fee
|
|
308
|
+
if (index === 0 && !isRefund) {
|
|
309
|
+
const processedLine = processLineItem(feeParams);
|
|
310
|
+
// Store the total transaction fee for distribution
|
|
311
|
+
totalStripeFee = processedLine.totalTransactionFee || processedLine.stripeFees;
|
|
312
|
+
sales.push(processedLine);
|
|
313
|
+
} else {
|
|
314
|
+
// For subsequent items, pass the total fee for proportional distribution
|
|
315
|
+
const processedLine = processLineItem({
|
|
316
|
+
...feeParams,
|
|
317
|
+
totalFee: totalStripeFee
|
|
318
|
+
});
|
|
319
|
+
sales.push(processedLine);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Process shipping if present
|
|
324
|
+
if (invoice?.shipping_cost?.amount_total) {
|
|
325
|
+
const shippingCost = processShippingCost({
|
|
326
|
+
invoice,
|
|
327
|
+
associatedOrder: findMatches({ invoice, sanitySales }).associatedOrder,
|
|
328
|
+
totalFee: totalStripeFee, // Pass the total fee for proportional distribution
|
|
329
|
+
paymentMethodData // Pass the payment method data
|
|
330
|
+
});
|
|
331
|
+
if (shippingCost) {
|
|
332
|
+
sales.push(shippingCost);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return sales;
|
|
337
|
+
}
|