import type { PackageData, NearbyVerifiedProvider } from '../../../components/pages/PackagesStep'; import type { PackageSection, PackageLineItem } from '../../../components/organisms/PackageDetail'; import type { ComparisonPackage, ComparisonSection, } from '../../../components/organisms/ComparisonTable'; import { providers, providersById } from './providers'; /** * Packages live keyed by providerId. Each provider has TWO PackageData lists * (matching = primary "matching your preferences", other = secondary "more * from this provider") and a parallel ComparisonPackage projection that * concatenates both for use by ComparisonPage. Same `id` across all three * lists so the basket can hold a single compound key: * `${providerId}:${packageId}`. * * Verified-provider packages all share the same nine canonical Essentials * line items per FA convention — only prices/treatment vary. Optionals and * Extras are free to vary per package. See memory: * project_canonical_essentials.md for the rule and reasoning. */ interface PackageBundle { matching: PackageData[]; other: PackageData[]; forComparison: ComparisonPackage[]; } // ─── Canonical Essentials factories ────────────────────────────────────────── type IncludedTreatment = 'complimentary' | 'included'; interface EssentialsPrices { coffin: number; // rendered as allowance cremationCert: number; crematorium: number; deathReg: number; dressing: number | IncludedTreatment; govLevy: number; govLevyLabel?: string; // override to "Government Levy — Cremation" for non-NSW mortuary: number; service: number; transport: number | IncludedTreatment; } const labelFor = (kind: IncludedTreatment): string => kind === 'complimentary' ? 'Complimentary' : 'Included'; function essentialsForStep(p: EssentialsPrices): PackageSection { const item = (name: string, value: number | IncludedTreatment): PackageLineItem => typeof value === 'number' ? { name, price: value } : { name, price: 0, priceLabel: labelFor(value) }; return { heading: 'Essentials', items: [ { name: 'Allowance for Coffin', price: p.coffin, isAllowance: true }, item('Cremation Certificate/Permit', p.cremationCert), item('Crematorium', p.crematorium), item('Death Registration Certificate', p.deathReg), item('Dressing Fee', p.dressing), item(p.govLevyLabel ?? 'NSW Government Levy — Cremation', p.govLevy), item('Professional Mortuary Care', p.mortuary), item('Professional Service Fee', p.service), item('Transportation Service Fee', p.transport), ], }; } function essentialsForComparison(p: EssentialsPrices): ComparisonSection { const cell = (value: number | IncludedTreatment) => typeof value === 'number' ? ({ type: 'price', amount: value } as const) : ({ type: value } as const); return { heading: 'Essentials', items: [ { name: 'Allowance for Coffin', info: 'Allowance amount — upgrade options available.', value: { type: 'allowance', amount: p.coffin }, }, { name: 'Cremation Certificate/Permit', info: 'Statutory medical referee fee.', value: cell(p.cremationCert), }, { name: 'Crematorium', info: 'Cremation facility fees.', value: cell(p.crematorium) }, { name: 'Death Registration Certificate', info: 'Lodgement with the registry.', value: cell(p.deathReg), }, { name: 'Dressing Fee', info: 'Dressing and preparation.', value: cell(p.dressing) }, { name: p.govLevyLabel ?? 'NSW Government Levy — Cremation', info: 'Government cremation levy.', value: cell(p.govLevy), }, { name: 'Professional Mortuary Care', info: 'Preparation and care.', value: cell(p.mortuary), }, { name: 'Professional Service Fee', info: 'Coordination of arrangements.', value: cell(p.service), }, { name: 'Transportation Service Fee', info: 'Transfer of the deceased.', value: cell(p.transport), }, ], }; } const sumEssentials = (p: EssentialsPrices): number => { const num = (v: number | IncludedTreatment) => (typeof v === 'number' ? v : 0); return ( p.coffin + num(p.cremationCert) + num(p.crematorium) + num(p.deathReg) + num(p.dressing) + p.govLevy + p.mortuary + p.service + num(p.transport) ); }; // ─── Optionals helpers ─────────────────────────────────────────────────────── // // Rule: Optionals an Extras list an item ONLY when that package actually // offers it. Missing items are not placeholders — they're just absent. In // PackageDetail that means the row isn't rendered. In ComparisonTable the // cross-join (see `buildMergedSections` + `lookupValue`) surfaces the row // as "Not Included" for any column that doesn't include it. interface Optional { name: string; treatment: IncludedTreatment; } const optionalsForStep = (items: Optional[]): PackageSection => ({ heading: 'Optionals', items: items.map((it) => ({ name: it.name, price: 0, priceLabel: labelFor(it.treatment), })), }); const optionalsForComparison = (items: Optional[]): ComparisonSection => ({ heading: 'Optionals', items: items.map((it) => ({ name: it.name, value: { type: it.treatment }, })), }); // ─── Extras helpers ────────────────────────────────────────────────────────── interface Extra { name: string; /** number = fixed price; { allowance } = allowance amount; 'poa' = price on application; 'complimentary' = free */ value: number | { allowance: number } | 'poa' | 'complimentary'; } const extrasForStep = (items: Extra[]): PackageSection => ({ heading: 'Extras', items: items.map((it) => { if (typeof it.value === 'number') return { name: it.name, price: it.value }; if (it.value === 'poa') return { name: it.name, price: 0, priceLabel: 'POA' }; if (it.value === 'complimentary') return { name: it.name, price: 0, priceLabel: 'Complimentary' }; return { name: it.name, price: it.value.allowance, isAllowance: true }; }), }); const extrasForComparison = (items: Extra[]): ComparisonSection => ({ heading: 'Extras', items: items.map((it) => ({ name: it.name, value: typeof it.value === 'number' ? { type: 'price', amount: it.value } : it.value === 'poa' ? { type: 'poa' } : it.value === 'complimentary' ? { type: 'complimentary' } : { type: 'allowance', amount: it.value.allowance }, })), }); // ─── H.Parsons (verified — premium tier, complimentary treatments) ─────────── const parsonsEverydayEssentials: EssentialsPrices = { coffin: 1750, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'complimentary', govLevy: 45.1, mortuary: 440, service: 3650.9, transport: 'complimentary', }; const parsonsEssentialEssentials: EssentialsPrices = { coffin: 1200, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'complimentary', govLevy: 45.1, mortuary: 440, service: 1729.9, transport: 'complimentary', }; const parsonsDeluxeEssentials: EssentialsPrices = { coffin: 2500, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'complimentary', govLevy: 45.1, mortuary: 440, service: 5384.9, transport: 'complimentary', }; const parsonsTraditionalEssentials: EssentialsPrices = { coffin: 2200, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'complimentary', govLevy: 45.1, mortuary: 440, service: 4534.9, transport: 'complimentary', }; const parsonsMemorialEssentials: EssentialsPrices = { coffin: 900, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'complimentary', govLevy: 45.1, mortuary: 440, service: 3034.9, transport: 'complimentary', }; const parsonsForStep: PackageData[] = [ { id: 'everyday', name: 'Everyday Funeral Package', price: sumEssentials(parsonsEverydayEssentials), description: 'Funeral service at a chapel or church with a procession. Includes the most commonly selected funeral options.', sections: [ essentialsForStep(parsonsEverydayEssentials), optionalsForStep([ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, { name: 'Flowers', treatment: 'complimentary' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 550 } }, { name: 'Catering', value: 'poa' }, { name: 'Newspaper Notice', value: 'poa' }, { name: 'Saturday Service Fee', value: 880 }, ]), }, { id: 'essential', name: 'Essential Funeral Package', price: sumEssentials(parsonsEssentialEssentials), description: 'A simple, dignified option covering the essential requirements for a cremation.', sections: [ essentialsForStep(parsonsEssentialEssentials), optionalsForStep([ { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 400 } }, { name: 'Catering', value: 'poa' }, ]), }, { id: 'deluxe', name: 'Deluxe Funeral Package', price: sumEssentials(parsonsDeluxeEssentials), description: 'Premium inclusions, higher-quality coffin selection, expanded service options for a more personalised farewell.', sections: [ essentialsForStep(parsonsDeluxeEssentials), optionalsForStep([ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, { name: 'Flowers', treatment: 'complimentary' }, { name: 'Webstreaming', treatment: 'complimentary' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 700 } }, { name: 'Catering', value: 1200 }, { name: 'Newspaper Notice', value: 350 }, { name: 'Saturday Service Fee', value: 'complimentary' }, ]), }, { id: 'traditional-burial', name: 'Traditional Burial Package', price: sumEssentials(parsonsTraditionalEssentials), description: 'A traditional funeral service followed by burial, including a full procession and graveside committal.', sections: [ essentialsForStep(parsonsTraditionalEssentials), optionalsForStep([ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, { name: 'Flowers', treatment: 'complimentary' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 600 } }, { name: 'Catering', value: 1100 }, { name: 'Newspaper Notice', value: 350 }, { name: 'Saturday Service Fee', value: 880 }, ]), }, { id: 'memorial-service', name: 'Memorial Service Package', price: sumEssentials(parsonsMemorialEssentials), description: 'A memorial service held after the cremation, giving families time to plan a personalised gathering.', sections: [ essentialsForStep(parsonsMemorialEssentials), optionalsForStep([ { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, { name: 'Flowers', treatment: 'complimentary' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 500 } }, { name: 'Catering', value: 'poa' }, { name: 'Newspaper Notice', value: 280 }, ]), }, ]; const parsonsForComparison: ComparisonPackage[] = parsonsForStep.map((pkg, idx) => { const ess = [ parsonsEverydayEssentials, parsonsEssentialEssentials, parsonsDeluxeEssentials, parsonsTraditionalEssentials, parsonsMemorialEssentials, ][idx]; const allOptionals: Optional[][] = [ [ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, { name: 'Flowers', treatment: 'complimentary' }, ], [ { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, ], [ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, { name: 'Flowers', treatment: 'complimentary' }, { name: 'Webstreaming', treatment: 'complimentary' }, ], [ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, { name: 'Flowers', treatment: 'complimentary' }, ], [ { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, { name: 'Flowers', treatment: 'complimentary' }, ], ]; const optionals = allOptionals[idx]; const extras: Extra[] = [ [ { name: 'Allowance for Celebrant', value: { allowance: 550 } }, { name: 'Catering', value: 'poa' as const }, { name: 'Newspaper Notice', value: 'poa' as const }, { name: 'Saturday Service Fee', value: 880 }, ], [ { name: 'Allowance for Celebrant', value: { allowance: 400 } }, { name: 'Catering', value: 'poa' as const }, ], [ { name: 'Allowance for Celebrant', value: { allowance: 700 } }, { name: 'Catering', value: 1200 }, { name: 'Newspaper Notice', value: 350 }, { name: 'Saturday Service Fee', value: 'complimentary' as const }, ], [ { name: 'Allowance for Celebrant', value: { allowance: 600 } }, { name: 'Catering', value: 1100 }, { name: 'Newspaper Notice', value: 350 }, { name: 'Saturday Service Fee', value: 880 }, ], [ { name: 'Allowance for Celebrant', value: { allowance: 500 } }, { name: 'Catering', value: 'poa' as const }, { name: 'Newspaper Notice', value: 280 }, ], ][idx]; return { id: pkg.id, name: pkg.name, price: pkg.price, provider: { name: 'H.Parsons Funeral Directors', location: 'Wentworth, NSW', logoUrl: providersById['parsons'].logoUrl, rating: 4.6, reviewCount: 7, verified: true, }, sections: [ essentialsForComparison(ess), optionalsForComparison(optionals), extrasForComparison(extras), ], }; }); // ─── Rankins (verified — mid-market, "included" treatment) ─────────────────── const rankinsStandardEssentials: EssentialsPrices = { coffin: 1500, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 2430.35, transport: 'included', }; const rankinsPremiumEssentials: EssentialsPrices = { coffin: 2200, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 4034.9, transport: 'included', }; const rankinsDirectEssentials: EssentialsPrices = { coffin: 800, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 434.9, transport: 'included', }; const rankinsForStep: PackageData[] = [ { id: 'standard', name: 'Standard Cremation Package', price: sumEssentials(rankinsStandardEssentials), description: 'A balanced cremation package suitable for most families.', sections: [ essentialsForStep(rankinsStandardEssentials), optionalsForStep([ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 450 } }, { name: 'Catering', value: 'poa' }, { name: 'Saturday Service Fee', value: 750 }, ]), }, { id: 'premium', name: 'Premium Funeral Service', price: sumEssentials(rankinsPremiumEssentials), description: 'A more personalised service with venue and celebrant coordination.', sections: [ essentialsForStep(rankinsPremiumEssentials), optionalsForStep([ { name: 'Digital Recording', treatment: 'included' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 600 } }, { name: 'Catering', value: 950 }, { name: 'Newspaper Notice', value: 280 }, ]), }, { id: 'direct-cremation', name: 'Direct Cremation', price: sumEssentials(rankinsDirectEssentials), description: 'An unattended cremation with no service — the lowest-cost option.', sections: [essentialsForStep(rankinsDirectEssentials), optionalsForStep([])], extras: extrasForStep([]), }, ]; const rankinsForComparison: ComparisonPackage[] = rankinsForStep.map((pkg, idx) => { const ess = [rankinsStandardEssentials, rankinsPremiumEssentials, rankinsDirectEssentials][idx]; const allOptionals: Optional[][] = [ [ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ], [ { name: 'Digital Recording', treatment: 'included' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ], [], ]; const optionals = allOptionals[idx]; const extras: Extra[] = [ [ { name: 'Allowance for Celebrant', value: { allowance: 450 } }, { name: 'Catering', value: 'poa' as const }, { name: 'Saturday Service Fee', value: 750 }, ], [ { name: 'Allowance for Celebrant', value: { allowance: 600 } }, { name: 'Catering', value: 950 }, { name: 'Newspaper Notice', value: 280 }, ], [], ][idx]; return { id: pkg.id, name: pkg.name, price: pkg.price, provider: { name: 'Rankins Funeral Services', location: 'Wollongong, NSW', logoUrl: providersById['rankins'].logoUrl, rating: 4.8, reviewCount: 23, verified: true, }, sections: [ essentialsForComparison(ess), optionalsForComparison(optionals), extrasForComparison(extras), ], }; }); // ─── Killick (verified, QLD — generic levy label) ──────────────────────────── const killickClassicEssentials: EssentialsPrices = { coffin: 1600, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'complimentary', govLevy: 45.1, govLevyLabel: 'Government Levy — Cremation', mortuary: 440, service: 2614.9, transport: 'complimentary', }; const killickSimpleEssentials: EssentialsPrices = { coffin: 1100, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'complimentary', govLevy: 45.1, govLevyLabel: 'Government Levy — Cremation', mortuary: 440, service: 1534.9, transport: 'complimentary', }; const killickTraditionalEssentials: EssentialsPrices = { coffin: 2500, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'complimentary', govLevy: 45.1, govLevyLabel: 'Government Levy — Cremation', mortuary: 440, service: 4234.9, transport: 'complimentary', }; const killickForStep: PackageData[] = [ { id: 'classic', name: 'Classic Farewell Package', price: sumEssentials(killickClassicEssentials), description: 'A complete farewell service with chapel use and graveside committal.', sections: [ essentialsForStep(killickClassicEssentials), optionalsForStep([ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 500 } }, { name: 'Catering', value: 'poa' }, { name: 'Saturday Service Fee', value: 800 }, ]), }, { id: 'simple', name: 'Simple Cremation', price: sumEssentials(killickSimpleEssentials), description: 'A direct cremation without a service.', sections: [ essentialsForStep(killickSimpleEssentials), optionalsForStep([{ name: 'Online Notice', treatment: 'complimentary' }]), ], extras: extrasForStep([{ name: 'Allowance for Celebrant', value: { allowance: 350 } }]), }, { id: 'traditional-burial', name: 'Traditional Burial Package', price: sumEssentials(killickTraditionalEssentials), description: 'A traditional burial service with chapel use, full procession, and graveside committal.', sections: [ essentialsForStep(killickTraditionalEssentials), optionalsForStep([ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 550 } }, { name: 'Catering', value: 'poa' }, { name: 'Saturday Service Fee', value: 800 }, ]), }, ]; const killickForComparison: ComparisonPackage[] = killickForStep.map((pkg, idx) => { const ess = [killickClassicEssentials, killickSimpleEssentials, killickTraditionalEssentials][ idx ]; const allOptionals: Optional[][] = [ [ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, ], [{ name: 'Online Notice', treatment: 'complimentary' }], [ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, ], ]; const optionals = allOptionals[idx]; const extras: Extra[] = [ [ { name: 'Allowance for Celebrant', value: { allowance: 500 } }, { name: 'Catering', value: 'poa' as const }, { name: 'Saturday Service Fee', value: 800 }, ], [{ name: 'Allowance for Celebrant', value: { allowance: 350 } }], [ { name: 'Allowance for Celebrant', value: { allowance: 550 } }, { name: 'Catering', value: 'poa' as const }, { name: 'Saturday Service Fee', value: 800 }, ], ][idx]; return { id: pkg.id, name: pkg.name, price: pkg.price, provider: { name: 'Killick Family Funerals', location: 'Kingaroy, QLD', logoUrl: providersById['killick'].logoUrl, rating: 4.9, reviewCount: 15, verified: true, }, sections: [ essentialsForComparison(ess), optionalsForComparison(optionals), extrasForComparison(extras), ], }; }); // ─── Mackay (verified, NSW) ────────────────────────────────────────────────── const mackayEverydayEssentials: EssentialsPrices = { coffin: 1500, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 2430.35, transport: 'included', }; const mackayPremiumEssentials: EssentialsPrices = { coffin: 2200, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 4034.9, transport: 'included', }; const mackaySimpleEssentials: EssentialsPrices = { coffin: 800, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 434.9, transport: 'included', }; const mackayMemorialEssentials: EssentialsPrices = { coffin: 900, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 2834.9, transport: 'included', }; const mackayForStep: PackageData[] = [ { id: 'everyday', name: 'Everyday Funeral Package', price: sumEssentials(mackayEverydayEssentials), description: 'A complete funeral service with a chapel ceremony.', sections: [ essentialsForStep(mackayEverydayEssentials), optionalsForStep([ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 450 } }, { name: 'Catering', value: 'poa' }, { name: 'Saturday Service Fee', value: 750 }, ]), }, { id: 'premium', name: 'Premium Funeral Service', price: sumEssentials(mackayPremiumEssentials), description: 'An enhanced service with premium coffin selection and expanded inclusions.', sections: [ essentialsForStep(mackayPremiumEssentials), optionalsForStep([ { name: 'Digital Recording', treatment: 'included' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 600 } }, { name: 'Catering', value: 1100 }, { name: 'Newspaper Notice', value: 320 }, ]), }, { id: 'simple', name: 'Simple Cremation', price: sumEssentials(mackaySimpleEssentials), description: 'A direct cremation without a formal service.', sections: [ essentialsForStep(mackaySimpleEssentials), optionalsForStep([{ name: 'Online Notice', treatment: 'included' }]), ], extras: extrasForStep([{ name: 'Allowance for Celebrant', value: { allowance: 350 } }]), }, { id: 'memorial-service', name: 'Memorial Service Package', price: sumEssentials(mackayMemorialEssentials), description: 'A memorial service held separately from the cremation, allowing time for planning and family gatherings.', sections: [ essentialsForStep(mackayMemorialEssentials), optionalsForStep([ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 500 } }, { name: 'Catering', value: 'poa' }, ]), }, ]; const mackayForComparison: ComparisonPackage[] = mackayForStep.map((pkg, idx) => { const ess = [ mackayEverydayEssentials, mackayPremiumEssentials, mackaySimpleEssentials, mackayMemorialEssentials, ][idx]; const allOptionals: Optional[][] = [ [ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ], [ { name: 'Digital Recording', treatment: 'included' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ], [{ name: 'Online Notice', treatment: 'included' }], [ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, ], ]; const optionals = allOptionals[idx]; const extras: Extra[] = [ [ { name: 'Allowance for Celebrant', value: { allowance: 450 } }, { name: 'Catering', value: 'poa' as const }, { name: 'Saturday Service Fee', value: 750 }, ], [ { name: 'Allowance for Celebrant', value: { allowance: 600 } }, { name: 'Catering', value: 1100 }, { name: 'Newspaper Notice', value: 320 }, ], [{ name: 'Allowance for Celebrant', value: { allowance: 350 } }], [ { name: 'Allowance for Celebrant', value: { allowance: 500 } }, { name: 'Catering', value: 'poa' as const }, ], ][idx]; return { id: pkg.id, name: pkg.name, price: pkg.price, provider: { name: 'Mackay Family Funeral Directors', location: 'Ourimbah, NSW', logoUrl: providersById['mackay'].logoUrl, rating: 4.6, reviewCount: 87, verified: true, }, sections: [ essentialsForComparison(ess), optionalsForComparison(optionals), extrasForComparison(extras), ], }; }); // ─── Mannings (verified, NSW) ──────────────────────────────────────────────── const manningsStandardEssentials: EssentialsPrices = { coffin: 1300, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 2114.9, transport: 'included', }; const manningsPremiumEssentials: EssentialsPrices = { coffin: 2100, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 3734.9, transport: 'included', }; const manningsSimpleEssentials: EssentialsPrices = { coffin: 750, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 284.9, transport: 'included', }; const manningsDirectEssentials: EssentialsPrices = { coffin: 600, cremationCert: 350, crematorium: 660, deathReg: 70, dressing: 'included', govLevy: 45.1, mortuary: 440, service: 34.9, transport: 'included', }; const manningsForStep: PackageData[] = [ { id: 'standard', name: 'Standard Cremation Package', price: sumEssentials(manningsStandardEssentials), description: 'A respectful cremation with chapel service.', sections: [ essentialsForStep(manningsStandardEssentials), optionalsForStep([ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 400 } }, { name: 'Catering', value: 'poa' }, ]), }, { id: 'premium', name: 'Premium Funeral Service', price: sumEssentials(manningsPremiumEssentials), description: 'A more elaborate service with an upgraded coffin and broader inclusions.', sections: [ essentialsForStep(manningsPremiumEssentials), optionalsForStep([ { name: 'Digital Recording', treatment: 'included' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ]), ], extras: extrasForStep([ { name: 'Allowance for Celebrant', value: { allowance: 600 } }, { name: 'Catering', value: 950 }, { name: 'Newspaper Notice', value: 280 }, ]), }, { id: 'simple', name: 'Simple Cremation', price: sumEssentials(manningsSimpleEssentials), description: 'A straightforward cremation without a chapel service.', sections: [ essentialsForStep(manningsSimpleEssentials), optionalsForStep([{ name: 'Online Notice', treatment: 'included' }]), ], extras: extrasForStep([{ name: 'Allowance for Celebrant', value: { allowance: 350 } }]), }, { id: 'direct-cremation', name: 'Direct Cremation', price: sumEssentials(manningsDirectEssentials), description: 'An unattended cremation with no service — the lowest-cost option.', sections: [essentialsForStep(manningsDirectEssentials), optionalsForStep([])], extras: extrasForStep([]), }, ]; const manningsForComparison: ComparisonPackage[] = manningsForStep.map((pkg, idx) => { const ess = [ manningsStandardEssentials, manningsPremiumEssentials, manningsSimpleEssentials, manningsDirectEssentials, ][idx]; const allOptionals: Optional[][] = [ [ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, ], [ { name: 'Digital Recording', treatment: 'included' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ], [{ name: 'Online Notice', treatment: 'included' }], [], ]; const optionals = allOptionals[idx]; const extras: Extra[] = [ [ { name: 'Allowance for Celebrant', value: { allowance: 400 } }, { name: 'Catering', value: 'poa' as const }, ], [ { name: 'Allowance for Celebrant', value: { allowance: 600 } }, { name: 'Catering', value: 950 }, { name: 'Newspaper Notice', value: 280 }, ], [{ name: 'Allowance for Celebrant', value: { allowance: 350 } }], [], ][idx]; return { id: pkg.id, name: pkg.name, price: pkg.price, provider: { name: 'Mannings Funerals', location: 'Bega, NSW', logoUrl: providersById['mannings'].logoUrl, rating: 4.7, reviewCount: 31, verified: true, }, sections: [ essentialsForComparison(ess), optionalsForComparison(optionals), extrasForComparison(extras), ], }; }); // ─── Wollongong City (tier 3 — itemised but unverified, mostly unknowns) ───── // Tier-3 step view: simpler — show what we know, omit unknowns from breakdown. const wollongongForStep: PackageData[] = [ { id: 'standard', name: 'Standard Funeral Service', price: 3400, description: 'Itemised package based on publicly available information. Make an enquiry to confirm details.', sections: [ { heading: 'Essentials (estimated)', items: [ { name: 'Allowance for Coffin', price: 1400, isAllowance: true }, { name: 'Cremation Certificate/Permit', price: 350 }, { name: 'Crematorium', price: 660 }, { name: 'Professional Service Fee', price: 990 }, ], }, ], total: 3400, }, ]; const wollongongForComparison: ComparisonPackage[] = [ { id: 'standard', name: 'Standard Funeral Service', price: 3400, provider: { name: 'Wollongong City Funerals', location: 'Wollongong, NSW', rating: 4.2, reviewCount: 15, verified: false, }, sections: [ { heading: 'Essentials', items: [ { name: 'Allowance for Coffin', value: { type: 'allowance', amount: 1400 } }, { name: 'Cremation Certificate/Permit', value: { type: 'price', amount: 350 } }, { name: 'Crematorium', value: { type: 'price', amount: 660 } }, { name: 'Death Registration Certificate', value: { type: 'unknown' } }, { name: 'Dressing Fee', value: { type: 'unknown' } }, { name: 'NSW Government Levy — Cremation', value: { type: 'unknown' } }, { name: 'Professional Mortuary Care', value: { type: 'unknown' } }, { name: 'Professional Service Fee', value: { type: 'price', amount: 990 } }, { name: 'Transportation Service Fee', value: { type: 'unknown' } }, ], }, ], }, ]; // ─── Botanical (tier 2 — price only, no breakdown) ────────────────────────── const botanicalForStep: PackageData[] = [ { id: 'standard', name: 'Standard Funeral Service', price: 5200, description: 'A full-service package based on publicly available information. Breakdown not available.', sections: [], }, { id: 'basic', name: 'Basic Cremation', price: 3400, description: 'Entry-level package. Pricing is indicative only.', sections: [], }, ]; const botanicalForComparison: ComparisonPackage[] = [ { id: 'standard', name: 'Standard Funeral Service', price: 5200, provider: { name: 'Botanical Funerals', location: 'Newtown, NSW', rating: 4.9, reviewCount: 8, verified: false, }, itemizedAvailable: false, sections: [], }, { id: 'basic', name: 'Basic Cremation', price: 3400, provider: { name: 'Botanical Funerals', location: 'Newtown, NSW', rating: 4.9, reviewCount: 8, verified: false, }, itemizedAvailable: false, sections: [], }, ]; // ─── Bundle map ───────────────────────────────────────────────────────────── // Per-provider matching/other split. The "matching your preferences" list is // the recommended set; "other" is everything else from the same provider. // Tier-3/2 providers don't show an "other" list — they show nearby-verified // alternatives instead — so their `other` arrays stay empty. export const packagesByProvider: Record = { parsons: { matching: parsonsForStep.slice(0, 1), other: parsonsForStep.slice(1), forComparison: parsonsForComparison, }, rankins: { matching: rankinsForStep.slice(0, 1), other: rankinsForStep.slice(1), forComparison: rankinsForComparison, }, killick: { matching: killickForStep.slice(0, 1), other: killickForStep.slice(1), forComparison: killickForComparison, }, mackay: { matching: mackayForStep.slice(0, 1), other: mackayForStep.slice(1), forComparison: mackayForComparison, }, mannings: { matching: manningsForStep.slice(0, 1), other: manningsForStep.slice(1), forComparison: manningsForComparison, }, 'wollongong-city': { matching: wollongongForStep, other: [], forComparison: wollongongForComparison, }, botanical: { matching: botanicalForStep, other: [], forComparison: botanicalForComparison, }, }; /** Compound basket key: `${providerId}:${packageId}` */ export type BasketKey = string; export const makeBasketKey = (providerId: string, packageId: string): BasketKey => `${providerId}:${packageId}`; export const parseBasketKey = ( key: BasketKey, ): { providerId: string; packageId: string } | null => { const [providerId, packageId] = key.split(':'); if (!providerId || !packageId) return null; return { providerId, packageId }; }; /** Resolve a basket key to its ComparisonPackage, or null if missing. */ export function resolveComparisonPackage(key: BasketKey): ComparisonPackage | null { const parsed = parseBasketKey(key); if (!parsed) return null; const bundle = packagesByProvider[parsed.providerId]; if (!bundle) return null; return bundle.forComparison.find((p) => p.id === parsed.packageId) ?? null; } /** * Demo recommendation: the package ComparisonPage always surfaces as the * system-recommended option. Not part of the user's basket and doesn't * count against the 3-package basket cap — it's an editorial suggestion * layered on top of whatever the user has selected. If the same package * is already in the basket, the Comparison route dedupes so it appears * once (as the recommended column). */ export const DEMO_RECOMMENDED_KEY: BasketKey = 'parsons:deluxe'; /** * Verified providers surfaced under the "Similar packages from verified * providers" grid on unverified tier-2 / tier-3 pages. Derived from the * main `providers` fixture filtered to tier === 'verified', with * `startingPrice` taken from their first matching package. Only providers * that actually have an image in the fixture are eligible (MiniCard * requires `imageUrl`). */ export const nearbyVerifiedProviders: NearbyVerifiedProvider[] = providers .filter((p) => p.tier === 'verified' && p.imageUrl) .map((p) => ({ id: p.id, name: p.name, imageUrl: p.imageUrl!, location: p.location, startingPrice: packagesByProvider[p.id]?.matching[0]?.price ?? p.startingPrice ?? 0, rating: p.rating, reviewCount: p.reviewCount, }));