From 2ab42fe96d1f29341a936612cc6d4149d4752d44 Mon Sep 17 00:00:00 2001 From: CPrutean Date: Thu, 18 Jun 2026 13:50:28 -0700 Subject: [PATCH] fix: made orders tax and shipping subtract from fund buckets --- scripts/verify-order-form.ts | 4 ++-- src/app/api/orders/[id]/action/route.ts | 8 +++++++- src/app/api/orders/[id]/route.ts | 10 +++++++-- src/app/api/orders/route.ts | 10 +++++++-- src/components/orders/AdminOrderQueue.tsx | 12 ++++++++--- src/components/orders/OrderForm.tsx | 11 +++++----- src/components/orders/OrderTable.tsx | 6 +++--- src/components/orders/TeamOrderTable.tsx | 6 +++--- src/lib/finance/finance.test.ts | 21 +++++++++++-------- src/lib/finance/finance.ts | 13 ++++++++---- src/lib/finance/order-export.ts | 8 ++++++-- src/lib/finance/order-pricing.test.ts | 18 ++++++++++++++++ src/lib/finance/order-pricing.ts | 25 +++++++++++++++++++++++ 13 files changed, 117 insertions(+), 35 deletions(-) diff --git a/scripts/verify-order-form.ts b/scripts/verify-order-form.ts index e445cf9..8b9ed9c 100644 --- a/scripts/verify-order-form.ts +++ b/scripts/verify-order-form.ts @@ -122,7 +122,7 @@ const admin = db.select().from(user).limit(1).get(); assert(admin != null, "Need a user in the database"); const stfUnitCents = Math.round(stfData.unitCost * 100); -const stfTotal = orderTotalCents(stfData.quantity, stfUnitCents); +const stfTotal = orderTotalCents(stfData.quantity, stfUnitCents, "STF"); const stfBalance = validateOrderBalance("STF", stfData.stfBucketId, stfTotal); assert(stfBalance.ok, "STF balance check should pass before insert"); @@ -171,7 +171,7 @@ assert(getGiftFundValueCents() === 50_000, "Gift fund adjustment failed"); const giftData = giftParsed.data!; const giftUnitCents = Math.round(giftData.unitCost * 100); -const giftTotal = orderTotalCents(giftData.quantity, giftUnitCents); +const giftTotal = orderTotalCents(giftData.quantity, giftUnitCents, "Gift"); const giftOrder = db .insert(order) diff --git a/src/app/api/orders/[id]/action/route.ts b/src/app/api/orders/[id]/action/route.ts index fdb7562..801572c 100644 --- a/src/app/api/orders/[id]/action/route.ts +++ b/src/app/api/orders/[id]/action/route.ts @@ -5,6 +5,7 @@ import { db } from "@/lib/db"; import { order, orderHistory, user } from "@/lib/db/schema"; import { deductGiftFundForApproval, + ensureFinanceSettingsRow, orderTotalCents, sendOrderApprovedEmail, sendOrderDeniedEmail, @@ -45,8 +46,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: return NextResponse.json({ error: "Only pending orders can be reviewed" }, { status: 400 }); } + ensureFinanceSettingsRow(); const newStatus = ORDER_ACTION_STATUS[parsed.data.action]; - const totalCostCents = orderTotalCents(existing.quantity, existing.unitCostCents); + const totalCostCents = orderTotalCents( + existing.quantity, + existing.unitCostCents, + existing.fundType + ); if (parsed.data.action === "approve") { const balanceCheck = validateOrderBalance( diff --git a/src/app/api/orders/[id]/route.ts b/src/app/api/orders/[id]/route.ts index 0dd4693..1366519 100644 --- a/src/app/api/orders/[id]/route.ts +++ b/src/app/api/orders/[id]/route.ts @@ -4,6 +4,7 @@ import { NextResponse, type NextRequest } from "next/server"; import { db } from "@/lib/db"; import { order, orderHistory } from "@/lib/db/schema"; import { + ensureFinanceSettingsRow, getActiveQuarter, orderTotalCents, restoreGiftFundForDeletion, @@ -55,8 +56,9 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id } const d = parsed.data; + ensureFinanceSettingsRow(); const unitCostCents = Math.round(d.unitCost * 100); - const totalCostCents = orderTotalCents(d.quantity, unitCostCents); + const totalCostCents = orderTotalCents(d.quantity, unitCostCents, d.fundType); const balanceCheck = validateOrderBalance(d.fundType, d.stfBucketId, totalCostCents); if (!balanceCheck.ok) { @@ -132,7 +134,11 @@ export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ } if (isLockedOrderStatus(existing.status) && existing.fundType === "Gift") { - const totalCostCents = orderTotalCents(existing.quantity, existing.unitCostCents); + const totalCostCents = orderTotalCents( + existing.quantity, + existing.unitCostCents, + existing.fundType + ); restoreGiftFundForDeletion(orderId, totalCostCents, user.id); } diff --git a/src/app/api/orders/route.ts b/src/app/api/orders/route.ts index 2e7c980..a56e811 100644 --- a/src/app/api/orders/route.ts +++ b/src/app/api/orders/route.ts @@ -2,7 +2,12 @@ import { NextResponse, type NextRequest } from "next/server"; import { db } from "@/lib/db"; import { order } from "@/lib/db/schema"; -import { getActiveQuarter, orderTotalCents, validateOrderBalance } from "@/lib/finance/finance"; +import { + getActiveQuarter, + ensureFinanceSettingsRow, + orderTotalCents, + validateOrderBalance, +} from "@/lib/finance/finance"; import { getSessionUser } from "@/lib/auth/session"; import { orderInputSchema } from "@/lib/validation"; @@ -22,8 +27,9 @@ export async function POST(req: NextRequest) { } const d = parsed.data; + ensureFinanceSettingsRow(); const unitCostCents = Math.round(d.unitCost * 100); - const totalCostCents = orderTotalCents(d.quantity, unitCostCents); + const totalCostCents = orderTotalCents(d.quantity, unitCostCents, d.fundType); const balanceCheck = validateOrderBalance(d.fundType, d.stfBucketId, totalCostCents); if (!balanceCheck.ok) { diff --git a/src/components/orders/AdminOrderQueue.tsx b/src/components/orders/AdminOrderQueue.tsx index 1eae7ef..27a85da 100644 --- a/src/components/orders/AdminOrderQueue.tsx +++ b/src/components/orders/AdminOrderQueue.tsx @@ -24,7 +24,7 @@ import { formatOrderForExcel, } from "@/lib/finance/order-export"; import { - computeOrderTotalCents, + orderChargeCents, displayPercentToBps, type OrderPricingSettings, } from "@/lib/finance/order-pricing"; @@ -459,7 +459,8 @@ function OrderSection({ {formatPriceCents( - computeOrderTotalCents( + orderChargeCents( + o.fundType, o.quantity, o.unitCostCents, pricingSettings @@ -526,7 +527,12 @@ function OrderDetail({ }) { const pricingSettings = toPricingSettings(orderPricing); const excelRow = formatOrderForExcel(order, false, pricingSettings); - const total = computeOrderTotalCents(order.quantity, order.unitCostCents, pricingSettings); + const total = orderChargeCents( + order.fundType, + order.quantity, + order.unitCostCents, + pricingSettings + ); return (
diff --git a/src/components/orders/OrderForm.tsx b/src/components/orders/OrderForm.tsx index 596b113..59330f4 100644 --- a/src/components/orders/OrderForm.tsx +++ b/src/components/orders/OrderForm.tsx @@ -33,7 +33,7 @@ import { StfBucketSelectItemContent, } from "@/components/BalanceAmount"; import type { FundType, OrderStatus } from "@/lib/db/schema"; -import { computeOrderTotalCents, displayPercentToBps } from "@/lib/finance/order-pricing"; +import { orderChargeCents, displayPercentToBps } from "@/lib/finance/order-pricing"; import { cn, formatPriceCents } from "@/lib/utils"; type StfBucketBalance = { @@ -166,13 +166,14 @@ export function OrderForm({ initialOrder }: { initialOrder?: OrderFormInitial }) const cost = Number(unitCost); const pricing = balances?.orderPricing; if (!Number.isFinite(qty) || !Number.isFinite(cost) || qty < 1 || cost <= 0) return null; - if (!pricing) return null; + if (!pricing || !fundType) return null; const unitCostCents = Math.round(cost * 100); - return computeOrderTotalCents(qty, unitCostCents, { + const settings = { taxPercentBps: displayPercentToBps(pricing.taxPercent), shippingPercentBps: displayPercentToBps(pricing.shippingPercent), - }); - }, [quantity, unitCost, balances?.orderPricing]); + }; + return orderChargeCents(fundType, qty, unitCostCents, settings); + }, [quantity, unitCost, balances?.orderPricing, fundType]); const balanceError = useMemo(() => { if (!fundType || totalCostCents == null || !balances) return null; diff --git a/src/components/orders/OrderTable.tsx b/src/components/orders/OrderTable.tsx index 95373f4..636291d 100644 --- a/src/components/orders/OrderTable.tsx +++ b/src/components/orders/OrderTable.tsx @@ -24,7 +24,7 @@ import { } from "@/components/ui/table"; import type { FundType, OrderStatus } from "@/lib/db/schema"; import { - computeOrderTotalCents, + orderChargeCents, displayPercentToBps, type OrderPricingSettings, } from "@/lib/finance/order-pricing"; @@ -49,10 +49,10 @@ type FundFilter = "all" | FundType; type SortKey = "newest" | "oldest" | "item-asc" | "item-desc" | "total-desc" | "total-asc"; function totalCostCents( - row: { quantity: number; unitCostCents: number }, + row: { fundType: FundType; quantity: number; unitCostCents: number }, pricing: OrderPricingSettings ) { - return computeOrderTotalCents(row.quantity, row.unitCostCents, pricing); + return orderChargeCents(row.fundType, row.quantity, row.unitCostCents, pricing); } function canModifyOrder(status: OrderStatus) { diff --git a/src/components/orders/TeamOrderTable.tsx b/src/components/orders/TeamOrderTable.tsx index da0b4ee..b64682d 100644 --- a/src/components/orders/TeamOrderTable.tsx +++ b/src/components/orders/TeamOrderTable.tsx @@ -19,7 +19,7 @@ import { } from "@/components/ui/table"; import type { FundType, OrderStatus } from "@/lib/db/schema"; import { - computeOrderTotalCents, + orderChargeCents, displayPercentToBps, type OrderPricingSettings, } from "@/lib/finance/order-pricing"; @@ -45,10 +45,10 @@ type FundFilter = "all" | FundType; type SortKey = "newest" | "oldest" | "item-asc" | "item-desc" | "total-desc" | "total-asc"; function totalCostCents( - row: { quantity: number; unitCostCents: number }, + row: { fundType: FundType; quantity: number; unitCostCents: number }, pricing: OrderPricingSettings ) { - return computeOrderTotalCents(row.quantity, row.unitCostCents, pricing); + return orderChargeCents(row.fundType, row.quantity, row.unitCostCents, pricing); } function requesterLabel(row: TeamOrderRow) { diff --git a/src/lib/finance/finance.test.ts b/src/lib/finance/finance.test.ts index ae1ac97..1922cc2 100644 --- a/src/lib/finance/finance.test.ts +++ b/src/lib/finance/finance.test.ts @@ -44,20 +44,24 @@ afterEach(() => { }); describe("orderTotalCents", () => { - it("includes default tax and shipping on the subtotal", () => { - expect(orderTotalCents(2, 5000)).toBe(13_100); + it("includes default tax and shipping on the gift subtotal", () => { + expect(orderTotalCents(2, 5000, "Gift")).toBe(13_100); + }); + + it("includes flux, tax, and shipping on the STF subtotal", () => { + expect(orderTotalCents(2, 5000, "STF")).toBe(15_720); }); it("returns 0 when quantity is 0", () => { - expect(orderTotalCents(0, 5000)).toBe(0); + expect(orderTotalCents(0, 5000, "Gift")).toBe(0); }); it("returns 0 when unit cost is 0", () => { - expect(orderTotalCents(5, 0)).toBe(0); + expect(orderTotalCents(5, 0, "STF")).toBe(0); }); it("handles large quantities and costs without overflow", () => { - const result = orderTotalCents(9999, 999999); + const result = orderTotalCents(9999, 999999, "Gift"); expect(Number.isFinite(result)).toBe(true); expect(result).toBe( computeExpectedTotal( @@ -76,7 +80,8 @@ describe("updateOrderPricingSettings", () => { updateOrderPricingSettings({ taxPercentBps: 500, shippingPercentBps: 1000 }); expect(getOrderPricingSettings()).toEqual({ taxPercentBps: 500, shippingPercentBps: 1000 }); - expect(orderTotalCents(1, 10_000)).toBe(11_500); + expect(orderTotalCents(1, 10_000, "Gift")).toBe(11_500); + expect(orderTotalCents(1, 10_000, "STF")).toBe(13_800); updateOrderPricingSettings(previous); expect(getOrderPricingSettings()).toEqual(previous); @@ -117,7 +122,7 @@ describe("getBucketApprovedSpendCents", () => { .get(); const spendAfterApproved = getBucketApprovedSpendCents(bucketRecord.id, quarter.id); - expect(spendAfterApproved - spendBefore).toBe(orderTotalCents(1, 1000)); + expect(spendAfterApproved - spendBefore).toBe(orderTotalCents(1, 1000, "STF")); db.update(order).set({ status: "ordered" }).where(eq(order.id, approved.id)).run(); const spendAfterOrdered = getBucketApprovedSpendCents(bucketRecord.id, quarter.id); @@ -151,7 +156,7 @@ describe("restoreGiftFundForDeletion", () => { .returning() .get(); - const total = orderTotalCents(1, 5000); + const total = orderTotalCents(1, 5000, "Gift"); deductGiftFundForApproval(giftOrder.id, total, requester.id); const afterDeduction = db diff --git a/src/lib/finance/finance.ts b/src/lib/finance/finance.ts index 549f204..c4002a1 100644 --- a/src/lib/finance/finance.ts +++ b/src/lib/finance/finance.ts @@ -13,16 +13,21 @@ import { type FundType, } from "@/lib/db/schema"; import { - computeOrderTotalCents, DEFAULT_ORDER_PRICING, + orderChargeCents, + stfOrderTotalCents, type OrderPricingSettings, } from "@/lib/finance/order-pricing"; export const GIFT_FUND_ID = 1; export const FINANCE_SETTINGS_ID = 1; -export function orderTotalCents(quantity: number, unitCostCents: number): number { - return computeOrderTotalCents(quantity, unitCostCents, getOrderPricingSettings()); +export function orderTotalCents( + quantity: number, + unitCostCents: number, + fundType: FundType +): number { + return orderChargeCents(fundType, quantity, unitCostCents, getOrderPricingSettings()); } export function getOrderPricingSettings(): OrderPricingSettings { @@ -93,7 +98,7 @@ export function getBucketApprovedSpendCents(bucketId: number, quarterId: number) .all(); return rows.reduce( - (sum, row) => sum + computeOrderTotalCents(row.quantity, row.unitCostCents, settings), + (sum, row) => sum + stfOrderTotalCents(row.quantity, row.unitCostCents, settings), 0 ); } diff --git a/src/lib/finance/order-export.ts b/src/lib/finance/order-export.ts index 850f43a..49f5f60 100644 --- a/src/lib/finance/order-export.ts +++ b/src/lib/finance/order-export.ts @@ -1,7 +1,11 @@ import type { FundType, OrderStatus } from "@/lib/db/schema"; -import { DEFAULT_ORDER_PRICING, type OrderPricingSettings } from "@/lib/finance/order-pricing"; +import { + DEFAULT_ORDER_PRICING, + STF_PRICE_FLUX, + type OrderPricingSettings, +} from "@/lib/finance/order-pricing"; -export const STF_PRICE_FLUX = 1.2; +export { STF_PRICE_FLUX }; export type OrderExportRow = { itemName: string; diff --git a/src/lib/finance/order-pricing.test.ts b/src/lib/finance/order-pricing.test.ts index ac0f118..08ddc5b 100644 --- a/src/lib/finance/order-pricing.test.ts +++ b/src/lib/finance/order-pricing.test.ts @@ -4,7 +4,9 @@ import { computeOrderTotalCents, DEFAULT_ORDER_PRICING, displayPercentToBps, + orderChargeCents, percentBpsToDisplay, + stfOrderTotalCents, } from "./order-pricing"; describe("computeOrderTotalCents", () => { @@ -23,6 +25,22 @@ describe("computeOrderTotalCents", () => { }); }); +describe("stfOrderTotalCents", () => { + it("applies flux before tax and shipping", () => { + expect(stfOrderTotalCents(2, 5000, DEFAULT_ORDER_PRICING)).toBe(15_720); + }); +}); + +describe("orderChargeCents", () => { + it("uses the gift formula for gift orders", () => { + expect(orderChargeCents("Gift", 2, 5000, DEFAULT_ORDER_PRICING)).toBe(13_100); + }); + + it("uses the STF formula for STF orders", () => { + expect(orderChargeCents("STF", 2, 5000, DEFAULT_ORDER_PRICING)).toBe(15_720); + }); +}); + describe("percent conversions", () => { it("converts between display percent and basis points", () => { expect(displayPercentToBps(11)).toBe(1100); diff --git a/src/lib/finance/order-pricing.ts b/src/lib/finance/order-pricing.ts index 0c800c4..9265c89 100644 --- a/src/lib/finance/order-pricing.ts +++ b/src/lib/finance/order-pricing.ts @@ -1,3 +1,5 @@ +export const STF_PRICE_FLUX = 1.2; + export const DEFAULT_TAX_PERCENT_BPS = 1100; export const DEFAULT_SHIPPING_PERCENT_BPS = 2000; @@ -33,3 +35,26 @@ export function computeOrderTotalCents( const shipping = Math.round((subtotal * settings.shippingPercentBps) / 10_000); return subtotal + tax + shipping; } + +export function stfOrderTotalCents( + quantity: number, + unitCostCents: number, + settings: OrderPricingSettings = DEFAULT_ORDER_PRICING +): number { + const preTaxTotal = Math.round(orderSubtotalCents(quantity, unitCostCents) * STF_PRICE_FLUX); + const tax = Math.round((preTaxTotal * settings.taxPercentBps) / 10_000); + const shipping = Math.round((preTaxTotal * settings.shippingPercentBps) / 10_000); + return preTaxTotal + tax + shipping; +} + +export function orderChargeCents( + fundType: "STF" | "Gift", + quantity: number, + unitCostCents: number, + settings: OrderPricingSettings = DEFAULT_ORDER_PRICING +): number { + if (fundType === "STF") { + return stfOrderTotalCents(quantity, unitCostCents, settings); + } + return computeOrderTotalCents(quantity, unitCostCents, settings); +}