hvp-shared 6.45.1 → 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.
@@ -25,8 +25,6 @@ export declare enum PricePolicy {
25
25
  EXT_COST = "EXT_COST",
26
26
  /** Internal services (priced by market study / competition) */
27
27
  COMP_SVC = "COMP_SVC",
28
- /** Diagnostic tests (TBD — no formula yet) */
29
- TEST = "TEST",
30
28
  /** Individual formula per item */
31
29
  SPECIAL = "SPECIAL",
32
30
  /** Fixed manual price (no formula) */
@@ -130,4 +128,29 @@ export declare function calculateRecommendedPricePVP(params: {
130
128
  unitPurchasePriceBI: number;
131
129
  vatSalePercent: number;
132
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;
133
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
@@ -37,8 +38,6 @@ var PricePolicy;
37
38
  PricePolicy["EXT_COST"] = "EXT_COST";
38
39
  /** Internal services (priced by market study / competition) */
39
40
  PricePolicy["COMP_SVC"] = "COMP_SVC";
40
- /** Diagnostic tests (TBD — no formula yet) */
41
- PricePolicy["TEST"] = "TEST";
42
41
  /** Individual formula per item */
43
42
  PricePolicy["SPECIAL"] = "SPECIAL";
44
43
  /** Fixed manual price (no formula) */
@@ -65,7 +64,6 @@ exports.PRICE_POLICY_LABELS = {
65
64
  [PricePolicy.COST]: 'Insumos / Artículos (por costo)',
66
65
  [PricePolicy.EXT_COST]: 'Servicios externos',
67
66
  [PricePolicy.COMP_SVC]: 'Servicios internos (estudio de mercado)',
68
- [PricePolicy.TEST]: 'Pruebas diagnósticas',
69
67
  [PricePolicy.SPECIAL]: 'Fórmula individual',
70
68
  [PricePolicy.MANUAL]: 'Precio manual',
71
69
  [PricePolicy.NONE]: 'No se vende',
@@ -105,7 +103,6 @@ exports.AUTO_TIER_POLICIES = [
105
103
  /** Policies that have no formula (recommendedPrice = null) */
106
104
  exports.NO_FORMULA_POLICIES = [
107
105
  PricePolicy.COMP_SVC,
108
- PricePolicy.TEST,
109
106
  PricePolicy.SPECIAL,
110
107
  PricePolicy.MANUAL,
111
108
  PricePolicy.NONE,
@@ -139,7 +136,7 @@ exports.ALLOWED_POLICIES_BY_SECTION = {
139
136
  [qvet_catalog_1.QVET_SECTIONS.FARMACIA_INTERNA]: [PricePolicy.PHARM, PricePolicy.PHARM_FRAC, PricePolicy.COST_ML, PricePolicy.SPECIAL, PricePolicy.MANUAL],
140
137
  [qvet_catalog_1.QVET_SECTIONS.INSUMOS_MEDICOS]: [PricePolicy.COST, PricePolicy.SPECIAL, PricePolicy.MANUAL],
141
138
  [qvet_catalog_1.QVET_SECTIONS.OTROS_ARTICULOS]: [PricePolicy.COST, PricePolicy.SPECIAL, PricePolicy.MANUAL],
142
- [qvet_catalog_1.QVET_SECTIONS.SERVICIOS_MEDICOS]: [PricePolicy.COMP_SVC, PricePolicy.TEST, PricePolicy.SPECIAL, PricePolicy.MANUAL],
139
+ [qvet_catalog_1.QVET_SECTIONS.SERVICIOS_MEDICOS]: [PricePolicy.COMP_SVC, PricePolicy.SPECIAL, PricePolicy.MANUAL],
143
140
  [qvet_catalog_1.QVET_SECTIONS.OTROS_SERVICIOS]: [PricePolicy.COMP_SVC, PricePolicy.SPECIAL, PricePolicy.MANUAL],
144
141
  [qvet_catalog_1.QVET_SECTIONS.SERVICIOS_EXTERNOS]: [PricePolicy.EXT_COST, PricePolicy.SPECIAL, PricePolicy.MANUAL],
145
142
  };
@@ -218,6 +215,13 @@ function getDefaultPricePolicy(section, saleUnit, conversionFactor) {
218
215
  // Fallback: manual pricing
219
216
  return { policy: PricePolicy.MANUAL, tier: null };
220
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
+ }
221
225
  /**
222
226
  * Calculate the recommended price PVP (con IVA) for a catalog item.
223
227
  *
@@ -234,6 +238,56 @@ function calculateRecommendedPricePVP(params) {
234
238
  return null;
235
239
  const priceBI = unitPurchasePriceBI * factor;
236
240
  const pricePVP = priceBI * (1 + vatSalePercent / 100);
237
- // Round to nearest multiple of 5
238
- return Math.round(pricePVP / 5) * 5;
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 };
239
293
  }
@@ -100,7 +100,6 @@ describe('getMarkupFactor', () => {
100
100
  });
101
101
  it('should return null for policies without a formula', () => {
102
102
  expect((0, pricing_constants_1.getMarkupFactor)(pricing_constants_1.PricePolicy.COMP_SVC, pricing_constants_1.PricingTier.HIGH_MARGIN)).toBeNull();
103
- expect((0, pricing_constants_1.getMarkupFactor)(pricing_constants_1.PricePolicy.TEST, pricing_constants_1.PricingTier.HIGH_MARGIN)).toBeNull();
104
103
  expect((0, pricing_constants_1.getMarkupFactor)(pricing_constants_1.PricePolicy.SPECIAL, pricing_constants_1.PricingTier.HIGH_MARGIN)).toBeNull();
105
104
  expect((0, pricing_constants_1.getMarkupFactor)(pricing_constants_1.PricePolicy.MANUAL, pricing_constants_1.PricingTier.HIGH_MARGIN)).toBeNull();
106
105
  expect((0, pricing_constants_1.getMarkupFactor)(pricing_constants_1.PricePolicy.NONE, pricing_constants_1.PricingTier.HIGH_MARGIN)).toBeNull();
@@ -265,15 +264,6 @@ describe('calculateRecommendedPricePVP', () => {
265
264
  });
266
265
  expect(result).toBeNull();
267
266
  });
268
- it('should return null for TEST', () => {
269
- const result = (0, pricing_constants_1.calculateRecommendedPricePVP)({
270
- pricePolicy: pricing_constants_1.PricePolicy.TEST,
271
- pricingTier: pricing_constants_1.PricingTier.HIGH_MARGIN,
272
- unitPurchasePriceBI: 100,
273
- vatSalePercent: 16,
274
- });
275
- expect(result).toBeNull();
276
- });
277
267
  it('should return null for SPECIAL', () => {
278
268
  const result = (0, pricing_constants_1.calculateRecommendedPricePVP)({
279
269
  pricePolicy: pricing_constants_1.PricePolicy.SPECIAL,
@@ -322,3 +312,85 @@ describe('calculateRecommendedPricePVP', () => {
322
312
  expect(result).toBe(60);
323
313
  });
324
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 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hvp-shared",
3
- "version": "6.45.1",
3
+ "version": "6.47.0",
4
4
  "description": "Shared types and utilities for HVP backend and frontend",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",