diff --git a/src/demo/shared/fixtures/packages.ts b/src/demo/shared/fixtures/packages.ts index 1952a7e..ddb2fd6 100644 --- a/src/demo/shared/fixtures/packages.ts +++ b/src/demo/shared/fixtures/packages.ts @@ -134,26 +134,32 @@ const sumEssentials = (p: EssentialsPrices): number => { }; // ─── 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 | 'unknown'; + treatment: IncludedTreatment; } const optionalsForStep = (items: Optional[]): PackageSection => ({ heading: 'Optionals', - items: items.map((it) => - it.treatment === 'unknown' - ? { name: it.name, price: 0, priceLabel: '—' } - : { name: it.name, price: 0, priceLabel: labelFor(it.treatment) }, - ), + 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 === 'unknown' ? 'unknown' : it.treatment }, + value: { type: it.treatment }, })), }); @@ -226,6 +232,28 @@ const parsonsDeluxeEssentials: EssentialsPrices = { 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[] = [ { @@ -290,10 +318,58 @@ const parsonsForStep: PackageData[] = [ { 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][idx]; + const ess = [ + parsonsEverydayEssentials, + parsonsEssentialEssentials, + parsonsDeluxeEssentials, + parsonsTraditionalEssentials, + parsonsMemorialEssentials, + ][idx]; const allOptionals: Optional[][] = [ [ { name: 'Digital Recording', treatment: 'complimentary' }, @@ -312,6 +388,17 @@ const parsonsForComparison: ComparisonPackage[] = parsonsForStep.map((pkg, idx) { 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[] = [ @@ -331,6 +418,17 @@ const parsonsForComparison: ComparisonPackage[] = parsonsForStep.map((pkg, idx) { 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, @@ -376,6 +474,17 @@ const rankinsPremiumEssentials: EssentialsPrices = { 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[] = [ { @@ -386,7 +495,6 @@ const rankinsForStep: PackageData[] = [ sections: [ essentialsForStep(rankinsStandardEssentials), optionalsForStep([ - { name: 'Digital Recording', treatment: 'unknown' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, @@ -418,13 +526,20 @@ const rankinsForStep: PackageData[] = [ { 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][idx]; + const ess = [rankinsStandardEssentials, rankinsPremiumEssentials, rankinsDirectEssentials][idx]; const allOptionals: Optional[][] = [ [ - { name: 'Digital Recording', treatment: 'unknown' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, @@ -435,6 +550,7 @@ const rankinsForComparison: ComparisonPackage[] = rankinsForStep.map((pkg, idx) { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, ], + [], ]; const optionals = allOptionals[idx]; const extras: Extra[] = [ @@ -448,6 +564,7 @@ const rankinsForComparison: ComparisonPackage[] = rankinsForStep.map((pkg, idx) { name: 'Catering', value: 950 }, { name: 'Newspaper Notice', value: 280 }, ], + [], ][idx]; return { id: pkg.id, @@ -495,6 +612,18 @@ const killickSimpleEssentials: EssentialsPrices = { 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[] = [ { @@ -508,7 +637,6 @@ const killickForStep: PackageData[] = [ { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, { name: 'Viewing Fee', treatment: 'complimentary' }, - { name: 'Flowers', treatment: 'unknown' }, ]), ], extras: extrasForStep([ @@ -524,27 +652,47 @@ const killickForStep: PackageData[] = [ description: 'A direct cremation without a service.', sections: [ essentialsForStep(killickSimpleEssentials), - optionalsForStep([ - { name: 'Online Notice', treatment: 'complimentary' }, - { name: 'Viewing Fee', treatment: 'unknown' }, - ]), + 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][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: 'Flowers', treatment: 'unknown' }, ], + [{ name: 'Online Notice', treatment: 'complimentary' }], [ + { name: 'Digital Recording', treatment: 'complimentary' }, { name: 'Online Notice', treatment: 'complimentary' }, - { name: 'Viewing Fee', treatment: 'unknown' }, + { name: 'Viewing Fee', treatment: 'complimentary' }, ], ]; const optionals = allOptionals[idx]; @@ -555,6 +703,11 @@ const killickForComparison: ComparisonPackage[] = killickForStep.map((pkg, idx) { 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, @@ -589,6 +742,39 @@ const mackayEverydayEssentials: EssentialsPrices = { 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[] = [ { @@ -599,7 +785,6 @@ const mackayForStep: PackageData[] = [ sections: [ essentialsForStep(mackayEverydayEssentials), optionalsForStep([ - { name: 'Digital Recording', treatment: 'unknown' }, { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, { name: 'Flowers', treatment: 'included' }, @@ -611,35 +796,119 @@ const mackayForStep: PackageData[] = [ { 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) => ({ - 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(mackayEverydayEssentials), - optionalsForComparison([ - { name: 'Digital Recording', treatment: 'unknown' }, +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' }, - ]), - extrasForComparison([ + ], + [ + { 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' }, + { 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) ──────────────────────────────────────────────── @@ -654,6 +923,39 @@ const manningsStandardEssentials: EssentialsPrices = { 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[] = [ { @@ -666,7 +968,6 @@ const manningsForStep: PackageData[] = [ optionalsForStep([ { name: 'Online Notice', treatment: 'included' }, { name: 'Viewing Fee', treatment: 'included' }, - { name: 'Flowers', treatment: 'unknown' }, ]), ], extras: extrasForStep([ @@ -674,33 +975,101 @@ const manningsForStep: PackageData[] = [ { 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) => ({ - 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(manningsStandardEssentials), - optionalsForComparison([ +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: 'Flowers', treatment: 'unknown' }, - ]), - extrasForComparison([ + ], + [ + { 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' }, - ]), - ], -})); + { 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) ───── @@ -832,13 +1201,13 @@ export const packagesByProvider: Record = { forComparison: killickForComparison, }, mackay: { - matching: mackayForStep, - other: [], + matching: mackayForStep.slice(0, 1), + other: mackayForStep.slice(1), forComparison: mackayForComparison, }, mannings: { - matching: manningsForStep, - other: [], + matching: manningsForStep.slice(0, 1), + other: manningsForStep.slice(1), forComparison: manningsForComparison, }, 'wollongong-city': {