diff --git a/client/src/pages/Analytics.tsx b/client/src/pages/Analytics.tsx
index ec65cfd..42a2a99 100644
--- a/client/src/pages/Analytics.tsx
+++ b/client/src/pages/Analytics.tsx
@@ -1,7 +1,7 @@
import { useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '../store';
-import { calculateOrderPrintingCost, enrichOrderItemsWithCosts } from '../utils/orderCalculations';
+import { calculateOrderPrintingCost, enrichOrderItemsWithCosts, orderNetRevenue } from '../utils/orderCalculations';
import { formatAustralianDate } from '../utils/dateFormatter';
import { TrendingUp, DollarSign, Package, Users, ShoppingCart, Download, Printer } from 'lucide-react';
@@ -253,7 +253,9 @@ const Analytics = () => {
}, [expenses, dateRange]);
// Calculate metrics with filtered data
- const totalRevenue = filteredOrders.reduce((sum, order) => sum + (order?.total || 0), 0);
+ // Revenue is net of buyer refunds
+ const totalRevenue = filteredOrders.reduce((sum, order) => sum + orderNetRevenue(order), 0);
+ const totalRefunds = filteredOrders.reduce((sum, order) => sum + (order?.refundTotal || 0), 0);
const totalPrintingCosts = filteredOrders.reduce((sum, order) => {
return sum + getUpdatedPrintingCost(order);
}, 0);
@@ -313,7 +315,7 @@ const Analytics = () => {
const monthKey = `${months[monthIndex]} ${year}`;
const current = monthlyMap.get(monthKey);
if (current) {
- current.revenue += order.total || 0;
+ current.revenue += orderNetRevenue(order);
}
}
});
@@ -557,15 +559,18 @@ const Analytics = () => {
Total Revenue
${totalRevenue.toFixed(2)}
+
net of refunds
-
- 12.5%
- vs last period
+ {totalRefunds > 0 ? (
+ −${totalRefunds.toFixed(2)} refunded
+ ) : (
+ No refunds
+ )}
diff --git a/client/src/store/slices/orderSlice.ts b/client/src/store/slices/orderSlice.ts
index b583f1c..67dbb2f 100644
--- a/client/src/store/slices/orderSlice.ts
+++ b/client/src/store/slices/orderSlice.ts
@@ -4,7 +4,8 @@ export interface Order {
_id: string;
orderNumber: string;
total: number;
- status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
+ refundTotal?: number;
+ status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
dateOrdered: string;
customer?: {
name: string;
diff --git a/client/src/utils/orderCalculations.ts b/client/src/utils/orderCalculations.ts
index 16fe1a5..b23ce85 100644
--- a/client/src/utils/orderCalculations.ts
+++ b/client/src/utils/orderCalculations.ts
@@ -17,6 +17,10 @@ export interface Product {
sku?: string;
}
+// Net revenue kept on an order after buyer refunds (gross total - refunds)
+export const orderNetRevenue = (order: { total?: number; refundTotal?: number }): number =>
+ Math.max(0, (order.total || 0) - (order.refundTotal || 0));
+
// Calculate total printing cost for an order
export const calculateOrderPrintingCost = (items: OrderItem[]): number => {
return items.reduce((total, item) => {
diff --git a/client/src/utils/profitAnalysisService.ts b/client/src/utils/profitAnalysisService.ts
index e641619..d36bd98 100644
--- a/client/src/utils/profitAnalysisService.ts
+++ b/client/src/utils/profitAnalysisService.ts
@@ -1,10 +1,11 @@
import { Order } from '../store/slices/orderSlice';
import { Product } from '../store/slices/productSlice';
import { Expense } from '../store/slices/expenseSlice';
-import { calculateOrderPrintingCost, calculateOrderProfit, enrichOrderItemsWithCosts } from './orderCalculations';
+import { calculateOrderPrintingCost, calculateOrderProfit, enrichOrderItemsWithCosts, orderNetRevenue } from './orderCalculations';
export interface ProfitMetrics {
- totalRevenue: number;
+ totalRevenue: number; // Net of buyer refunds
+ totalRefunds: number;
totalPrintingCosts: number;
totalExpenses: number; // Added: All expenses excluding transaction fees
totalProfit: number;
@@ -75,6 +76,7 @@ export class ProfitAnalysisService {
if (!orders || orders.length === 0) {
return {
totalRevenue: 0,
+ totalRefunds: 0,
totalPrintingCosts: 0,
totalExpenses: 0,
totalProfit: 0,
@@ -87,18 +89,20 @@ export class ProfitAnalysisService {
}
let totalRevenue = 0;
+ let totalRefunds = 0;
let totalPrintingCosts = 0;
let profitableOrderCount = 0;
- // Calculate revenue and printing costs from orders
+ // Calculate revenue (net of refunds) and printing costs from orders
orders.forEach(order => {
- const revenue = order.total || 0;
+ const revenue = orderNetRevenue(order);
const enrichedItems = enrichOrderItemsWithCosts(order.items || [], products || []);
const printingCost = calculateOrderPrintingCost(enrichedItems);
-
+
totalRevenue += revenue;
+ totalRefunds += order.refundTotal || 0;
totalPrintingCosts += printingCost;
-
+
// Count profitable orders based on printing costs only (before expenses)
const orderProfit = calculateOrderProfit(enrichedItems, revenue);
if (orderProfit > 0) profitableOrderCount++;
@@ -118,6 +122,7 @@ export class ProfitAnalysisService {
return {
totalRevenue,
+ totalRefunds,
totalPrintingCosts,
totalExpenses,
totalProfit,
@@ -193,7 +198,7 @@ export class ProfitAnalysisService {
const monthKey = `${orderDate.getFullYear()}-${String(orderDate.getMonth() + 1).padStart(2, '0')}`;
const monthName = orderDate.toLocaleDateString('en-AU', { year: 'numeric', month: 'long' });
- const revenue = order.total || 0;
+ const revenue = orderNetRevenue(order);
const enrichedItems = enrichOrderItemsWithCosts(order.items || [], products || []);
const costs = calculateOrderPrintingCost(enrichedItems);
@@ -441,7 +446,7 @@ export class ProfitAnalysisService {
return orders.map(order => {
const enrichedItems = enrichOrderItemsWithCosts(order.items || [], products);
- const revenue = order.total || 0;
+ const revenue = orderNetRevenue(order);
const printingCosts = calculateOrderPrintingCost(enrichedItems);
const profit = calculateOrderProfit(enrichedItems, revenue);
const margin = revenue > 0 ? (profit / revenue) * 100 : 0;
diff --git a/server/src/models/Order.ts b/server/src/models/Order.ts
index d766d67..8e2fdad 100644
--- a/server/src/models/Order.ts
+++ b/server/src/models/Order.ts
@@ -20,6 +20,7 @@ export interface IOrder extends Document {
shipping: number;
tax: number;
total: number;
+ refundTotal: number;
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
shippingAddress?: {
@@ -69,6 +70,8 @@ const OrderSchema: Schema = new Schema({
shipping: { type: Number, default: 0 },
tax: { type: Number, default: 0 },
total: { type: Number, required: true, min: 0 },
+ // Amount refunded to the buyer; net revenue for the order is total - refundTotal
+ refundTotal: { type: Number, default: 0, min: 0 },
status: {
type: String,
enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'],
diff --git a/server/src/services/etsyApi.ts b/server/src/services/etsyApi.ts
index 73a6018..006fdf7 100644
--- a/server/src/services/etsyApi.ts
+++ b/server/src/services/etsyApi.ts
@@ -195,9 +195,15 @@ const matchProduct = (
return undefined;
};
-const mapStatus = (receipt: any): string => {
+// Sum all refunds on a receipt (each refund.amount is a Money object)
+const refundTotalOf = (receipt: any): number =>
+ (receipt.refunds || []).reduce((sum: number, r: any) => sum + money(r.amount), 0);
+
+const mapStatus = (receipt: any, refundTotal: number, total: number): string => {
const status = String(receipt.status || '').toLowerCase();
if (status === 'canceled') return 'cancelled';
+ // A refund covering (nearly) the whole order marks it refunded
+ if (refundTotal > 0 && refundTotal >= total - 0.01) return 'refunded';
if (status === 'completed') return 'delivered';
if (receipt.is_shipped) return 'shipped';
return 'processing';
@@ -280,15 +286,18 @@ const upsertOrderFromReceipt = async (
});
const shipment = (receipt.shipments || [])[0];
+ const total = money(receipt.grandtotal);
+ const refundTotal = refundTotalOf(receipt);
const orderData: any = {
orderNumber,
etsyOrderId: orderNumber,
- total: money(receipt.grandtotal),
+ total,
+ refundTotal,
subtotal: money(receipt.subtotal),
shipping: money(receipt.total_shipping_cost),
tax: money(receipt.total_tax_cost),
- status: mapStatus(receipt),
+ status: mapStatus(receipt, refundTotal, total),
paymentStatus: receipt.is_paid ? 'paid' : 'pending',
dateOrdered: new Date((receipt.created_timestamp || receipt.create_timestamp) * 1000),
customer: { name: receipt.name || 'Etsy Customer', email: '' },