@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,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processor for Stripe payment intent data
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { calculateFees, formatRefunds } from '../feeCalculator';
|
|
6
|
+
|
|
7
|
+
const debuggingAccounts = ["colby@liiift.studio", "quinn@liiift.studio", "qkeave@gmail.com", "quinn@quitetype.com"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Finds a matching product based on price and creation time
|
|
11
|
+
* @param {Object} payment - Payment intent
|
|
12
|
+
* @param {Object} item - Payment item
|
|
13
|
+
* @param {Array} sanitySales - Sanity orders to search
|
|
14
|
+
* @returns {string} Product description or null
|
|
15
|
+
*/
|
|
16
|
+
function findMatchingProduct(payment, item, sanitySales) {
|
|
17
|
+
if (!payment?.amount || !item?.amount || item.amount <= 0) return null;
|
|
18
|
+
|
|
19
|
+
// Look for orders with similar price and creation time
|
|
20
|
+
const matchingOrders = sanitySales.filter(order => {
|
|
21
|
+
if (!order?._createdAt) return false;
|
|
22
|
+
|
|
23
|
+
const orderTime = new Date(order._createdAt).getTime() / 1000;
|
|
24
|
+
const paymentTime = payment.created;
|
|
25
|
+
const timeDiff = Math.abs(orderTime - paymentTime);
|
|
26
|
+
const dayInSeconds = 24 * 60 * 60;
|
|
27
|
+
|
|
28
|
+
// Match if:
|
|
29
|
+
// 1. Price matches exactly
|
|
30
|
+
// 2. Created within 2 days of each other
|
|
31
|
+
return Math.abs(order.cost * 100 - item.amount) < 1 && // Compare in cents with 1 cent tolerance
|
|
32
|
+
timeDiff < (2 * dayInSeconds); // Within 2 days
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (matchingOrders.length > 0) {
|
|
36
|
+
// Get the order closest in time
|
|
37
|
+
const closestOrder = matchingOrders.reduce((closest, current) => {
|
|
38
|
+
const closestTime = new Date(closest._createdAt).getTime() / 1000;
|
|
39
|
+
const currentTime = new Date(current._createdAt).getTime() / 1000;
|
|
40
|
+
const closestDiff = Math.abs(closestTime - payment.created);
|
|
41
|
+
const currentDiff = Math.abs(currentTime - payment.created);
|
|
42
|
+
return currentDiff < closestDiff ? current : closest;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Generate description from typefaces
|
|
46
|
+
if (closestOrder?.typefaces?.length) {
|
|
47
|
+
return closestOrder.typefaces.map(typeface =>
|
|
48
|
+
`${typeface?.typeface?.title?.toUpperCase()} (${(typeface?.fonts?.length || 0) + 1} styles)`
|
|
49
|
+
).join(', ');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a new order object from payment intent data
|
|
58
|
+
* @param {Object} payment - Payment intent
|
|
59
|
+
* @param {Object} item - Payment item
|
|
60
|
+
* @returns {Object} Created order object
|
|
61
|
+
*/
|
|
62
|
+
function createOrderFromPayment(payment, item) {
|
|
63
|
+
const email = payment.customer?.email || payment.latest_charge?.billing_details?.email || '';
|
|
64
|
+
const orderNumber = payment.metadata?.orderNumber || payment.id || '';
|
|
65
|
+
|
|
66
|
+
// Create empty address structure
|
|
67
|
+
const emptyAddress = {
|
|
68
|
+
city: "",
|
|
69
|
+
postalCode: "",
|
|
70
|
+
email: "",
|
|
71
|
+
address: "",
|
|
72
|
+
phone: "",
|
|
73
|
+
state: "",
|
|
74
|
+
country: "",
|
|
75
|
+
name: "",
|
|
76
|
+
address2: ""
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Get billing address from payment
|
|
80
|
+
const billingAddress = {
|
|
81
|
+
...emptyAddress,
|
|
82
|
+
address: payment.latest_charge?.billing_details?.address?.line1 || "",
|
|
83
|
+
address2: payment.latest_charge?.billing_details?.address?.line2 || "",
|
|
84
|
+
postalCode: payment.latest_charge?.billing_details?.address?.postal_code || "",
|
|
85
|
+
email: email,
|
|
86
|
+
name: payment.latest_charge?.billing_details?.name || "",
|
|
87
|
+
country: payment.latest_charge?.billing_details?.address?.country || "",
|
|
88
|
+
city: payment.latest_charge?.billing_details?.address?.city || "",
|
|
89
|
+
phone: payment.latest_charge?.billing_details?.phone || "",
|
|
90
|
+
state: payment.latest_charge?.billing_details?.address?.state || ""
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Create order object with minimal required fields
|
|
94
|
+
return {
|
|
95
|
+
_type: "order",
|
|
96
|
+
_id: orderNumber,
|
|
97
|
+
title: email,
|
|
98
|
+
orderNumber,
|
|
99
|
+
cost: payment.amount / 100,
|
|
100
|
+
_createdAt: new Date(payment.created * 1000).toISOString(),
|
|
101
|
+
_updatedAt: new Date(payment.created * 1000).toISOString(),
|
|
102
|
+
orderSuccess: true,
|
|
103
|
+
billingAddress,
|
|
104
|
+
orderStatus: {
|
|
105
|
+
status: "verified",
|
|
106
|
+
paymentIntentId: payment.id,
|
|
107
|
+
orderNumber: orderNumber
|
|
108
|
+
},
|
|
109
|
+
typefaces: [{
|
|
110
|
+
typeface: {
|
|
111
|
+
title: item.description || "Unknown Typeface"
|
|
112
|
+
}
|
|
113
|
+
}],
|
|
114
|
+
slug: {
|
|
115
|
+
current: `${email}-${orderNumber}`,
|
|
116
|
+
_type: "slug"
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Finds matching order and typeface for a payment item
|
|
123
|
+
* @param {Object} params - Search parameters
|
|
124
|
+
* @param {Object} params.payment - Payment to match
|
|
125
|
+
* @param {Object} [params.item] - Payment item (optional)
|
|
126
|
+
* @param {Array} params.sanitySales - Sanity orders to search
|
|
127
|
+
* @returns {Object} Matching order and typeface
|
|
128
|
+
*/
|
|
129
|
+
function findMatches({ payment, item = {}, sanitySales = [] }) {
|
|
130
|
+
if (!payment || !Array.isArray(sanitySales)) {
|
|
131
|
+
return { associatedOrder: null, associatedTypeface: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Find matching order
|
|
135
|
+
let associatedOrder = sanitySales.find(order => {
|
|
136
|
+
if (!order?.orderStatus) return false;
|
|
137
|
+
|
|
138
|
+
const isPaymentMatch = order.orderStatus.paymentIntentId === payment.id;
|
|
139
|
+
const isTimeMatch = payment?.customer?.email &&
|
|
140
|
+
order._createdAt &&
|
|
141
|
+
Math.abs(new Date(order._createdAt) - new Date(payment.created * 1000)) < (1000 * 60 * 60);
|
|
142
|
+
|
|
143
|
+
return isPaymentMatch || isTimeMatch;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// If no order found, create one from payment data
|
|
147
|
+
if (!associatedOrder && item) {
|
|
148
|
+
associatedOrder = createOrderFromPayment(payment, item);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fix description for unknown items before finding typeface
|
|
152
|
+
if (item?.description === 'Unknown item' && associatedOrder?.typefaces?.length) {
|
|
153
|
+
item.description = associatedOrder.typefaces.map(typeface =>
|
|
154
|
+
`${typeface?.typeface?.title} ${typeface?.fonts?.length ? `(${(typeface?.fonts?.length) + 1} styles)` : ``}`
|
|
155
|
+
).join(', ');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Find matching typeface
|
|
159
|
+
let associatedTypeface = null;
|
|
160
|
+
if (associatedOrder?.typefaces?.length) {
|
|
161
|
+
try {
|
|
162
|
+
const sortedTypefaces = [...associatedOrder.typefaces]
|
|
163
|
+
.filter(t => t?.typeface?.title)
|
|
164
|
+
.sort((a, b) => b.typeface.title.length - a.typeface.title.length);
|
|
165
|
+
|
|
166
|
+
if (item?.description) {
|
|
167
|
+
const matchingTypeface = sortedTypefaces.find(typeface =>
|
|
168
|
+
item.description.toLowerCase().includes(typeface.typeface.title.toLowerCase())
|
|
169
|
+
);
|
|
170
|
+
associatedTypeface = matchingTypeface?.typeface || null;
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.warn('Error finding matching typeface:', error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { associatedOrder, associatedTypeface };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Processes a single item from a payment intent
|
|
182
|
+
* @param {Object} params - Processing parameters
|
|
183
|
+
* @param {Object} params.item - Payment item
|
|
184
|
+
* @param {Object} params.payment - Parent payment intent
|
|
185
|
+
* @param {Object} params.associatedOrder - Matching Sanity order if found
|
|
186
|
+
* @param {Object} params.associatedTypeface - Matching typeface if found
|
|
187
|
+
* @param {number} params.itemIndex - Index of item in payment
|
|
188
|
+
* @param {number} params.totalItems - Total number of items in payment
|
|
189
|
+
* @param {boolean} params.isPrimaryItem - Whether this is the first/primary item that gets transaction fees
|
|
190
|
+
* @param {number} [params.totalFee] - Total transaction fee for the payment (for non-primary items)
|
|
191
|
+
* @returns {Object} Processed payment item data
|
|
192
|
+
*/
|
|
193
|
+
function processPaymentItem({ item = {}, payment, associatedOrder, associatedTypeface, itemIndex, totalItems, sanitySales, isPrimaryItem = false, totalFee = null }) {
|
|
194
|
+
// Check if this is a refund - we'll consider it a refund if it has a negative amount
|
|
195
|
+
// or if the payment status indicates it's a refund
|
|
196
|
+
const isRefund = (item.amount || 0) < 0 || payment.status === 'refunded';
|
|
197
|
+
|
|
198
|
+
// Calculate proper amounts that account for discounts
|
|
199
|
+
const discountAmount = payment.metadata?.discount ? Number(payment.metadata.discount) * 100 : 0; // Convert to cents
|
|
200
|
+
const itemDiscountPortion = discountAmount * ((item.amount || 0) / payment.amount);
|
|
201
|
+
const itemAmountAdjusted = (item.amount || 0) - itemDiscountPortion;
|
|
202
|
+
const paymentTotalAdjusted = payment.amount - discountAmount;
|
|
203
|
+
|
|
204
|
+
// Calculate fees and dispute amounts using discount-adjusted values
|
|
205
|
+
const result = calculateFees({
|
|
206
|
+
charge: payment.latest_charge,
|
|
207
|
+
amount: itemAmountAdjusted, // Use discount-adjusted amount
|
|
208
|
+
total: paymentTotalAdjusted, // Use discount-adjusted total
|
|
209
|
+
isPrimaryItem,
|
|
210
|
+
isRefund,
|
|
211
|
+
totalFee
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const { disputed, disputeAmount, totalFees, disputeDetails, totalTransactionFee } = result;
|
|
215
|
+
|
|
216
|
+
const itemTaxAmount = (associatedOrder?.tax || 0) / totalItems;
|
|
217
|
+
const itemTotal = (item.amount || 0) - itemTaxAmount - disputeAmount;
|
|
218
|
+
const itemTotalWithTax = (item.amount || 0) - disputeAmount;
|
|
219
|
+
|
|
220
|
+
// Try to find matching product if no description
|
|
221
|
+
let description = item.description;
|
|
222
|
+
if (!description && payment.saleType === "paymentIntent" && item.amount > 0) {
|
|
223
|
+
description = findMatchingProduct(payment, item, sanitySales);
|
|
224
|
+
}
|
|
225
|
+
console.log("description", description);
|
|
226
|
+
console.log("payment", payment);
|
|
227
|
+
return {
|
|
228
|
+
totalWithTax: itemTotalWithTax,
|
|
229
|
+
total: itemTotal,
|
|
230
|
+
invoiceTotal: payment.amount,
|
|
231
|
+
invoiceTotalWithTax: payment.amount,
|
|
232
|
+
discountAmounts: [],
|
|
233
|
+
stripeFees: totalFees,
|
|
234
|
+
disputed,
|
|
235
|
+
disputeDetails,
|
|
236
|
+
taxAmounts: [{
|
|
237
|
+
amount: itemTaxAmount,
|
|
238
|
+
inclusive: true,
|
|
239
|
+
tax_rate: null,
|
|
240
|
+
taxability_reason: null,
|
|
241
|
+
taxable_amount: item.amount
|
|
242
|
+
}],
|
|
243
|
+
refunds: formatRefunds(
|
|
244
|
+
payment.latest_charge?.refunds?.data,
|
|
245
|
+
item.amount || 0,
|
|
246
|
+
payment.amount
|
|
247
|
+
),
|
|
248
|
+
description: description || payment.metadata?.product_description || 'Unknown item',
|
|
249
|
+
discounts: payment.metadata?.discount ? [{
|
|
250
|
+
amount: payment.metadata.discount,
|
|
251
|
+
description: 'Discount applied'
|
|
252
|
+
}] : [],
|
|
253
|
+
lineId: `${payment.id}-${itemIndex}`,
|
|
254
|
+
created: payment.created,
|
|
255
|
+
id: payment.id,
|
|
256
|
+
invoicePdf: payment?.latest_charge?.receipt_url || null,
|
|
257
|
+
number: payment?.latest_charge?.receipt_number || null,
|
|
258
|
+
customer: payment.customer,
|
|
259
|
+
testSale: debuggingAccounts.includes(payment?.customer?.email) ||
|
|
260
|
+
debuggingAccounts.includes(payment?.shipping?.address?.email) ||
|
|
261
|
+
debuggingAccounts.includes(payment?.latest_charge?.billing_details?.email) ||
|
|
262
|
+
debuggingAccounts.includes(associatedOrder?.billingAddress?.email) ||
|
|
263
|
+
debuggingAccounts.includes(associatedOrder?.shippingAddress?.email) ||
|
|
264
|
+
debuggingAccounts.includes(associatedOrder?.licenseeAddress?.email),
|
|
265
|
+
customerAddress: payment.shipping?.address || payment.latest_charge?.billing_details?.address || null,
|
|
266
|
+
// Payment method data
|
|
267
|
+
paymentMethod: {
|
|
268
|
+
type: payment?.payment_method?.type,
|
|
269
|
+
card: payment?.payment_method?.card,
|
|
270
|
+
origin: payment?.payment_method?.card?.country ? {
|
|
271
|
+
country: payment?.payment_method?.card?.country
|
|
272
|
+
} : null
|
|
273
|
+
},
|
|
274
|
+
order: associatedOrder || null,
|
|
275
|
+
orderNumber: associatedOrder?.orderNumber || null,
|
|
276
|
+
author: associatedTypeface?.author || null,
|
|
277
|
+
typeface: associatedTypeface || null,
|
|
278
|
+
saleType: "paymentIntent"
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Processes shipping costs from a payment intent
|
|
284
|
+
* @param {Object} params - Processing parameters
|
|
285
|
+
* @param {Object} params.payment - Parent payment intent
|
|
286
|
+
* @param {Object} params.associatedOrder - Matching Sanity order if found
|
|
287
|
+
* @param {number} [params.totalFee] - Total transaction fee for the payment (for proportional distribution)
|
|
288
|
+
* @returns {Object} Processed shipping data
|
|
289
|
+
*/
|
|
290
|
+
function processShippingCost({ payment, associatedOrder, totalFee = null }) {
|
|
291
|
+
if (!payment?.shipping_cost) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check if this is a refund shipping cost
|
|
296
|
+
const isRefund = payment.shipping_cost < 0 || payment.status === 'refunded';
|
|
297
|
+
|
|
298
|
+
// Shipping should get proportional fee allocation
|
|
299
|
+
const { disputed, disputeAmount, totalFees, disputeDetails } = calculateFees({
|
|
300
|
+
charge: payment.latest_charge,
|
|
301
|
+
amount: payment.shipping_cost,
|
|
302
|
+
total: payment.amount,
|
|
303
|
+
isPrimaryItem: false, // Shipping never gets the primary transaction fee
|
|
304
|
+
isRefund,
|
|
305
|
+
totalFee // Use the total fee for proportional distribution
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
totalWithTax: payment.shipping_cost - disputeAmount,
|
|
310
|
+
total: payment.shipping_cost - disputeAmount,
|
|
311
|
+
invoiceTotal: payment.amount,
|
|
312
|
+
invoiceTotalWithTax: payment.amount,
|
|
313
|
+
discountAmounts: [],
|
|
314
|
+
stripeFees: totalFees,
|
|
315
|
+
disputed,
|
|
316
|
+
disputeDetails,
|
|
317
|
+
taxAmounts: [],
|
|
318
|
+
refunds: formatRefunds(
|
|
319
|
+
payment.latest_charge?.refunds?.data,
|
|
320
|
+
payment.shipping_cost,
|
|
321
|
+
payment.amount
|
|
322
|
+
),
|
|
323
|
+
shippingProvision: true,
|
|
324
|
+
description: `Shipping for ${associatedOrder?.orderNumber || "?"} : ${payment.id}`,
|
|
325
|
+
discounts: [],
|
|
326
|
+
lineId: `${payment.id}-shipping`,
|
|
327
|
+
created: payment.created,
|
|
328
|
+
id: payment.id,
|
|
329
|
+
invoicePdf: null,
|
|
330
|
+
number: null,
|
|
331
|
+
customer: payment.customer,
|
|
332
|
+
testSale: debuggingAccounts.includes(payment?.customer?.email) ||
|
|
333
|
+
debuggingAccounts.includes(payment?.shipping?.address?.email) ||
|
|
334
|
+
debuggingAccounts.includes(payment?.latest_charge?.billing_details?.email) ||
|
|
335
|
+
debuggingAccounts.includes(associatedOrder?.billingAddress?.email) ||
|
|
336
|
+
debuggingAccounts.includes(associatedOrder?.shippingAddress?.email) ||
|
|
337
|
+
debuggingAccounts.includes(associatedOrder?.licenseeAddress?.email),
|
|
338
|
+
customerAddress: payment.shipping?.address || payment.latest_charge?.billing_details?.address || null,
|
|
339
|
+
// Payment method data
|
|
340
|
+
paymentMethod: {
|
|
341
|
+
type: payment?.payment_method?.type,
|
|
342
|
+
card: payment?.payment_method?.card,
|
|
343
|
+
origin: payment?.payment_method?.card?.country ? {
|
|
344
|
+
country: payment?.payment_method?.card?.country
|
|
345
|
+
} : null
|
|
346
|
+
},
|
|
347
|
+
order: associatedOrder || null,
|
|
348
|
+
orderNumber: associatedOrder?.orderNumber || null,
|
|
349
|
+
author: null,
|
|
350
|
+
typeface: null,
|
|
351
|
+
saleType: "paymentIntent"
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Processes a payment intent and its items
|
|
357
|
+
* @param {Object} params - Processing parameters
|
|
358
|
+
* @param {Object} params.payment - Payment intent to process
|
|
359
|
+
* @param {Array} params.sanitySales - Sanity orders to match against
|
|
360
|
+
* @returns {Array} Processed sales data
|
|
361
|
+
*/
|
|
362
|
+
export function processPaymentIntent({ payment, sanitySales = [] }) {
|
|
363
|
+
if (!payment?.amount) {
|
|
364
|
+
console.warn('Invalid payment intent received:', payment);
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Make sure we're using the payment's ID consistently and extract payment method data
|
|
369
|
+
let paymentMethodData = null;
|
|
370
|
+
|
|
371
|
+
if (payment.id && typeof payment.id === 'object' && payment.id.id) {
|
|
372
|
+
console.log(`Payment intent ID is an object, extracting ID: ${payment.id.id}`);
|
|
373
|
+
payment.id = payment.id.id;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Extract and store payment method data
|
|
377
|
+
if (payment.payment_method) {
|
|
378
|
+
paymentMethodData = payment.payment_method;
|
|
379
|
+
console.log(`Payment intent ${payment.id} payment method data:`,
|
|
380
|
+
JSON.stringify({
|
|
381
|
+
type: paymentMethodData.type,
|
|
382
|
+
card_country: paymentMethodData.card?.country
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log(`Processing payment intent: ${payment.id}`);
|
|
387
|
+
|
|
388
|
+
const sales = [];
|
|
389
|
+
let totalStripeFee = null;
|
|
390
|
+
|
|
391
|
+
// Parse items from metadata or create default item
|
|
392
|
+
let items;
|
|
393
|
+
try {
|
|
394
|
+
items = payment.metadata?.items ? JSON.parse(payment.metadata.items) : null;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.warn(`Failed to parse payment items for ${payment.id}:`, error);
|
|
397
|
+
items = null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Validate parsed items
|
|
401
|
+
if (!Array.isArray(items) || items.length === 0 || !items.every(item => item && typeof item === 'object')) {
|
|
402
|
+
items = [{
|
|
403
|
+
description: payment.description || 'Unknown item',
|
|
404
|
+
amount: payment.amount
|
|
405
|
+
}];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Process each item
|
|
409
|
+
items.forEach((item, index) => {
|
|
410
|
+
try {
|
|
411
|
+
const { associatedOrder, associatedTypeface } = findMatches({ payment, item, sanitySales });
|
|
412
|
+
// Check if this is a refund item
|
|
413
|
+
const isRefund = (item.amount || 0) < 0 || payment.status === 'refunded';
|
|
414
|
+
|
|
415
|
+
// Calculate parameters for fee calculation
|
|
416
|
+
const itemParams = {
|
|
417
|
+
item,
|
|
418
|
+
payment,
|
|
419
|
+
associatedOrder,
|
|
420
|
+
associatedTypeface,
|
|
421
|
+
itemIndex: index,
|
|
422
|
+
totalItems: items.length,
|
|
423
|
+
sanitySales,
|
|
424
|
+
isPrimaryItem: index === 0, // First item calculates the total fee
|
|
425
|
+
isRefund
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// For the first non-refund item, calculate the total fee
|
|
429
|
+
if (index === 0 && !isRefund) {
|
|
430
|
+
const processedItem = processPaymentItem(itemParams);
|
|
431
|
+
// Store the total transaction fee for distribution
|
|
432
|
+
totalStripeFee = processedItem.totalTransactionFee || processedItem.stripeFees;
|
|
433
|
+
sales.push(processedItem);
|
|
434
|
+
} else {
|
|
435
|
+
// For subsequent items, pass the total fee for proportional distribution
|
|
436
|
+
const processedItem = processPaymentItem({
|
|
437
|
+
...itemParams,
|
|
438
|
+
totalFee: totalStripeFee
|
|
439
|
+
});
|
|
440
|
+
sales.push(processedItem);
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error(`Error processing payment item ${index} for payment ${payment.id}:`, error);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Process shipping if present
|
|
448
|
+
try {
|
|
449
|
+
const shippingCost = processShippingCost({
|
|
450
|
+
payment,
|
|
451
|
+
associatedOrder: findMatches({ payment, sanitySales }).associatedOrder,
|
|
452
|
+
totalFee: totalStripeFee // Pass the total fee for proportional distribution
|
|
453
|
+
});
|
|
454
|
+
if (shippingCost) {
|
|
455
|
+
sales.push(shippingCost);
|
|
456
|
+
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error(`Error processing shipping cost for payment ${payment.id}:`, error);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return sales;
|
|
462
|
+
}
|