hvp-shared 6.46.0 → 6.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -128,4 +128,29 @@ export declare function calculateRecommendedPricePVP(params: {
|
|
|
128
128
|
unitPurchasePriceBI: number;
|
|
129
129
|
vatSalePercent: number;
|
|
130
130
|
}): number | null;
|
|
131
|
+
/** Cost-based multiplier for floor price (PHARM, PHARM_FRAC, COST_ML, COST) */
|
|
132
|
+
export declare const MIN_PRICE_COST_FACTOR = 0.25;
|
|
133
|
+
/** Cost-based multiplier for floor price (EXT_COST) */
|
|
134
|
+
export declare const MIN_PRICE_EXT_COST_FACTOR = 1;
|
|
135
|
+
/** Fraction of salePricePVP for recommended minimum price (COMP_SVC) */
|
|
136
|
+
export declare const MIN_PRICE_COMP_SVC_SALE_FRACTION = 0.5;
|
|
137
|
+
export interface FloorPriceResult {
|
|
138
|
+
recommendedMinimumPrice: number | null;
|
|
139
|
+
floorPrice: number | null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Calculate floor prices (recommended minimum price and absolute floor) for a catalog item.
|
|
143
|
+
*
|
|
144
|
+
* Business rules by PricePolicy:
|
|
145
|
+
* - PHARM, PHARM_FRAC, COST_ML, COST: both = unitCostBI × 0.25 × (1 + VAT/100), round5
|
|
146
|
+
* - EXT_COST: both = unitCostBI × 1.0 × (1 + VAT/100), round5
|
|
147
|
+
* - COMP_SVC: recommendedMinimumPrice = salePricePVP × 0.50 (round5), floorPrice = 0
|
|
148
|
+
* - NONE, SPECIAL, MANUAL: both null
|
|
149
|
+
*/
|
|
150
|
+
export declare function calculateFloorPrices(params: {
|
|
151
|
+
pricePolicy: PricePolicy;
|
|
152
|
+
unitPurchasePriceBI: number;
|
|
153
|
+
vatSalePercent: number;
|
|
154
|
+
salePricePVP: number;
|
|
155
|
+
}): FloorPriceResult;
|
|
131
156
|
export {};
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* recommendedPrice = unitCostBI * markupFactor * (1 + vatSale/100)
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.ALLOWED_POLICIES_BY_SECTION = exports.COMP_SVC_SECTIONS = exports.NON_SELLABLE_SECTIONS = exports.NO_FORMULA_POLICIES = exports.AUTO_TIER_POLICIES = exports.AUTO_TIER_RANGES = exports.MARKUP_FACTORS = exports.PRICING_TIER_VALUES = exports.PRICE_POLICY_VALUES = exports.PRICING_TIER_LABELS = exports.PRICE_POLICY_LABELS = exports.PricingTier = exports.PricePolicy = void 0;
|
|
12
|
+
exports.MIN_PRICE_COMP_SVC_SALE_FRACTION = exports.MIN_PRICE_EXT_COST_FACTOR = exports.MIN_PRICE_COST_FACTOR = exports.ALLOWED_POLICIES_BY_SECTION = exports.COMP_SVC_SECTIONS = exports.NON_SELLABLE_SECTIONS = exports.NO_FORMULA_POLICIES = exports.AUTO_TIER_POLICIES = exports.AUTO_TIER_RANGES = exports.MARKUP_FACTORS = exports.PRICING_TIER_VALUES = exports.PRICE_POLICY_VALUES = exports.PRICING_TIER_LABELS = exports.PRICE_POLICY_LABELS = exports.PricingTier = exports.PricePolicy = void 0;
|
|
13
13
|
exports.getAutoTier = getAutoTier;
|
|
14
14
|
exports.getMarkupFactor = getMarkupFactor;
|
|
15
15
|
exports.getDefaultPricePolicy = getDefaultPricePolicy;
|
|
16
16
|
exports.calculateRecommendedPricePVP = calculateRecommendedPricePVP;
|
|
17
|
+
exports.calculateFloorPrices = calculateFloorPrices;
|
|
17
18
|
const qvet_catalog_1 = require("./qvet-catalog");
|
|
18
19
|
// =============================================================================
|
|
19
20
|
// ENUMS
|
|
@@ -214,6 +215,13 @@ function getDefaultPricePolicy(section, saleUnit, conversionFactor) {
|
|
|
214
215
|
// Fallback: manual pricing
|
|
215
216
|
return { policy: PricePolicy.MANUAL, tier: null };
|
|
216
217
|
}
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// ROUNDING HELPER
|
|
220
|
+
// =============================================================================
|
|
221
|
+
/** Round to nearest multiple of 5 */
|
|
222
|
+
function roundToNearest5(value) {
|
|
223
|
+
return Math.round(value / 5) * 5;
|
|
224
|
+
}
|
|
217
225
|
/**
|
|
218
226
|
* Calculate the recommended price PVP (con IVA) for a catalog item.
|
|
219
227
|
*
|
|
@@ -230,6 +238,56 @@ function calculateRecommendedPricePVP(params) {
|
|
|
230
238
|
return null;
|
|
231
239
|
const priceBI = unitPurchasePriceBI * factor;
|
|
232
240
|
const pricePVP = priceBI * (1 + vatSalePercent / 100);
|
|
233
|
-
|
|
234
|
-
|
|
241
|
+
return roundToNearest5(pricePVP);
|
|
242
|
+
}
|
|
243
|
+
// =============================================================================
|
|
244
|
+
// FLOOR PRICE CALCULATION
|
|
245
|
+
// =============================================================================
|
|
246
|
+
/** Cost-based multiplier for floor price (PHARM, PHARM_FRAC, COST_ML, COST) */
|
|
247
|
+
exports.MIN_PRICE_COST_FACTOR = 0.25;
|
|
248
|
+
/** Cost-based multiplier for floor price (EXT_COST) */
|
|
249
|
+
exports.MIN_PRICE_EXT_COST_FACTOR = 1.0;
|
|
250
|
+
/** Fraction of salePricePVP for recommended minimum price (COMP_SVC) */
|
|
251
|
+
exports.MIN_PRICE_COMP_SVC_SALE_FRACTION = 0.50;
|
|
252
|
+
/** Policies that use cost-based floor price (cost × factor × (1+VAT)) */
|
|
253
|
+
const COST_BASED_FLOOR_POLICIES = [
|
|
254
|
+
PricePolicy.PHARM,
|
|
255
|
+
PricePolicy.PHARM_FRAC,
|
|
256
|
+
PricePolicy.COST_ML,
|
|
257
|
+
PricePolicy.COST,
|
|
258
|
+
];
|
|
259
|
+
/**
|
|
260
|
+
* Calculate floor prices (recommended minimum price and absolute floor) for a catalog item.
|
|
261
|
+
*
|
|
262
|
+
* Business rules by PricePolicy:
|
|
263
|
+
* - PHARM, PHARM_FRAC, COST_ML, COST: both = unitCostBI × 0.25 × (1 + VAT/100), round5
|
|
264
|
+
* - EXT_COST: both = unitCostBI × 1.0 × (1 + VAT/100), round5
|
|
265
|
+
* - COMP_SVC: recommendedMinimumPrice = salePricePVP × 0.50 (round5), floorPrice = 0
|
|
266
|
+
* - NONE, SPECIAL, MANUAL: both null
|
|
267
|
+
*/
|
|
268
|
+
function calculateFloorPrices(params) {
|
|
269
|
+
const { pricePolicy, unitPurchasePriceBI, vatSalePercent, salePricePVP } = params;
|
|
270
|
+
// Cost-based policies (PHARM, PHARM_FRAC, COST_ML, COST)
|
|
271
|
+
if (COST_BASED_FLOOR_POLICIES.includes(pricePolicy)) {
|
|
272
|
+
if (unitPurchasePriceBI <= 0)
|
|
273
|
+
return { recommendedMinimumPrice: null, floorPrice: null };
|
|
274
|
+
const price = roundToNearest5(unitPurchasePriceBI * exports.MIN_PRICE_COST_FACTOR * (1 + vatSalePercent / 100));
|
|
275
|
+
return { recommendedMinimumPrice: price, floorPrice: price };
|
|
276
|
+
}
|
|
277
|
+
// External cost services
|
|
278
|
+
if (pricePolicy === PricePolicy.EXT_COST) {
|
|
279
|
+
if (unitPurchasePriceBI <= 0)
|
|
280
|
+
return { recommendedMinimumPrice: null, floorPrice: null };
|
|
281
|
+
const price = roundToNearest5(unitPurchasePriceBI * exports.MIN_PRICE_EXT_COST_FACTOR * (1 + vatSalePercent / 100));
|
|
282
|
+
return { recommendedMinimumPrice: price, floorPrice: price };
|
|
283
|
+
}
|
|
284
|
+
// Internal services (market-study pricing)
|
|
285
|
+
if (pricePolicy === PricePolicy.COMP_SVC) {
|
|
286
|
+
const minPrice = salePricePVP > 0
|
|
287
|
+
? roundToNearest5(salePricePVP * exports.MIN_PRICE_COMP_SVC_SALE_FRACTION)
|
|
288
|
+
: null;
|
|
289
|
+
return { recommendedMinimumPrice: minPrice, floorPrice: 0 };
|
|
290
|
+
}
|
|
291
|
+
// NONE, SPECIAL, MANUAL — no floor prices
|
|
292
|
+
return { recommendedMinimumPrice: null, floorPrice: null };
|
|
235
293
|
}
|
|
@@ -312,3 +312,85 @@ describe('calculateRecommendedPricePVP', () => {
|
|
|
312
312
|
expect(result).toBe(60);
|
|
313
313
|
});
|
|
314
314
|
});
|
|
315
|
+
// =============================================================================
|
|
316
|
+
// calculateFloorPrices
|
|
317
|
+
// =============================================================================
|
|
318
|
+
describe('calculateFloorPrices', () => {
|
|
319
|
+
const defaults = { unitPurchasePriceBI: 100, vatSalePercent: 16, salePricePVP: 500 };
|
|
320
|
+
describe('cost-based policies (PHARM, PHARM_FRAC, COST_ML, COST)', () => {
|
|
321
|
+
const costPolicies = [pricing_constants_1.PricePolicy.PHARM, pricing_constants_1.PricePolicy.PHARM_FRAC, pricing_constants_1.PricePolicy.COST_ML, pricing_constants_1.PricePolicy.COST];
|
|
322
|
+
it.each(costPolicies)('%s: both = cost × 0.25 × (1+VAT/100), round5', (policy) => {
|
|
323
|
+
// $100 * 0.25 * 1.16 = $29 → round5 = $30
|
|
324
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: policy });
|
|
325
|
+
expect(result.recommendedMinimumPrice).toBe(30);
|
|
326
|
+
expect(result.floorPrice).toBe(30);
|
|
327
|
+
});
|
|
328
|
+
it('should round to nearest 5', () => {
|
|
329
|
+
// $80 * 0.25 * 1.16 = $23.20 → round5 = $25
|
|
330
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.PHARM, unitPurchasePriceBI: 80 });
|
|
331
|
+
expect(result.recommendedMinimumPrice).toBe(25);
|
|
332
|
+
expect(result.floorPrice).toBe(25);
|
|
333
|
+
});
|
|
334
|
+
it('should return null when cost is 0', () => {
|
|
335
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.PHARM, unitPurchasePriceBI: 0 });
|
|
336
|
+
expect(result.recommendedMinimumPrice).toBeNull();
|
|
337
|
+
expect(result.floorPrice).toBeNull();
|
|
338
|
+
});
|
|
339
|
+
it('should return null when cost is negative', () => {
|
|
340
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.COST, unitPurchasePriceBI: -10 });
|
|
341
|
+
expect(result.recommendedMinimumPrice).toBeNull();
|
|
342
|
+
expect(result.floorPrice).toBeNull();
|
|
343
|
+
});
|
|
344
|
+
it('should handle 0% VAT', () => {
|
|
345
|
+
// $100 * 0.25 * 1.0 = $25 → $25
|
|
346
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.COST, vatSalePercent: 0 });
|
|
347
|
+
expect(result.recommendedMinimumPrice).toBe(25);
|
|
348
|
+
expect(result.floorPrice).toBe(25);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
describe('EXT_COST', () => {
|
|
352
|
+
it('both = cost × 1.0 × (1+VAT/100), round5', () => {
|
|
353
|
+
// $100 * 1.0 * 1.16 = $116 → round5 = $115
|
|
354
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.EXT_COST });
|
|
355
|
+
expect(result.recommendedMinimumPrice).toBe(115);
|
|
356
|
+
expect(result.floorPrice).toBe(115);
|
|
357
|
+
});
|
|
358
|
+
it('should return null when cost is 0', () => {
|
|
359
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.EXT_COST, unitPurchasePriceBI: 0 });
|
|
360
|
+
expect(result.recommendedMinimumPrice).toBeNull();
|
|
361
|
+
expect(result.floorPrice).toBeNull();
|
|
362
|
+
});
|
|
363
|
+
it('should round to nearest 5', () => {
|
|
364
|
+
// $200 * 1.0 * 1.16 = $232 → round5 = $230
|
|
365
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.EXT_COST, unitPurchasePriceBI: 200 });
|
|
366
|
+
expect(result.recommendedMinimumPrice).toBe(230);
|
|
367
|
+
expect(result.floorPrice).toBe(230);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
describe('COMP_SVC', () => {
|
|
371
|
+
it('recommendedMinimumPrice = salePricePVP × 0.50, floorPrice = 0', () => {
|
|
372
|
+
// $500 * 0.50 = $250 → round5 = $250
|
|
373
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.COMP_SVC });
|
|
374
|
+
expect(result.recommendedMinimumPrice).toBe(250);
|
|
375
|
+
expect(result.floorPrice).toBe(0);
|
|
376
|
+
});
|
|
377
|
+
it('should round salePricePVP fraction to nearest 5', () => {
|
|
378
|
+
// $330 * 0.50 = $165 → round5 = $165
|
|
379
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.COMP_SVC, salePricePVP: 330 });
|
|
380
|
+
expect(result.recommendedMinimumPrice).toBe(165);
|
|
381
|
+
expect(result.floorPrice).toBe(0);
|
|
382
|
+
});
|
|
383
|
+
it('should return null recommendedMinimumPrice when salePricePVP is 0', () => {
|
|
384
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: pricing_constants_1.PricePolicy.COMP_SVC, salePricePVP: 0 });
|
|
385
|
+
expect(result.recommendedMinimumPrice).toBeNull();
|
|
386
|
+
expect(result.floorPrice).toBe(0);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
describe('no-formula policies (NONE, SPECIAL, MANUAL)', () => {
|
|
390
|
+
it.each([pricing_constants_1.PricePolicy.NONE, pricing_constants_1.PricePolicy.SPECIAL, pricing_constants_1.PricePolicy.MANUAL])('%s: both null', (policy) => {
|
|
391
|
+
const result = (0, pricing_constants_1.calculateFloorPrices)({ ...defaults, pricePolicy: policy });
|
|
392
|
+
expect(result.recommendedMinimumPrice).toBeNull();
|
|
393
|
+
expect(result.floorPrice).toBeNull();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|
|
@@ -103,6 +103,10 @@ export interface UpdateCatalogItemHvpDataRequest {
|
|
|
103
103
|
pricingTier?: PricingTier;
|
|
104
104
|
/** Recommended price PVP (calculated from policy + tier + cost) */
|
|
105
105
|
recommendedPrice?: number;
|
|
106
|
+
/** Recommended minimum price PVP (calculated from policy + cost) */
|
|
107
|
+
recommendedMinimumPrice?: number;
|
|
108
|
+
/** Floor price PVP (calculated from policy + cost) */
|
|
109
|
+
floorPrice?: number;
|
|
106
110
|
/** How stock levels are managed */
|
|
107
111
|
stockPolicy?: StockPolicy;
|
|
108
112
|
/** Observations generated by analysis script */
|
|
@@ -135,6 +135,10 @@ export interface CatalogItemDetailResponse {
|
|
|
135
135
|
* Calculated from: unitCostBI * markupFactor * (1 + VAT/100)
|
|
136
136
|
*/
|
|
137
137
|
recommendedPrice?: number;
|
|
138
|
+
/** Precio mínimo recomendado PVP (con IVA) — suggested QVET "tarifa mínima" */
|
|
139
|
+
recommendedMinimumPrice?: number;
|
|
140
|
+
/** Precio piso PVP (con IVA) — absolute minimum, never sell below this */
|
|
141
|
+
floorPrice?: number;
|
|
138
142
|
/** How stock levels are managed */
|
|
139
143
|
stockPolicy?: StockPolicy;
|
|
140
144
|
/** Observations generated by analysis script */
|