diff --git a/compiled/index.js b/compiled/index.js index 00341d6..9b7abdd 100644 --- a/compiled/index.js +++ b/compiled/index.js @@ -4,5 +4,6 @@ module.exports = { vendor: require('./vendor'), flights: require('./flights'), offer: require('./offer'), - currency: require('./currency') + currency: require('./currency'), + occupancy: require('./occupancy') }; \ No newline at end of file diff --git a/compiled/occupancy/index.js b/compiled/occupancy/index.js new file mode 100644 index 0000000..a21ffc5 --- /dev/null +++ b/compiled/occupancy/index.js @@ -0,0 +1,89 @@ +"use strict"; + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +var match = function match(occupancy) { + return !(occupancy.match(/^[0-9]{1,2}-[0-9]{1,2}-[0-9]{1,2}?$/gi) == null) || !(occupancy.match(/^[0-9]{1,2}(-[0-9]{1,2}?(,[0-9]{1,2})*)?$/ig) == null); +}; + +var parse = function parse(occupancy) { + if (occupancy.match(/^[0-9]{1,2}(-[0-9]{1,2}?(,[0-9]{1,2})*)?$/ig)) { + var _occupancy$split = occupancy.split('-'), + _occupancy$split2 = _slicedToArray(_occupancy$split, 2), + adults = _occupancy$split2[0], + ages = _occupancy$split2[1]; + + var children = 0; + var childrenAges = []; + + if (ages) { + childrenAges = ages.split(',').map(function (x) { + return Number(x); + }); + children = childrenAges.length; + } + + return { + adults: Number(adults), + children: Number(children), + infants: 0, + childrenAges: childrenAges + }; + } else { + var _occupancy$split3 = occupancy.split('-'), + _occupancy$split4 = _slicedToArray(_occupancy$split3, 3), + _adults = _occupancy$split4[0], + _children = _occupancy$split4[1], + infants = _occupancy$split4[2]; + + return { + adults: Number(_adults), + children: Number(_children), + infants: Number(infants), + childrenAges: [] + }; + } +}; + +var toString = function toString(occupancy) { + if (occupancy.childrenAges && occupancy.childrenAges.length) { + return "".concat(occupancy.adults, "-").concat(occupancy.childrenAges.join(',')); + } else { + return "".concat(occupancy.adults, "-").concat(occupancy.children, "-").concat(occupancy.infants); + } +}; + +var get = function get(occupancy) { + var occupancies = []; + + if (typeof occupancy === 'string') { + if (occupancy.split(',').every(function (occupancy) { + return !!occupancy.match(/^[0-9]{1,2}-[0-9]{1,2}-[0-9]{1,2}?$/gi); + })) { + occupancies = occupancy.split(',').map(parse); + } else { + occupancies = [occupancy].map(parse); + } + } else { + occupancies = occupancy.map(parse); + } + + return occupancies; +}; + +module.exports = { + parse: parse, + get: get, + match: match, + toString: toString +}; \ No newline at end of file diff --git a/compiled/offer/pricing.js b/compiled/offer/pricing.js index 13eed7c..bf8c5e3 100644 --- a/compiled/offer/pricing.js +++ b/compiled/offer/pricing.js @@ -1,18 +1,95 @@ "use strict"; -var calculateTaxAmount = function calculateTaxAmount(_ref) { - var price = _ref.price, - taxesAndFees = _ref.taxesAndFees, - numberOfNights = _ref.numberOfNights; +var countOfMembers = function countOfMembers(occupancies) { + return (occupancies || []).reduce(function (acc, occupancy) { + return acc + occupancy.adults + (occupancy.children || 0) + (occupancy.infants || 0); + }, 0) || 2; +}; + +var perNight = function perNight(_ref) { + var total = _ref.total, + unit = _ref.unit, + value = _ref.value, + nights = _ref.nights, + perPerson = _ref.perPerson, + occupancies = _ref.occupancies; + var members = perPerson ? countOfMembers(occupancies) : 1; + + if (unit === 'percentage') { + return total / 100 * value * members; + } else { + return value * nights * members; + } +}; + +var perStay = function perStay(_ref2) { + var total = _ref2.total, + unit = _ref2.unit, + value = _ref2.value, + perPerson = _ref2.perPerson, + occupancies = _ref2.occupancies; + var members = perPerson ? countOfMembers(occupancies) : 1; + + if (unit === 'percentage') { + return total / 100 * value * members; + } else { + return value * members; + } +}; +/** + * Extract and validate the offer and offer package for an order + * + * interface TaxesAndFees { + * name: string; + * unit: "percentage" | "amount"; + * type: "night" | "stay"; + * per_person: boolean; + * value: number; + * } + * + * interface Occupants { + * adults: number; + * children?: number; + * infants?: number; + * childrenAge?: Array; + * } + * + * @param {object} params - All params + * @param {number} params.total - The total amount of booking period + * @param {Array} params.taxesAndFees - The orders currency code + * @param {number} params.nights - The number of nights + * @param {Array} params.occupancies - The occupancies + * @returns {number} Sum of taxes and fees + */ + + +var calculateTaxAmount = function calculateTaxAmount(_ref3) { + var total = _ref3.total, + taxesAndFees = _ref3.taxesAndFees, + nights = _ref3.nights, + occupancies = _ref3.occupancies; - if (taxesAndFees && price) { + if (taxesAndFees && total) { return Math.floor(taxesAndFees.reduce(function (acc, item) { var tax = 0; - if (item.unit === 'percentage') { - tax = price / 100 * item.value; + if (item.type === 'stay') { + tax = perStay({ + total: total, + unit: item.unit, + value: item.value, + perPerson: item.per_person, + occupancies: occupancies + }); } else { - tax = item.value * numberOfNights; + tax = perNight({ + total: total, + unit: item.unit, + value: item.value, + nights: nights, + perPerson: item.per_person, + occupancies: occupancies + }); } return acc + tax; diff --git a/index.d.ts b/index.d.ts index 83dc434..6a7c030 100644 --- a/index.d.ts +++ b/index.d.ts @@ -18,8 +18,17 @@ declare module "@luxuryescapes/lib-global" { interface TaxesAndFees { name: string; unit: "percentage" | "amount"; + type: "night" | "stay"; + per_person: boolean; value: number; } + + interface Occupants { + adults: number; + children?: number; + infants?: number; + childrenAge?: Array; + } export type LeHotelOfferType = | "hotel" @@ -52,9 +61,15 @@ declare module "@luxuryescapes/lib-global" { getFromPackages: (packages: Array, offerType: string, holidayTypes: Array) => string; }; pricing: { - calculateTaxAmount: ({ price, taxesAndFees, numberOfNights }: { price: number, taxesAndFees: TaxesAndFees[], numberOfNights: number }) => number; + calculateTaxAmount: ({ total, taxesAndFees, nights, occupancies }: { total: number, taxesAndFees: TaxesAndFees[], nights: number, occupancies?: Occupants[] }) => number; }; }; + const occupancy: { + get: (occupancy: string | string[]) => Occupants[]; + parse: (occupancy: string) => Occupants; + match: (occupancy: string) => boolean; + toString: (occupancy: Occupants) => string; + }; const currency: { addDollarType: ( formattedAmount: string, diff --git a/src/index.js b/src/index.js index c24a781..91b8ba2 100644 --- a/src/index.js +++ b/src/index.js @@ -3,4 +3,5 @@ module.exports = { flights: require('./flights'), offer: require('./offer'), currency: require('./currency'), + occupancy: require('./occupancy'), } diff --git a/src/occupancy/index.js b/src/occupancy/index.js new file mode 100644 index 0000000..83e9300 --- /dev/null +++ b/src/occupancy/index.js @@ -0,0 +1,62 @@ +const match = occupancy => !(occupancy.match(/^[0-9]{1,2}-[0-9]{1,2}-[0-9]{1,2}?$/gi) == null) || + !(occupancy.match(/^[0-9]{1,2}(-[0-9]{1,2}?(,[0-9]{1,2})*)?$/ig) == null) + +const parse = (occupancy) => { + if (occupancy.match(/^[0-9]{1,2}(-[0-9]{1,2}?(,[0-9]{1,2})*)?$/ig)) { + const [adults, ages] = occupancy.split('-') + let children = 0 + let childrenAges = [] + + if (ages) { + childrenAges = ages.split(',').map(x => Number(x)) + children = childrenAges.length + } + + return { + adults: Number(adults), + children: Number(children), + infants: 0, + childrenAges, + } + } else { + const [adults, children, infants] = occupancy.split('-') + + return { + adults: Number(adults), + children: Number(children), + infants: Number(infants), + childrenAges: [], + } + } +} + +const toString = occupancy => { + if (occupancy.childrenAges && occupancy.childrenAges.length) { + return `${occupancy.adults}-${occupancy.childrenAges.join(',')}` + } else { + return `${occupancy.adults}-${occupancy.children}-${occupancy.infants}` + } +} + +const get = occupancy => { + let occupancies = [] + + if (typeof occupancy === 'string') { + if (occupancy.split(',').every(occupancy => !!occupancy.match(/^[0-9]{1,2}-[0-9]{1,2}-[0-9]{1,2}?$/gi))) { + occupancies = occupancy.split(',').map(parse) + } else { + occupancies = [occupancy].map(parse) + } + } else { + occupancies = occupancy.map(parse) + } + + return occupancies +} + +module.exports = { + parse: parse, + get: get, + match: match, + toString: toString, +} diff --git a/src/offer/pricing.js b/src/offer/pricing.js index be16b42..089f7ff 100644 --- a/src/offer/pricing.js +++ b/src/offer/pricing.js @@ -1,12 +1,64 @@ -const calculateTaxAmount = ({ price, taxesAndFees, numberOfNights }) => { - if (taxesAndFees && price) { +const countOfMembers = (occupancies) => { + return (occupancies || []).reduce((acc, occupancy) => { + return acc + occupancy.adults + (occupancy.children || 0) + (occupancy.infants || 0) + }, 0) || 2 +} + +const perNight = ({ total, unit, value, nights, perPerson, occupancies }) => { + const members = perPerson ? countOfMembers(occupancies) : 1 + + if (unit === 'percentage') { + return ((total / 100) * value) * members + } else { + return (value * nights) * members + } +} + +const perStay = ({ total, unit, value, perPerson, occupancies }) => { + const members = perPerson ? countOfMembers(occupancies) : 1 + + if (unit === 'percentage') { + return ((total / 100) * value) * members + } else { + return value * members + } +} + +/** + * Extract and validate the offer and offer package for an order + * + * interface TaxesAndFees { + * name: string; + * unit: "percentage" | "amount"; + * type: "night" | "stay"; + * per_person: boolean; + * value: number; + * } + * + * interface Occupants { + * adults: number; + * children?: number; + * infants?: number; + * childrenAge?: Array; + * } + * + * @param {object} params - All params + * @param {number} params.total - The total amount of booking period + * @param {Array} params.taxesAndFees - The orders currency code + * @param {number} params.nights - The number of nights + * @param {Array} params.occupancies - The occupancies + * @returns {number} Sum of taxes and fees + */ +const calculateTaxAmount = ({ total, taxesAndFees, nights, occupancies }) => { + if (taxesAndFees && total) { return Math.floor( taxesAndFees.reduce((acc, item) => { let tax = 0 - if (item.unit === 'percentage') { - tax = (price / 100) * item.value + + if (item.type === 'stay') { + tax = perStay({ total, unit: item.unit, value: item.value, perPerson: item.per_person, occupancies }) } else { - tax = item.value * numberOfNights + tax = perNight({ total, unit: item.unit, value: item.value, nights, perPerson: item.per_person, occupancies }) } return acc + tax diff --git a/test/occupancy/index.test.js b/test/occupancy/index.test.js new file mode 100644 index 0000000..3f0a865 --- /dev/null +++ b/test/occupancy/index.test.js @@ -0,0 +1,80 @@ +const chai = require('chai') + +const { occupancy } = require('../../compiled') + +const expect = chai.expect + +describe('Occupancy', () => { + describe('get()', () => { + it('get old variant of occupancy', () => { + expect(occupancy.get('2-0-0')).to.eql([{ + adults: 2, + children: 0, + infants: 0, + childrenAges: [], + }]) + }) + + it('get old variant of occupancy with multiple entries', () => { + expect(occupancy.get('2-0-0,1-3-0')).to.eql([ + { + adults: 2, + children: 0, + infants: 0, + childrenAges: [], + }, + { + adults: 1, + children: 3, + infants: 0, + childrenAges: [], + }, + ]) + }) + + it('get occupancy with ages', () => { + expect(occupancy.get('2-12,1')).to.eql([ + { + adults: 2, + children: 2, + infants: 0, + childrenAges: [12, 1], + }, + ]) + }) + + it('get occupancy without ages', () => { + expect(occupancy.get('2')).to.eql([ + { + adults: 2, + children: 0, + infants: 0, + childrenAges: [], + }, + ]) + }) + + it('match', async() => { + expect(occupancy.match('2-0-0')).to.eql(true) + expect(occupancy.match('2-1,2')).to.eql(true) + expect(occupancy.match('2-12')).to.eql(true) + expect(occupancy.match('2-1--2')).to.eql(false) + expect(occupancy.match('2,1,2')).to.eql(false) + }) + + it('toString', async() => { + expect(occupancy.toString({ + adults: 2, + children: 2, + infants: 0, + childrenAges: [12, 1], + })).to.eql('2-12,1') + expect(occupancy.toString({ + adults: 4, + children: 2, + infants: 1, + childrenAges: [], + })).to.eql('4-2-1') + }) + }) +}) diff --git a/test/offer/index.test.js b/test/offer/index.test.js index bdfaf16..22b15b7 100644 --- a/test/offer/index.test.js +++ b/test/offer/index.test.js @@ -172,32 +172,104 @@ describe('Offer', () => { }) describe("calculateTaxAmount()", function () { - it("returns the tax is calculated by percentage", function () { + it("returns the tax is calculated by percentage and type per night", function () { expect( offer.pricing.calculateTaxAmount({ - numberOfNights: 1, - price: 102, + nights: 1, + total: 102, taxesAndFees: [{ name: "Test", unit: "percentage", value: 10 }], }) ).to.eql(10) expect( offer.pricing.calculateTaxAmount({ - numberOfNights: 5, - price: 102, + nights: 5, + total: 102 * 5, taxesAndFees: [{ name: "Test", unit: "percentage", value: 10 }], }) - ).to.eql(10) + ).to.eql(51) }) - it("returns the tax is calculated by amount", function () { + it("returns the tax is calculated by amount and type per night", function () { expect( offer.pricing.calculateTaxAmount({ - numberOfNights: 2, - price: 100, + nights: 2, + total: 100 * 2, taxesAndFees: [{ name: "Test", unit: "amount", value: 10 }], }) ).to.eql(20) }) + + it("returns the tax is calculated by percentage and type per stay", function () { + expect( + offer.pricing.calculateTaxAmount({ + nights: 5, + total: 100 * 5, + taxesAndFees: [{ name: "Test", unit: "percentage", value: 10, type: "stay" }], + }) + ).to.eql(50) + }) + + it("returns the tax is calculated by amount and type per stay", function () { + expect( + offer.pricing.calculateTaxAmount({ + nights: 5, + total: 100 * 5, + taxesAndFees: [{ name: "Test", unit: "amount", value: 100, type: "stay" }], + }) + ).to.eql(100) + }) + + it("returns the tax is calculated by percentage and type stay - per person", function () { + expect( + offer.pricing.calculateTaxAmount({ + nights: 5, + total: 100 * 5, + taxesAndFees: [{ name: "Test", unit: "percentage", value: 5, type: "stay", per_person: true }], + occupancies: [{ + adults: 5, + }], + }) + ).to.eql(125) + }) + + it("returns the tax is calculated by amount and type stay - per person", function () { + expect( + offer.pricing.calculateTaxAmount({ + nights: 5, + total: 100 * 5, + taxesAndFees: [{ name: "Test", unit: "amount", value: 5, type: "stay", per_person: true }], + occupancies: [{ + adults: 5, + }], + }) + ).to.eql(25) + }) + + it("returns the tax is calculated by percentage and type night - per person", function () { + expect( + offer.pricing.calculateTaxAmount({ + nights: 5, + total: 100 * 5, + taxesAndFees: [{ name: "Test", unit: "percentage", value: 5, type: "night", per_person: true }], + occupancies: [{ + adults: 5, + }], + }) + ).to.eql(125) + }) + + it("returns the tax is calculated by amount and type night - per person", function () { + expect( + offer.pricing.calculateTaxAmount({ + nights: 5, + total: 100 * 5, + taxesAndFees: [{ name: "Test", unit: "amount", value: 5, type: "night", per_person: true }], + occupancies: [{ + adults: 5, + }], + }) + ).to.eql(125) + }) }) })