The ComparisonPage's `recommendedPackage` prop was never wired in the demo — users only saw their basket contents. Now always surface a default recommended package (parsons:deluxe) as an extra column, deduped against the basket so it never appears twice. Basket mechanics are unchanged: the 3-package cap counts user selections only, and the recommended is layered on top as an editorial suggestion. The empty state only renders when there is genuinely nothing to show — since the recommended is static, it's effectively defensive for a future state where resolution could fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1277 lines
40 KiB
TypeScript
1277 lines
40 KiB
TypeScript
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<PackageLineItem>((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<PackageLineItem>((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<string, PackageBundle> = {
|
|
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,
|
|
}));
|