Pricing Module - Quick Start Guide
For Developers: How to use the pricing module in your features
Frontend: Resolve Prices in Your Component
1. Import the Service
import { resolveSaleItemPrice } from "@/services/pricingService";
import { useAuth } from "@/contexts/AuthContext";
import { useCurrentBusiness } from "@/contexts/useCurrentBusiness";
import { useCurrentLocation } from "@/contexts/useCurrentLocation";
2. Call the API
const { token } = useAuth();
const { currentBusiness } = useCurrentBusiness();
const { currentLocation } = useCurrentLocation();
// When user selects a product/service
const handleItemSelect = async (item: Product | Service) => {
try {
const resolved = await resolveSaleItemPrice(token, {
businessId: currentBusiness.id,
locationId: currentLocation.id,
channel: "pos", // or "online", "dine_in", etc.
itemType: item.itemType, // "product" or "service"
itemId: item.id,
quantity: 1,
baseCurrencyId: item.currencyId,
basePrice: item.price || 0,
});
// Use resolved price
setUnitPrice(resolved.unitPrice);
setPriceSource(resolved.source); // "price_list" or "base_price"
setPriceListId(resolved.priceListId);
} catch (error) {
// Fallback to base price on error
setUnitPrice(item.price || 0);
setPriceSource("base_price");
}
};
3. Handle Quantity Changes (Tier Pricing)
useEffect(() => {
if (quantity !== previousQuantity && itemId) {
// Re-resolve price for new quantity tier
resolveSaleItemPrice(token, {
businessId: currentBusiness.id,
locationId: currentLocation.id,
channel: "pos",
itemType: "product",
itemId: itemId,
quantity: quantity,
baseCurrencyId: currencyId,
basePrice: basePrice,
}).then((resolved) => {
if (resolved.unitPrice !== currentPrice) {
setUnitPrice(resolved.unitPrice);
// Show toast: "Tier price applied"
}
});
}
}, [quantity]);
Backend: Validate Prices on Submit
1. Inject the Service
import { SalePricingService } from "@/sales/application/sale-pricing.service";
@Injectable()
export class YourService {
constructor(
private readonly salePricingService: SalePricingService,
) {}
}
2. Resolve Price Snapshots
const snapshots = await this.salePricingService.resolveOrderItemSnapshots({
businessId: sale.businessId,
locationId: sale.locationId,
channel: "pos",
itemType: item.itemType, // "product" or "service"
itemId: item.productId || item.serviceId,
quantity: item.quantity,
item: {
id: product.id,
businessId: product.businessId,
price: product.price,
priceMinor: product.priceMinor,
currencyId: product.currencyId,
minorUnit: product.minorUnit,
taxes: product.taxes,
},
});
// Store in database
item.unitPriceSnapshot = snapshots.unitPriceSnapshot;
item.priceListId = snapshots.priceListId;
item.priceSource = snapshots.source;
Feature Flag
Enable Price Lists
# .env
ENABLE_PRICE_LISTS=true # Use price lists
ENABLE_PRICE_LISTS=false # Use base prices (default)
Check Status
const enabled = process.env.ENABLE_PRICE_LISTS === "true";
API Reference
Resolve Price Endpoint
POST /api/v1/pricing/resolve
Request:
{
"businessId": "uuid",
"locationId": "uuid",
"channel": "pos",
"itemType": "product",
"itemId": "uuid",
"quantity": 5,
"baseCurrencyId": "uuid",
"basePrice": 25.99
}
Response:
{
"unitPrice": 23.99,
"unitPriceMinor": "2399",
"currencyId": "uuid",
"priceListId": "uuid",
"source": "price_list",
"priority": 200,
"minQty": 5
}
Common Patterns
Show Price Source Badge
{priceSource && (
<span className={`badge ${
priceSource === "price_list"
? "badge-blue"
: "badge-gray"
}`}>
{priceSource === "price_list"
? "From Price List"
: "Base Price"}
</span>
)}
Loading State
const [isResolvingPrice, setIsResolvingPrice] = useState(false);
// During resolution
setIsResolvingPrice(true);
const resolved = await resolveSaleItemPrice(...);
setIsResolvingPrice(false);
// In UI
{isResolvingPrice && <span>Resolving price...</span>}
Error Handling
try {
const resolved = await resolveSaleItemPrice(...);
setPrice(resolved.unitPrice);
} catch (error) {
console.error("Price resolution failed:", error);
// Fallback to base price
setPrice(item.price || 0);
toast({
title: "Price resolution failed",
description: "Using base price",
variant: "destructive",
});
}
Testing
Mock Price Resolution
// In tests
jest.mock("@/services/pricingService", () => ({
resolveSaleItemPrice: jest.fn().mockResolvedValue({
unitPrice: 23.99,
unitPriceMinor: "2399",
currencyId: "uuid",
priceListId: "uuid",
source: "price_list",
priority: 200,
minQty: 5,
}),
}));
Test Scenarios
- ✅ Price list exists → uses list price
- ✅ No price list → falls back to base price
- ✅ Quantity tier → price changes
- ✅ API error → graceful fallback
- ✅ Feature flag off → uses base price
Troubleshooting
Price not resolving
- Check
ENABLE_PRICE_LISTS=true - Verify price list exists and is active
- Check price list scope (location + channel)
- Clear Redis cache:
POST /api/v1/pricing/price-lists/cache/clear
Wrong price
- Check price list priority
- Verify quantity tier (min_qty)
- Check validity period (valid_from/valid_to)
- Verify currency matches
Performance issues
- Check cache hit rate:
GET /api/v1/pricing/price-lists/cache/stats - Monitor API response times
- Consider batch resolution for multiple items
Best Practices
- ✅ Always provide base price as fallback
- ✅ Handle errors gracefully (don't crash on API failure)
- ✅ Show loading states during resolution
- ✅ Cache results when appropriate
- ✅ Re-resolve on quantity change for tier pricing
- ✅ Display price source for transparency
- ✅ Log errors for debugging
- ✅ Test with feature flag on and off
Need Help?
- Documentation:
apps/backend/src/pricing/README.md - Examples:
apps/frontend-pwa/src/components/forms/sale/SaleItemRow.tsx - API Docs:
specs/013-pricing-module/API-DOCUMENTATION.md - Integration Guide:
specs/013-pricing-module/INTEGRATION-GUIDE.md