Initial commit: monorepo Naturcalabacera reservas (apps/api + apps/web + packages/shared)

This commit is contained in:
2026-04-30 10:09:44 +01:00
commit a0ccb8ca64
188 changed files with 16418 additions and 0 deletions

View 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');
});
});

View 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);
}