Initial commit: monorepo Naturcalabacera reservas (apps/api + apps/web + packages/shared)
This commit is contained in:
114
packages/shared/src/utils/pricing.test.ts
Normal file
114
packages/shared/src/utils/pricing.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateNaturPrice, formatPrice } from './pricing.js';
|
||||
|
||||
describe('calculateNaturPrice — Los Dragos', () => {
|
||||
it('calculates base price for 1 night, within included persons', () => {
|
||||
const result = calculateNaturPrice({ property: 'los_dragos', nights: 1, totalPersons: 10, igicRate: 0 });
|
||||
expect(result.basePrice).toBe(900);
|
||||
expect(result.extraPersonsFee).toBe(0);
|
||||
expect(result.subtotal).toBe(900);
|
||||
expect(result.total).toBe(900);
|
||||
expect(result.extraPersons).toBe(0);
|
||||
expect(result.includedPersons).toBe(50);
|
||||
});
|
||||
|
||||
it('calculates extra persons fee above 50 included', () => {
|
||||
// 55 persons = 5 extra, 2 nights, extra fee = 5 * 12 * 2 = 120
|
||||
const result = calculateNaturPrice({ property: 'los_dragos', nights: 2, totalPersons: 55, igicRate: 0 });
|
||||
expect(result.basePrice).toBe(1800);
|
||||
expect(result.extraPersons).toBe(5);
|
||||
expect(result.extraPersonsFee).toBe(120);
|
||||
expect(result.subtotal).toBe(1920);
|
||||
expect(result.total).toBe(1920);
|
||||
});
|
||||
|
||||
it('applies IGIC at 7%', () => {
|
||||
const result = calculateNaturPrice({ property: 'los_dragos', nights: 1, totalPersons: 1, igicRate: 0.07 });
|
||||
expect(result.subtotal).toBe(900);
|
||||
expect(result.igicAmount).toBe(63);
|
||||
expect(result.total).toBe(963);
|
||||
});
|
||||
|
||||
it('returns zeros when nights is 0', () => {
|
||||
const result = calculateNaturPrice({ property: 'los_dragos', nights: 0, totalPersons: 20, igicRate: 0.07 });
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.basePrice).toBe(0);
|
||||
});
|
||||
|
||||
it('returns zeros when totalPersons is 0', () => {
|
||||
const result = calculateNaturPrice({ property: 'los_dragos', nights: 3, totalPersons: 0, igicRate: 0.07 });
|
||||
expect(result.total).toBe(0);
|
||||
});
|
||||
|
||||
it('uses default IGIC rate when not provided', () => {
|
||||
const withDefault = calculateNaturPrice({ property: 'los_dragos', nights: 1, totalPersons: 1 });
|
||||
const with7Pct = calculateNaturPrice({ property: 'los_dragos', nights: 1, totalPersons: 1, igicRate: 0.07 });
|
||||
expect(withDefault.total).toBe(with7Pct.total);
|
||||
});
|
||||
|
||||
it('exacty at included person limit produces no extra fee', () => {
|
||||
const result = calculateNaturPrice({ property: 'los_dragos', nights: 1, totalPersons: 50, igicRate: 0 });
|
||||
expect(result.extraPersons).toBe(0);
|
||||
expect(result.extraPersonsFee).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateNaturPrice — La Esquinita', () => {
|
||||
it('calculates base price at 1450€/night', () => {
|
||||
const result = calculateNaturPrice({ property: 'la_esquinita', nights: 1, totalPersons: 10, igicRate: 0 });
|
||||
expect(result.basePrice).toBe(1450);
|
||||
expect(result.extraPersonsFee).toBe(0);
|
||||
expect(result.includedPersons).toBe(60);
|
||||
});
|
||||
|
||||
it('calculates extra persons above 60 included', () => {
|
||||
// 65 persons = 5 extra, 1 night, extra fee = 5 * 12 = 60
|
||||
const result = calculateNaturPrice({ property: 'la_esquinita', nights: 1, totalPersons: 65, igicRate: 0 });
|
||||
expect(result.extraPersons).toBe(5);
|
||||
expect(result.extraPersonsFee).toBe(60);
|
||||
expect(result.subtotal).toBe(1510);
|
||||
});
|
||||
|
||||
it('applies IGIC to subtotal including extras', () => {
|
||||
// subtotal = 1510, IGIC 7% = 105.70, total = 1615.70
|
||||
const result = calculateNaturPrice({ property: 'la_esquinita', nights: 1, totalPersons: 65, igicRate: 0.07 });
|
||||
expect(result.igicAmount).toBe(105.7);
|
||||
expect(result.total).toBe(1615.7);
|
||||
});
|
||||
|
||||
it('rounds IGIC to 2 decimal places', () => {
|
||||
// 1 night, 1 person (within included), subtotal 1450, IGIC 7% = 101.5
|
||||
const result = calculateNaturPrice({ property: 'la_esquinita', nights: 1, totalPersons: 1, igicRate: 0.07 });
|
||||
expect(result.igicAmount).toBe(101.5);
|
||||
// Check it's a clean number (not e.g. 101.50000000001)
|
||||
expect(Number.isFinite(result.igicAmount)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateNaturPrice — configurable IGIC rate', () => {
|
||||
it('supports 0% IGIC', () => {
|
||||
const result = calculateNaturPrice({ property: 'los_dragos', nights: 1, totalPersons: 10, igicRate: 0 });
|
||||
expect(result.igicAmount).toBe(0);
|
||||
expect(result.total).toBe(result.subtotal);
|
||||
});
|
||||
|
||||
it('supports 10% IGIC', () => {
|
||||
const result = calculateNaturPrice({ property: 'los_dragos', nights: 1, totalPersons: 10, igicRate: 0.1 });
|
||||
expect(result.igicAmount).toBe(90);
|
||||
expect(result.total).toBe(990);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPrice', () => {
|
||||
it('formats as euros with Spanish locale', () => {
|
||||
// Note: Intl output can vary by environment; test the structure
|
||||
const result = formatPrice(1234.5);
|
||||
expect(result).toContain('€');
|
||||
expect(result).toContain('1');
|
||||
});
|
||||
|
||||
it('handles zero', () => {
|
||||
const result = formatPrice(0);
|
||||
expect(result).toContain('0');
|
||||
});
|
||||
});
|
||||
68
packages/shared/src/utils/pricing.ts
Normal file
68
packages/shared/src/utils/pricing.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { PROPERTY_CONFIG, DEFAULT_IGIC_RATE, getExtraPersonRate } from '../constants/properties.js';
|
||||
import type { PricingInput, PricingResult } from '../types/pricing.js';
|
||||
|
||||
/**
|
||||
* Calcula el precio de una reserva Naturcalabacera.
|
||||
*
|
||||
* Reglas:
|
||||
* - Los Dragos: 900€/noche base, hasta 50 personas incluidas
|
||||
* - La Esquinita: 1450€/noche base, hasta 60 personas incluidas
|
||||
* - Tarifa por persona extra: 12€ en 2026, 14€ desde 2027 (configurable por año)
|
||||
* - IGIC se aplica sobre el subtotal (base + extras)
|
||||
* - Si nights <= 0 o totalPersons <= 0, retorna todo a cero
|
||||
*/
|
||||
export function calculateNaturPrice(input: PricingInput): PricingResult {
|
||||
const {
|
||||
property,
|
||||
nights,
|
||||
totalPersons,
|
||||
igicRate = DEFAULT_IGIC_RATE,
|
||||
year = new Date().getFullYear(),
|
||||
extraPersonRateOverride,
|
||||
} = input;
|
||||
const config = PROPERTY_CONFIG[property].pricing;
|
||||
const extraPersonRate = extraPersonRateOverride ?? getExtraPersonRate(property, year);
|
||||
|
||||
if (nights <= 0 || totalPersons <= 0) {
|
||||
return {
|
||||
basePrice: 0,
|
||||
extraPersonsFee: 0,
|
||||
subtotal: 0,
|
||||
igicAmount: 0,
|
||||
total: 0,
|
||||
includedPersons: config.includedPersons,
|
||||
extraPersons: 0,
|
||||
extraPersonRate,
|
||||
};
|
||||
}
|
||||
|
||||
const extraPersons = Math.max(0, totalPersons - config.includedPersons);
|
||||
const basePrice = config.baseRatePerNight * nights;
|
||||
const extraPersonsFee = extraPersons * extraPersonRate * nights;
|
||||
const subtotal = basePrice + extraPersonsFee;
|
||||
const igicAmount = Math.round(subtotal * igicRate * 100) / 100;
|
||||
const total = Math.round((subtotal + igicAmount) * 100) / 100;
|
||||
|
||||
return {
|
||||
basePrice,
|
||||
extraPersonsFee,
|
||||
subtotal,
|
||||
igicAmount,
|
||||
total,
|
||||
includedPersons: config.includedPersons,
|
||||
extraPersons,
|
||||
extraPersonRate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea un número como precio en euros.
|
||||
* Ej: 1234.5 → "1.234,50 €"
|
||||
*/
|
||||
export function formatPrice(amount: number): string {
|
||||
return new Intl.NumberFormat('es-ES', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
Reference in New Issue
Block a user