Saltar al contenido principal

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

  1. ✅ Price list exists → uses list price
  2. ✅ No price list → falls back to base price
  3. ✅ Quantity tier → price changes
  4. ✅ API error → graceful fallback
  5. ✅ 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

  1. Always provide base price as fallback
  2. Handle errors gracefully (don't crash on API failure)
  3. Show loading states during resolution
  4. Cache results when appropriate
  5. Re-resolve on quantity change for tier pricing
  6. Display price source for transparency
  7. Log errors for debugging
  8. 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