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