Redemption rate part 3 + update cogs options
This commit is contained in:
@@ -165,6 +165,7 @@ router.post('/simulate', async (req, res) => {
|
|||||||
shippingTiers = [],
|
shippingTiers = [],
|
||||||
merchantFeePercent,
|
merchantFeePercent,
|
||||||
fixedCostPerOrder,
|
fixedCostPerOrder,
|
||||||
|
cogsCalculationMode = 'actual',
|
||||||
pointsConfig = {}
|
pointsConfig = {}
|
||||||
} = req.body || {};
|
} = req.body || {};
|
||||||
|
|
||||||
@@ -373,74 +374,25 @@ router.post('/simulate', async (req, res) => {
|
|||||||
|
|
||||||
const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE;
|
const pointDollarValue = config.points.pointDollarValue || DEFAULT_POINT_DOLLAR_VALUE;
|
||||||
|
|
||||||
// Calculate redemption rate using aggregated award vs redemption pairing per customer
|
// Calculate redemption rate using dollars redeemed from the matched order set
|
||||||
let calculatedRedemptionRate = 0;
|
let calculatedRedemptionRate = 0;
|
||||||
if (config.points.redemptionRate != null) {
|
if (config.points.redemptionRate != null) {
|
||||||
calculatedRedemptionRate = config.points.redemptionRate;
|
calculatedRedemptionRate = config.points.redemptionRate;
|
||||||
} else if (totals.pointsAwarded > 0 && pointDollarValue > 0) {
|
} else if (totals.pointsAwarded > 0 && pointDollarValue > 0) {
|
||||||
const extendedEndDt = DateTime.min(
|
const totalRedeemedPoints = totals.pointsRedeemed / pointDollarValue;
|
||||||
endDt.plus({ months: 12 }),
|
if (totalRedeemedPoints > 0) {
|
||||||
DateTime.now().endOf('day')
|
calculatedRedemptionRate = Math.min(1, totalRedeemedPoints / totals.pointsAwarded);
|
||||||
);
|
|
||||||
|
|
||||||
const redemptionStatsQuery = `
|
|
||||||
SELECT
|
|
||||||
SUM(awards.points_awarded) AS total_awarded_points,
|
|
||||||
SUM(
|
|
||||||
LEAST(
|
|
||||||
awards.points_awarded,
|
|
||||||
COALESCE(redemptions.redemption_amount, 0) / ?
|
|
||||||
)
|
|
||||||
) AS matched_redeemed_points
|
|
||||||
FROM (
|
|
||||||
SELECT
|
|
||||||
o.order_cid,
|
|
||||||
SUM(o.summary_points) AS points_awarded
|
|
||||||
FROM _order o
|
|
||||||
${promoJoin}
|
|
||||||
WHERE o.summary_shipping > 0
|
|
||||||
AND o.summary_total > 0
|
|
||||||
AND o.order_status NOT IN (15)
|
|
||||||
AND o.ship_method_selected <> 'holdit'
|
|
||||||
AND o.ship_country = ?
|
|
||||||
AND o.date_placed BETWEEN ? AND ?
|
|
||||||
${promoFilterClause}
|
|
||||||
GROUP BY o.order_cid
|
|
||||||
) AS awards
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT
|
|
||||||
o.order_cid,
|
|
||||||
SUM(od.discount_amount) AS redemption_amount
|
|
||||||
FROM order_discounts od
|
|
||||||
JOIN _order o ON od.order_id = o.order_id
|
|
||||||
WHERE od.discount_type = 20 AND od.discount_active = 1
|
|
||||||
AND o.order_status NOT IN (15)
|
|
||||||
AND o.ship_country = ?
|
|
||||||
AND o.date_placed BETWEEN ? AND ?
|
|
||||||
GROUP BY o.order_cid
|
|
||||||
) AS redemptions ON redemptions.order_cid = awards.order_cid
|
|
||||||
`;
|
|
||||||
|
|
||||||
const redemptionStatsParams = [
|
|
||||||
pointDollarValue,
|
|
||||||
...filteredOrdersParams,
|
|
||||||
shipCountry,
|
|
||||||
formatDateForSql(startDt),
|
|
||||||
formatDateForSql(extendedEndDt)
|
|
||||||
];
|
|
||||||
|
|
||||||
const [redemptionStatsRows] = await connection.execute(redemptionStatsQuery, redemptionStatsParams);
|
|
||||||
const redemptionStats = redemptionStatsRows[0] || {};
|
|
||||||
const totalAwardedPoints = Number(redemptionStats.total_awarded_points || 0);
|
|
||||||
const matchedRedeemedPoints = Number(redemptionStats.matched_redeemed_points || 0);
|
|
||||||
|
|
||||||
if (totalAwardedPoints > 0 && matchedRedeemedPoints > 0) {
|
|
||||||
calculatedRedemptionRate = Math.min(1, matchedRedeemedPoints / totalAwardedPoints);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const redemptionRate = calculatedRedemptionRate;
|
const redemptionRate = calculatedRedemptionRate;
|
||||||
|
|
||||||
|
// Calculate overall average COGS percentage for 'average' mode
|
||||||
|
let overallCogsPercentage = 0;
|
||||||
|
if (cogsCalculationMode === 'average' && totals.subtotal > 0) {
|
||||||
|
overallCogsPercentage = totals.cogs / totals.subtotal;
|
||||||
|
}
|
||||||
|
|
||||||
const bucketResults = [];
|
const bucketResults = [];
|
||||||
let weightedProfitAmount = 0;
|
let weightedProfitAmount = 0;
|
||||||
let weightedProfitPercent = 0;
|
let weightedProfitPercent = 0;
|
||||||
@@ -457,7 +409,16 @@ router.post('/simulate', async (req, res) => {
|
|||||||
const orderValue = data.avgSubtotal > 0 ? data.avgSubtotal : getMidpoint(range);
|
const orderValue = data.avgSubtotal > 0 ? data.avgSubtotal : getMidpoint(range);
|
||||||
const shippingChargeBase = data.avgShipRate > 0 ? data.avgShipRate : 0;
|
const shippingChargeBase = data.avgShipRate > 0 ? data.avgShipRate : 0;
|
||||||
const actualShippingCost = data.avgShipCost > 0 ? data.avgShipCost : 0;
|
const actualShippingCost = data.avgShipCost > 0 ? data.avgShipCost : 0;
|
||||||
const productCogs = data.avgCogs > 0 ? data.avgCogs : 0;
|
|
||||||
|
// Calculate COGS based on the selected mode
|
||||||
|
let productCogs;
|
||||||
|
if (cogsCalculationMode === 'average') {
|
||||||
|
// Use overall average COGS percentage applied to this bucket's order value
|
||||||
|
productCogs = orderValue * overallCogsPercentage;
|
||||||
|
} else {
|
||||||
|
// Use actual COGS data from this bucket (existing behavior)
|
||||||
|
productCogs = data.avgCogs > 0 ? data.avgCogs : 0;
|
||||||
|
}
|
||||||
const productDiscountAmount = orderValue * productDiscountRate;
|
const productDiscountAmount = orderValue * productDiscountRate;
|
||||||
const effectiveRegularPrice = productDiscountRate < 0.99
|
const effectiveRegularPrice = productDiscountRate < 0.99
|
||||||
? orderValue / (1 - productDiscountRate)
|
? orderValue / (1 - productDiscountRate)
|
||||||
@@ -557,7 +518,8 @@ router.post('/simulate', async (req, res) => {
|
|||||||
redemptionRate,
|
redemptionRate,
|
||||||
pointDollarValue,
|
pointDollarValue,
|
||||||
weightedProfitAmount,
|
weightedProfitAmount,
|
||||||
weightedProfitPercent
|
weightedProfitPercent,
|
||||||
|
overallCogsPercentage: cogsCalculationMode === 'average' ? overallCogsPercentage : undefined
|
||||||
},
|
},
|
||||||
buckets: bucketResults
|
buckets: bucketResults
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { DiscountPromoOption, DiscountPromoType, ShippingPromoType, ShippingTierConfig, DiscountSimulationResponse } from "@/types/discount-simulator";
|
import { DiscountPromoOption, DiscountPromoType, ShippingPromoType, ShippingTierConfig, DiscountSimulationResponse, CogsCalculationMode } from "@/types/discount-simulator";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { formatNumber } from "@/utils/productUtils";
|
import { formatNumber } from "@/utils/productUtils";
|
||||||
@@ -41,6 +41,8 @@ interface ConfigPanelProps {
|
|||||||
onMerchantFeeChange: (value: number) => void;
|
onMerchantFeeChange: (value: number) => void;
|
||||||
fixedCostPerOrder: number;
|
fixedCostPerOrder: number;
|
||||||
onFixedCostChange: (value: number) => void;
|
onFixedCostChange: (value: number) => void;
|
||||||
|
cogsCalculationMode: CogsCalculationMode;
|
||||||
|
onCogsCalculationModeChange: (mode: CogsCalculationMode) => void;
|
||||||
pointsPerDollar: number;
|
pointsPerDollar: number;
|
||||||
redemptionRate: number;
|
redemptionRate: number;
|
||||||
pointDollarValue: number;
|
pointDollarValue: number;
|
||||||
@@ -105,6 +107,8 @@ export function ConfigPanel({
|
|||||||
onMerchantFeeChange,
|
onMerchantFeeChange,
|
||||||
fixedCostPerOrder,
|
fixedCostPerOrder,
|
||||||
onFixedCostChange,
|
onFixedCostChange,
|
||||||
|
cogsCalculationMode,
|
||||||
|
onCogsCalculationModeChange,
|
||||||
pointsPerDollar,
|
pointsPerDollar,
|
||||||
redemptionRate,
|
redemptionRate,
|
||||||
pointDollarValue,
|
pointDollarValue,
|
||||||
@@ -288,6 +292,11 @@ export function ConfigPanel({
|
|||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
{formatPercent(result.totals.productDiscountRate)} avg discount
|
{formatPercent(result.totals.productDiscountRate)} avg discount
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{result.totals.overallCogsPercentage != null && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{formatPercent(result.totals.overallCogsPercentage)} avg COGS
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -515,34 +524,54 @@ export function ConfigPanel({
|
|||||||
|
|
||||||
<section className={sectionClass}>
|
<section className={sectionClass}>
|
||||||
<span className={sectionTitleClass}>Order costs</span>
|
<span className={sectionTitleClass}>Order costs</span>
|
||||||
<div className={fieldRowHorizontalClass}>
|
<div className={fieldRowClass}>
|
||||||
<div className={fieldClass}>
|
<div className={fieldClass}>
|
||||||
<Label className={labelClass}>Merchant fee (%)</Label>
|
<Label className={labelClass}>COGS calculation</Label>
|
||||||
<Input
|
<Select
|
||||||
className={compactNumberClass}
|
value={cogsCalculationMode}
|
||||||
type="number"
|
onValueChange={(value) => {
|
||||||
step="0.01"
|
|
||||||
value={merchantFeePercent}
|
|
||||||
onChange={(event) => {
|
|
||||||
onConfigInputChange();
|
onConfigInputChange();
|
||||||
onMerchantFeeChange(parseNumber(event.target.value, merchantFeePercent));
|
onCogsCalculationModeChange(value as CogsCalculationMode);
|
||||||
}}
|
}}
|
||||||
onBlur={handleFieldBlur}
|
>
|
||||||
/>
|
<SelectTrigger className={`${compactTriggerClass} w-full`}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="actual">Actual COGS per bucket</SelectItem>
|
||||||
|
<SelectItem value="average">Average COGS percentage</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className={fieldClass}>
|
<div className={fieldRowHorizontalClass}>
|
||||||
<Label className={labelClass}>Fixed cost/order ($)</Label>
|
<div className={fieldClass}>
|
||||||
<Input
|
<Label className={labelClass}>Merchant fee (%)</Label>
|
||||||
className={compactNumberClass}
|
<Input
|
||||||
type="number"
|
className={compactNumberClass}
|
||||||
step="0.01"
|
type="number"
|
||||||
value={fixedCostPerOrder}
|
step="0.01"
|
||||||
onChange={(event) => {
|
value={merchantFeePercent}
|
||||||
onConfigInputChange();
|
onChange={(event) => {
|
||||||
onFixedCostChange(parseNumber(event.target.value, fixedCostPerOrder));
|
onConfigInputChange();
|
||||||
}}
|
onMerchantFeeChange(parseNumber(event.target.value, merchantFeePercent));
|
||||||
onBlur={handleFieldBlur}
|
}}
|
||||||
/>
|
onBlur={handleFieldBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={fieldClass}>
|
||||||
|
<Label className={labelClass}>Fixed cost/order ($)</Label>
|
||||||
|
<Input
|
||||||
|
className={compactNumberClass}
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={fixedCostPerOrder}
|
||||||
|
onChange={(event) => {
|
||||||
|
onConfigInputChange();
|
||||||
|
onFixedCostChange(parseNumber(event.target.value, fixedCostPerOrder));
|
||||||
|
}}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
DiscountPromoType,
|
DiscountPromoType,
|
||||||
ShippingPromoType,
|
ShippingPromoType,
|
||||||
ShippingTierConfig,
|
ShippingTierConfig,
|
||||||
|
CogsCalculationMode,
|
||||||
} from "@/types/discount-simulator";
|
} from "@/types/discount-simulator";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ export function DiscountSimulator() {
|
|||||||
const [shippingTiers, setShippingTiers] = useState<ShippingTierConfig[]>([]);
|
const [shippingTiers, setShippingTiers] = useState<ShippingTierConfig[]>([]);
|
||||||
const [merchantFeePercent, setMerchantFeePercent] = useState(DEFAULT_MERCHANT_FEE);
|
const [merchantFeePercent, setMerchantFeePercent] = useState(DEFAULT_MERCHANT_FEE);
|
||||||
const [fixedCostPerOrder, setFixedCostPerOrder] = useState(DEFAULT_FIXED_COST);
|
const [fixedCostPerOrder, setFixedCostPerOrder] = useState(DEFAULT_FIXED_COST);
|
||||||
|
const [cogsCalculationMode, setCogsCalculationMode] = useState<CogsCalculationMode>('actual');
|
||||||
const [pointDollarValue, setPointDollarValue] = useState(DEFAULT_POINT_VALUE);
|
const [pointDollarValue, setPointDollarValue] = useState(DEFAULT_POINT_VALUE);
|
||||||
const [pointDollarTouched, setPointDollarTouched] = useState(false);
|
const [pointDollarTouched, setPointDollarTouched] = useState(false);
|
||||||
const [simulationResult, setSimulationResult] = useState<DiscountSimulationResponse | undefined>(undefined);
|
const [simulationResult, setSimulationResult] = useState<DiscountSimulationResponse | undefined>(undefined);
|
||||||
@@ -155,6 +157,7 @@ export function DiscountSimulator() {
|
|||||||
}),
|
}),
|
||||||
merchantFeePercent,
|
merchantFeePercent,
|
||||||
fixedCostPerOrder,
|
fixedCostPerOrder,
|
||||||
|
cogsCalculationMode,
|
||||||
pointsConfig: payloadPointsConfig,
|
pointsConfig: payloadPointsConfig,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
@@ -166,6 +169,7 @@ export function DiscountSimulator() {
|
|||||||
shippingTiers,
|
shippingTiers,
|
||||||
merchantFeePercent,
|
merchantFeePercent,
|
||||||
fixedCostPerOrder,
|
fixedCostPerOrder,
|
||||||
|
cogsCalculationMode,
|
||||||
pointDollarValue,
|
pointDollarValue,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -246,6 +250,7 @@ export function DiscountSimulator() {
|
|||||||
shippingTiers?: ShippingTierConfig[];
|
shippingTiers?: ShippingTierConfig[];
|
||||||
merchantFeePercent?: number;
|
merchantFeePercent?: number;
|
||||||
fixedCostPerOrder?: number;
|
fixedCostPerOrder?: number;
|
||||||
|
cogsCalculationMode?: CogsCalculationMode;
|
||||||
pointsConfig?: {
|
pointsConfig?: {
|
||||||
pointsPerDollar?: number | null;
|
pointsPerDollar?: number | null;
|
||||||
redemptionRate?: number | null;
|
redemptionRate?: number | null;
|
||||||
@@ -292,6 +297,10 @@ export function DiscountSimulator() {
|
|||||||
setFixedCostPerOrder(parsed.fixedCostPerOrder);
|
setFixedCostPerOrder(parsed.fixedCostPerOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsed.cogsCalculationMode === 'actual' || parsed.cogsCalculationMode === 'average') {
|
||||||
|
setCogsCalculationMode(parsed.cogsCalculationMode);
|
||||||
|
}
|
||||||
|
|
||||||
if (parsed.pointsConfig && typeof parsed.pointsConfig.pointDollarValue === 'number') {
|
if (parsed.pointsConfig && typeof parsed.pointsConfig.pointDollarValue === 'number') {
|
||||||
setPointDollarValue(parsed.pointsConfig.pointDollarValue);
|
setPointDollarValue(parsed.pointsConfig.pointDollarValue);
|
||||||
setPointDollarTouched(true);
|
setPointDollarTouched(true);
|
||||||
@@ -328,9 +337,10 @@ export function DiscountSimulator() {
|
|||||||
shippingTiers,
|
shippingTiers,
|
||||||
merchantFeePercent,
|
merchantFeePercent,
|
||||||
fixedCostPerOrder,
|
fixedCostPerOrder,
|
||||||
|
cogsCalculationMode,
|
||||||
pointDollarValue,
|
pointDollarValue,
|
||||||
});
|
});
|
||||||
}, [dateRange, selectedPromoId, productPromo, shippingPromo, shippingTiers, merchantFeePercent, fixedCostPerOrder, pointDollarValue]);
|
}, [dateRange, selectedPromoId, productPromo, shippingPromo, shippingTiers, merchantFeePercent, fixedCostPerOrder, cogsCalculationMode, pointDollarValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasLoadedConfig) {
|
if (!hasLoadedConfig) {
|
||||||
@@ -403,6 +413,7 @@ export function DiscountSimulator() {
|
|||||||
setShippingTiers([]);
|
setShippingTiers([]);
|
||||||
setMerchantFeePercent(DEFAULT_MERCHANT_FEE);
|
setMerchantFeePercent(DEFAULT_MERCHANT_FEE);
|
||||||
setFixedCostPerOrder(DEFAULT_FIXED_COST);
|
setFixedCostPerOrder(DEFAULT_FIXED_COST);
|
||||||
|
setCogsCalculationMode('actual');
|
||||||
setPointDollarValue(DEFAULT_POINT_VALUE);
|
setPointDollarValue(DEFAULT_POINT_VALUE);
|
||||||
setPointDollarTouched(false);
|
setPointDollarTouched(false);
|
||||||
setSimulationResult(undefined);
|
setSimulationResult(undefined);
|
||||||
@@ -449,6 +460,8 @@ export function DiscountSimulator() {
|
|||||||
onMerchantFeeChange={setMerchantFeePercent}
|
onMerchantFeeChange={setMerchantFeePercent}
|
||||||
fixedCostPerOrder={fixedCostPerOrder}
|
fixedCostPerOrder={fixedCostPerOrder}
|
||||||
onFixedCostChange={setFixedCostPerOrder}
|
onFixedCostChange={setFixedCostPerOrder}
|
||||||
|
cogsCalculationMode={cogsCalculationMode}
|
||||||
|
onCogsCalculationModeChange={setCogsCalculationMode}
|
||||||
pointsPerDollar={currentPointsPerDollar}
|
pointsPerDollar={currentPointsPerDollar}
|
||||||
redemptionRate={currentRedemptionRate}
|
redemptionRate={currentRedemptionRate}
|
||||||
pointDollarValue={pointDollarValue}
|
pointDollarValue={pointDollarValue}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export interface DiscountSimulationTotals {
|
|||||||
pointDollarValue: number;
|
pointDollarValue: number;
|
||||||
weightedProfitAmount: number;
|
weightedProfitAmount: number;
|
||||||
weightedProfitPercent: number;
|
weightedProfitPercent: number;
|
||||||
|
overallCogsPercentage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiscountSimulationResponse {
|
export interface DiscountSimulationResponse {
|
||||||
@@ -65,6 +66,8 @@ export interface DiscountSimulationResponse {
|
|||||||
buckets: DiscountSimulationBucket[];
|
buckets: DiscountSimulationBucket[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CogsCalculationMode = 'actual' | 'average';
|
||||||
|
|
||||||
export interface DiscountSimulationRequest {
|
export interface DiscountSimulationRequest {
|
||||||
dateRange: {
|
dateRange: {
|
||||||
start: string;
|
start: string;
|
||||||
@@ -89,6 +92,7 @@ export interface DiscountSimulationRequest {
|
|||||||
shippingTiers: ShippingTierConfig[];
|
shippingTiers: ShippingTierConfig[];
|
||||||
merchantFeePercent: number;
|
merchantFeePercent: number;
|
||||||
fixedCostPerOrder: number;
|
fixedCostPerOrder: number;
|
||||||
|
cogsCalculationMode: CogsCalculationMode;
|
||||||
pointsConfig: {
|
pointsConfig: {
|
||||||
pointsPerDollar: number | null;
|
pointsPerDollar: number | null;
|
||||||
redemptionRate: number | null;
|
redemptionRate: number | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user