import { NetworkProductModuleDefinitionResponse } from '../src/domain/product-module-definition'; import { RootConfigWrite } from '../src/domain/root-config'; export const testModule = ` /* global moment ReactivationOption InvalidRequestError AlteredPolicy AlterationPackage QuotePackage Application Policy generatePolicyNumber Joi RequotePolicy root */ /* eslint-disable @typescript-eslint/no-unused-vars */ function getReactivationOptions(policy) { return [ new ReactivationOption({ type: 'reinstatement', settlementAmount: policy.balance, description: 'For a policy to be reinstated, all arrear premiums must first be paid.', minimumBalanceRequired: true, }), new ReactivationOption({ type: 'recommencement', description: 'For a policy to be recommenced, all arrear premiums will be deducted from the first claim income.', minimumBalanceRequired: false, }), ]; } const dateOfBirthFromID = (id) => { let birthDate = moment(id.toString().slice(0, 6), 'YYMMDD'); if (birthDate.isSameOrAfter(moment())) { birthDate = moment('19' + id.toString().slice(0, 6), 'YYYYMMDD'); } return birthDate; }; const ageFromID = (id) => { const birthDate = dateOfBirthFromID(id); const age = moment().diff(birthDate, 'years'); return age; }; const ageFromDateOfBirth = (dob) => { const birthDate = moment(dob, 'YYYYMMDD'); const age = moment().diff(birthDate, 'years'); return age; }; const policyholderAge = (policyholder) => { const { id } = policyholder; if (id.type === 'id') return ageFromID(id.number); if (!policyholder.dateOfBirth && !policyholder.date_of_birth) throw new Error('Policyholder does not have date of birth'); else return ageFromDateOfBirth(policyholder.dateOfBirth || policyholder.date_of_birth); }; const genderFromID = (id) => { const code = parseInt(id.toString().slice(6, 10), 10); return code < 5000 ? 'female' : 'male'; }; const policyholderGender = (policyholder) => { const { id } = policyholder; return id.type === 'id' ? genderFromID(id.number) : policyholder.gender; }; const csvToObject = (csv) => { const allRows = csv .split('\n') .map((row) => row.split(/\s+/g).map((cell) => (isNaN(Number(cell)) ? cell.toLowerCase() : Number(cell)))); const headings = allRows[0]; const rows = allRows.slice(1); const data = rows.map((row) => row.reduce((acc, cur, i) => { acc[headings[i]] = cur; return acc; }, {}), ); return data; }; const pricingData = { mainMemberAgeRates: csvToObject('Age Male Female Blended_Gender 18 0.4826122829 0.3594848246 0.4296674759'), spouseAgeRates: csvToObject('Age Male Female Blended_Gender 18 0.48544531 0.35678688 0.41211000'), childrenAgeRates: csvToObject('Age Rate 0 0.6209950491'), childrenRate: 0.7755994882, familyRates: csvToObject('Age Male Female Blended_Gender 80 36.08343105 29.37096445 32.72719775'), }; const legalizeChildCoverAmount = (age, cover) => { let legalMax; if (age < 6) legalMax = 10000 * 100; else if (age < 14) legalMax = 10000 * 100; return legalMax < cover ? legalMax : cover; }; function getQuote(data) { const { cover_amount: coverAmount, age, gender, spouse_included: spouseIncluded, spouse_cover_amount: spouseCoverAmount, spouse, children_included: childrenIncluded, children_cover_amount: childrenCoverAmount, children, extended_family_included: extendedFamilyIncluded, extended_family_cover_amount: extendedFamilyCoverAmount, extended_family: extendedFamily, } = data; // Build up the premiums let riskPremium = 0; // Premium: Main policyholder const memberAgeRate = pricingData.mainMemberAgeRates.find((ageRate) => ageRate.age === age); const memberRate = memberAgeRate[gender || 'blended_gender']; riskPremium += (memberRate * coverAmount) / 1000; // Premium: Spouse if (spouseIncluded) { const mainHasGender = !!gender; const spouseHasGender = !!spouse.gender; if (mainHasGender && !spouseHasGender) { throw new InvalidRequestError('Spouse gender required when main member gender is specified'); } else if (!mainHasGender && spouseHasGender) { throw new InvalidRequestError('Spouse gender required when main member gender is specified'); } const spouseAgeRate = pricingData.spouseAgeRates.find((ageRate) => ageRate.age === spouse.age); const spouseRate = spouseAgeRate[spouse.gender || 'blended_gender']; riskPremium += (spouseRate * spouseCoverAmount) / 1000; } // Premium: Children if (childrenIncluded) { if (children) { const childrenPremium = children.reduce((sum, child) => { const childAgeRate = pricingData.childrenAgeRates.find((ageRate) => ageRate.age === child.age); // assuming childAgeRate !== undefined because Joi should confirm within range const legalCoverAmount = legalizeChildCoverAmount(child.age, childrenCoverAmount); const premium = (childAgeRate.rate * legalCoverAmount) / 1000; return sum + premium; }, 0); riskPremium += childrenPremium; } else { riskPremium += (pricingData.childrenRate * childrenCoverAmount) / 1000; } } // Premium: extended family members if (extendedFamilyIncluded) { const extendedFamilyPremium = extendedFamily.reduce((sum, member) => { const familyMemberAgeRate = pricingData.familyRates.find((ageRate) => ageRate.age === member.age); const familyMemberRate = familyMemberAgeRate[member.gender || 'blended_gender']; const premium = (familyMemberRate * extendedFamilyCoverAmount) / 1000; return sum + premium; }, 0); riskPremium += extendedFamilyPremium; } // Build name and terms let nameArray = ['Main Member']; let name; const termsArray = ['No claim for natural death in first 3 months.']; // Build name 1) Spouse if (spouseIncluded) { nameArray.push('Spouse'); } // Build name 2) Children if (childrenIncluded) { nameArray.push('Children'); termsArray.push( 'Cover for children terminates when they reach the age of 21. Legal cover limits apply to children.', ); } // Build name 3) Family if (nameArray.length === 3) { nameArray = ['Family']; } // Build name 4) Extended family if (extendedFamilyIncluded) { name = nameArray.join(', '); name += ' & Extended Family'; } else { name = nameArray.join(' & '); } const basePremium = Math.round(riskPremium / 0.9); // Include platform fee of 10% const suggestedPremium = Math.round(riskPremium / 0.7); // 10% platform + 20% commission return new QuotePackage({ package_name: 'Funeral Cover:' + name, sum_assured: coverAmount, base_premium: basePremium, suggested_premium: suggestedPremium, input_data: data, module: { type: 'root_funeral', cover_amount: coverAmount, age, gender, spouse_included: spouseIncluded, spouse_cover_amount: spouseCoverAmount, spouse, children_included: childrenIncluded, children_cover_amount: childrenCoverAmount, children, extended_family_included: extendedFamilyIncluded, extended_family_cover_amount: extendedFamilyCoverAmount, extended_family: extendedFamily, ...data, }, }); } function getApplication(data, policyholder, quotePackage) { const quoteModule = quotePackage.module; const { spouse, children, extended_family: extendedFamily } = data; // Check children if (quoteModule.children) { // Check number of children matches if (!children || children.length !== quoteModule.children.length) { throw new InvalidRequestError('Incorrect number of children provided. Expected: ' + 'quoteModule.children.length'); } // Check ages match quote const childrenAges = children.map((c) => ageFromDateOfBirth(c.date_of_birth)); const requiredAges = quoteModule.children.map((f) => f.age); if (childrenAges.sort().toString() !== requiredAges.sort().toString()) { throw new InvalidRequestError('Child ages do not match quote. Expected ages: ' + requiredAges.sort().toString()); } } // Check, when spouse' age is specified, if age matches quote if (quoteModule.spouse_included && quoteModule.spouse && quoteModule.spouse.age) { const spouseAge = ageFromDateOfBirth(spouse.date_of_birth); if (quoteModule.spouse.age !== spouseAge) { throw new InvalidRequestError( 'Spouse age does not match age provided in quote. Expected: ' + quoteModule.spouse.age, ); } } // Check extended family members if (quoteModule.extended_family_included) { // Check number of family members if (extendedFamily.length !== quoteModule.extended_family.length) { throw new InvalidRequestError( 'Number of extended family members must match quote. Expected: ' + quoteModule.extended_family.length, ); } // Check ages match quote const ages = extendedFamily.map((f) => ageFromDateOfBirth(f.date_of_birth)); const requiredAges = quoteModule.extended_family.map((f) => f.age); if (ages.sort().toString() !== requiredAges.sort().toString()) { throw new InvalidRequestError( 'Extended family members\' ages do not match quote. Expected ages: ' + requiredAges.sort().toString(), ); } } // Reject people 18 and younger, 65 and older const age = policyholderAge(policyholder); if (age < 18) { throw new InvalidRequestError('Policyholder must be older than 18.'); } else if (age >= 70) { throw new InvalidRequestError("Policyholder can't be older than 70."); } // Make sure policyholder age matches quote if (age !== quoteModule.age) { throw new InvalidRequestError('Policyholder does not match quoted age. Expected: ' + quoteModule.age); } // Make sure policyholder matches gender if (quoteModule.gender) { const gender = policyholderGender(policyholder); if (!gender) { throw new InvalidRequestError('Policyholder does not have gender set, but quote expects: ' + quoteModule.gender + '. Consider updating the policyholder\'s gender, or getting a gender-less (blended) quote instead'); } if (gender !== quoteModule.gender) { throw new InvalidRequestError( 'Policyholder does not match quoted gender. Expected: ' + quoteModule.gender. + 'Consider getting a gender-less (blended) quote instead.', ); } } return new Application({ package_name: quotePackage.package_name, sum_assured: quotePackage.sum_assured, base_premium: quotePackage.base_premium, monthly_premium: quotePackage.suggested_premium, input_data: data, module: { cover_amount: quoteModule.cover_amount, age: quoteModule.age, gender: quoteModule.gender, spouse_included: quoteModule.spouse_included, spouse_cover_amount: quoteModule.spouse_cover_amount, children_included: quoteModule.children_included, children_cover_amount: quoteModule.children_cover_amount, extended_family_included: quoteModule.extended_family_included, extended_family_cover_amount: quoteModule.extended_family_cover_amount, spouse, children, extended_family: extendedFamily, ...data, }, }); } function getPolicy(application, policyholder) { return new Policy({ policy_number: generatePolicyNumber(), package_name: application.package_name, sum_assured: application.sum_assured, base_premium: application.base_premium, monthly_premium: application.monthly_premium, start_date: moment(), end_date: moment().add(1, 'year'), module: { ...application.module }, charges: [ { type: 'fixed', name: 'Fixed Fee', description: 'Fixed Fee', amount: 1000, }, { type: 'variable', name: 'Variable Fee', description: 'Variable Fee', amount: 0.1, }, { type: 'balance', name: 'Balance', description: 'Balance', }, ], }); } const extendedFamilyRelationship = Joi.valid([ 'parent', 'parent_in_law', 'brother', 'sister', 'uncle', 'aunt', 'nephew', 'niece', 'additional_spouse', 'additional_child', ]); const validCountryCodes = ['ZA']; function validateQuoteRequest(data) { return Joi.validate( data, Joi.object() .keys({ cover_amount: Joi.number() .integer() .positive() .min(5000 * 100) .max(100000 * 100) .required(), // Main policyholder age: Joi.number().integer().positive().min(18).max(70).required(), gender: Joi.valid(['male', 'female']).allow(null).optional(), // Spouse spouse_included: Joi.boolean().required(), spouse_cover_amount: Joi.when('spouse_included', { is: true, then: Joi.number() .integer() .positive() .min(5000 * 100) .max(100000 * 100) .required(), otherwise: Joi.allow(null).optional().strip(), // if not included, remove from result }), spouse: Joi.when('spouse_included', { is: true, then: Joi.object() .keys({ age: Joi.number().integer().positive().min(18).max(70).required(), gender: Joi.valid(['male', 'female']).allow(null).optional(), }) .required(), otherwise: Joi.allow(null).optional().strip(), // if not included, remove from result }), // Children children_included: Joi.boolean().required(), children_cover_amount: Joi.when('children_included', { is: true, then: Joi.number() .integer() .positive() .min(1000 * 100) .max(100000 * 100) .required(), otherwise: Joi.allow(null).optional().strip(), }), children: Joi.when('children_included', { is: true, then: Joi.array().items( Joi.object() .keys({ age: Joi.number().integer().min(0).max(21).required(), }) .required(), ), otherwise: Joi.allow(null).optional().strip(), // if not included, remove from result }), // Extended family extended_family_included: Joi.boolean().required(), extended_family_cover_amount: Joi.when('extended_family_included', { is: true, then: Joi.number() .integer() .positive() .min(5000 * 100) .max(100000 * 100) .required(), otherwise: Joi.allow(null).optional().strip(), }), extended_family: Joi.when('extended_family_included', { is: true, then: Joi.array() .items( Joi.object() .keys({ age: Joi.number().integer().min(0).max(80).required(), gender: Joi.valid(['male', 'female']).optional(), }) .required(), ) .required(), otherwise: Joi.allow(null).optional().strip(), // if not included, remove from result }), }) .required(), ); } function validateApplicationRequest(data, policyholder, quotePackage) { const quoteModule = quotePackage.module; return Joi.validate( data, Joi.object() .keys({ spouse: quoteModule.spouse_included ? Joi.object() .keys({ first_name: Joi.string().required(), last_name: Joi.string().required(), date_of_birth: Joi.dateOfBirth().required(), }) .required() : Joi.allow(null).optional().strip(), // Children children: quoteModule.children_included ? Joi.array() .items( Joi.object() .keys({ first_name: Joi.string().required(), last_name: Joi.string().required(), date_of_birth: Joi.dateOfBirth().required(), }) .required(), ) .required() : Joi.allow(null).optional().strip(), // Extended family extended_family: quoteModule.extended_family_included ? Joi.array() .items( Joi.object() .keys({ first_name: Joi.string().required(), last_name: Joi.string().required(), date_of_birth: Joi.dateOfBirth().required(), relationship: extendedFamilyRelationship.required(), }) .required(), ) .required() : Joi.allow(null).optional().strip(), }) .required(), ); } function validateClaimRequest(data) { return Joi.validate( data, Joi.object() .keys({ has_after_policy_linked_to_claim_hook_run: Joi.boolean().optional(), has_after_claim_approved_hook_run: Joi.boolean().optional(), member_deceased: Joi.valid(['main_member', 'other_insured']).required(), incident_type: Joi.valid(['death', 'accidental_death']).required(), incident_cause: Joi.string().optional(), incident_date: Joi.string().required(), date_of_death: Joi.string().isoDate().required(), police_station: Joi.string().when('incident_type', { is: 'accidental_death', then: Joi.required(), otherwise: Joi.forbidden().allow(null), }), police_case_number: Joi.string().when('incident_type', { is: 'accidental_death', then: Joi.required(), otherwise: Joi.forbidden().allow(null), }), deceased: Joi.object() .keys({ id_type: Joi.valid(['id', 'passport']).required(), first_name: Joi.string().required(), last_name: Joi.string().required(), id_number: Joi.string() .idNumber() .when('id_type', { is: 'id', then: Joi.required(), otherwise: Joi.forbidden().allow(null), }), passport_number: Joi.string().when('id_type', { is: 'passport', then: Joi.required(), otherwise: Joi.forbidden().allow(null), }), id_country: Joi.valid(validCountryCodes).when('id_type', { is: 'passport', then: Joi.required(), otherwise: Joi.forbidden().allow(null), }), gender: Joi.string().when('id_type', { is: 'passport', then: Joi.required(), otherwise: Joi.forbidden().allow(null), }), date_of_birth: Joi.string() .isoDate() .when('id_type', { is: 'passport', then: Joi.required(), otherwise: Joi.forbidden().allow(null), }), }) .when('member_deceased', { is: 'main_member', then: Joi.forbidden().allow(null), otherwise: Joi.required(), }), dha_reference_number: Joi.string().required(), doctor: Joi.object() .keys({ name: Joi.string().optional(), contact_number: Joi.string().optional(), address: Joi.string().optional(), }) .optional(), }) .required(), ); }; `; export const embedConfig = { header: { links: {}, images: { titleUrl: '', }, wording: { title: 'Get Dinosure', }, displayOptionalSections: { titleImage: false, }, }, landing: { links: { youTubeVideoId: 'PJlmYh27MHg', }, images: { background: 'https://storage.googleapis.com/root-production-public-client-images/Dinosure%20Demo/dinosure-background.png', }, wording: { title: 'Get Dinosure', subTitle: 'From R149 p/m', learnMore: [], videoTitle: 'Dinosure Emergency Insurance', description: [ 'Dinosure is a best of breed insurance scheme which not only provides life and disability insurance for dinosaurs attacks, but also provides the following services to prevent that eventuality from ever occurring:', ], createQuoteButton: "Let's get started!", descriptionBullets: ['Early warning network', 'Extraction team', 'Security consultants'], }, displayOptionalSections: { learnMore: false, watchVideo: false, descriptionBullets: true, }, }, quote: { links: {}, images: {}, wording: { consentDisclaimer: { title: '', consentItems: [], }, description: "Please complete the steps below to get a personalised quote. Let's dive right in!", coverRejected: 'life cover', }, displayOptionalSections: { consentDisclaimer: true, }, }, prePersonalDetailsCompliance: { wording: { description: 'Let’s sign you up! Read the following terms and conditions before proceeding...', content: '**PersonalDetailsCompliance**\n', }, images: {}, links: {}, displayOptionalSections: { displayPrePersonalDetailsCompliance: true, }, }, personalDetails: { links: {}, images: {}, wording: {}, displayOptionalSections: {}, }, summary: { links: {}, images: {}, wording: { description: "Need a refresher? Let's walk you through your details.", }, displayOptionalSections: {}, }, prePaymentCompliance: { wording: { description: 'Let’s sign you up! Read the following terms and conditions before proceeding...', content: '**prePaymentCompliance**\n', }, images: {}, links: {}, displayOptionalSections: { displayPrePaymentCompliance: true, }, }, payment: { links: {}, images: {}, wording: { declaration: 'Dinosure is a sample application by Root. The source code is MIT licensed, while the website content is licensed CC BY NC SA 4.0. Dinosure Services (Pty) Ltd is an unauthorised financial services provider. FSP number: 123456. 3 Sauropoda Lane, Isla Nublar, 1337', description: 'Almost done! Please complete the payment below.', }, displayOptionalSections: {}, }, confirmation: { links: { openClaim: 'dinoparks.net/claims', redirectOnCompletedUrl: 'issue-policy', }, images: {}, wording: { title: 'Confirmation', subTitle: 'Thank you for taking out insurance with us.', description: 'You are now covered and your policy schedule should arrive in your inbox shortly.', contactNumber: '013 456 7890', }, displayOptionalSections: { contactNumber: true, }, }, footer: { links: {}, images: { disclaimerUrl: 'https://storage.googleapis.com/root-production-public-client-images/Dinosure%20Demo/root-logo.svg', }, wording: { disclaimer: 'Dinosure is a sample application by Root. The source code is MIT licensed, while the website content is licensed CC BY NC SA 4.0. Dinosure Services (Pty) Ltd is an unauthorised financial services provider. FSP number: 123456. 3 Sauropoda Lane, Isla Nublar, 1337', }, displayOptionalSections: { disclaimerImage: true, }, }, styles: { colors: { dark: '#00000', error: '#ff2b38', light: '#FFFFFF', border: 'rgba(0, 0, 0, 0.125)', primary: '#240E8B', success: '#1fc881', warning: '#ffab00', disabled: '#CDD4DE', highlight: '#FC64B1', secondary: '#1fc881', backgroundHighlight: 'rgba(245, 247, 252, 1)', }, fontSize: { body: '14px', title: '40px', button: '14px', subTitle: '22px', subscript: '8px', }, fontFamily: { body: 'Lato', title: 'Lato', }, borderRadius: { button: '50px', }, }, }; export const changedEmbedConfig = { ...embedConfig, header: { ...embedConfig.header, wording: { ...embedConfig.header.wording, title: 'Changed' } }, }; export const cliDefinitionExample: NetworkProductModuleDefinitionResponse = { product_module_key: 'root_funeral', product_module_name: 'Root Funeral', organization_id: '00000000-0000-0000-0000-000000000001', product_module_definition_id: '02b473e8-efa8-4129-8902-8cec5b24bb06', version: '1.1', root_config: { stack: 'root-node-20', settings: { policySchemeType: 'individual', dashboardIssuingEnabled: true, activatePoliciesOnEvent: 'payment_method_assigned', canReactivatePolicies: true, notTakenUpEnabled: true, welcomeLetterEnabled: false, policyDocuments: [ { type: 'policy_schedule', fileName: 'your_policy_schedule_{{policy.policy_number}}', }, { type: 'supplementary_terms', supplementaryTermsType: 'test-type', fileName: 'your_supplementary_terms_{{policy.policy_number}}', }, ], canRequote: true, gracePeriod: { lapseOn: { afterFirstMissedPayment: { period: 15, periodType: 'days', }, consecutiveMissedPayments: null, missedPaymentsOverPolicyTerm: null, missedPaymentsWithinPeriod: null, }, }, waitingPeriod: { applyTo: { theFullPolicy: { period: 30, periodType: 'days', }, }, }, coolingOffPeriod: { applyTo: { theFullPolicy: { refundType: 'all_premiums', period: 1, periodType: 'months', }, }, }, policyAnniversaryNotification: { daysBeforeToSend: 20, }, policyholder: { individualsAllowed: true, individualsIdAllowed: true, individualsPassportAllowed: true, individualsCellphoneAllowed: false, individualsCustomIdAllowed: true, individualsEmailAllowed: false, idCountry: 'ZA', customIdName: 'Custom identifier', companiesAllowed: false, }, beneficiaries: { makePolicyholderABeneficiary: true, min: 0, max: 10, }, claims: { checklistItems: [ 'Are all premiums up to date?', 'Is the incident date or date of death after the inception of cover?', 'Does the date of death fall outside of the waiting period? If no, was the insured covered under a similar policy prior to the commencement date?', 'Is the deceased`s identity and relationship to the main member confirmed as true and is the individual specifically covered under the policy?', 'Has the claim been logged within the permitted time period after the claim event?', 'No exclusions, as per the policy terms and conditions, apply on this claim.', 'Has the insured adhered to the terms and conditions, to the best of your knowledge?', 'Is the beneficiary correct in terms of the policy terms and conditions?', 'Are the beneficiaries` banking details loaded correctly', 'Have any other claims been logged for the policy?', ], documentLabels: ['Proof of claim'], }, billing: { allowStartDateChange: true, billBeforeWeekendEnabled: true, billingFrequency: 'monthly', consecutiveFailedPaymentsAllowed: 4, currency: 'ZAR', enableBillingOnSandbox: true, paymentSubmissionLeadTime: 0, clientStatementReference: 'Root', paymentMethodTypes: { debitOrders: { enabled: true, strategy: 'best_effort', naedoPoliciesInArrears: true, }, card: { enabled: false, }, eft: { enabled: false, }, external: { enabled: false, createPayments: false, }, }, retryFailedPayments: { enabled: true, daysBetweenRetries: 5, numberOfRetries: 1, }, proRataBilling: { enabled: true, proRataBillingOnIssue: true, minimumAmount: 1000, }, }, }, scheduledFunctions: [ { functionName: 'customScheduledFunction', policyStatuses: ['active', 'pending_initial_payment'], frequency: { type: 'daily', timeOfDay: '10:00', }, }, ], fulfillmentTypes: [ { key: 'airtime', label: 'Airtime', fulfillmentData: { airtime: { label: 'Airtime amount', isValue: true, valueType: 'currency', }, cellphone_number: { label: 'Cellphone number', valueType: 'string', }, }, }, { key: 'cds_account', label: 'CDS account', fulfillmentData: { cds_account: { label: 'Settlement amount', isValue: true, valueType: 'currency', }, cds_account_number: { label: 'CDS account number', valueType: 'string', }, }, }, ], }, readme_markdown: '#This is markdown\n', documents: { welcome_letter_html: '