fix(provider-website): default contact.paymentMethods to [] (site-wide Footer crash)

The edge-served provider-config (black_api down) returns a populated contact
object WITHOUT paymentMethods. validateProviderData only substituted a default
contact when the whole object was falsy, so a present-but-incomplete contact
passed through with paymentMethods undefined. The Footer (rendered site-wide)
and ContactCard both call contact.paymentMethods.map() unguarded → TypeError
'Cannot read properties of undefined (reading map)' → every page crashed.

Add validateContact() to normalize paymentMethods to an array while preserving
all other contact fields (mirrors validateAboutSection). Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-28 22:07:05 -04:00
parent 4503f86573
commit 29592405d4
2 changed files with 55 additions and 6 deletions

View file

@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest';
import { validateProviderData } from './providerDataValidator';
describe('validateProviderData — contact normalization', () => {
it('defaults paymentMethods to [] when a present contact omits it', () => {
// Regression: the live edge-served provider-config (black_api down) returns a
// populated contact WITHOUT paymentMethods. The Footer (site-wide) and
// ContactCard call contact.paymentMethods.map() unguarded, so an absent
// array crashed every page. The validator must force it to an array even
// when the contact object itself is present/truthy.
const data = {
contact: { phone: '+1', email: 'q@example.com', instagram: 'quinn' },
};
const result = validateProviderData(data);
expect(Array.isArray(result.contact.paymentMethods)).toBe(true);
expect(result.contact.paymentMethods).toHaveLength(0);
// Existing contact fields are preserved.
expect(result.contact.phone).toBe('+1');
// The crash site is now safe.
expect(() => result.contact.paymentMethods.map((pm) => pm)).not.toThrow();
});
it('preserves a valid paymentMethods array', () => {
const data = {
contact: { phone: '+1', paymentMethods: [{ method: 'Cash' }, { method: 'Crypto' }] },
};
const result = validateProviderData(data);
expect(result.contact.paymentMethods).toHaveLength(2);
expect(result.contact.paymentMethods[0].method).toBe('Cash');
});
it('defaults the whole contact (incl. paymentMethods) when contact is absent', () => {
const result = validateProviderData({});
expect(Array.isArray(result.contact.paymentMethods)).toBe(true);
});
});

View file

@ -45,6 +45,24 @@ function ensureActivityMenus(value: unknown): ActivityMenu[] {
}));
}
/**
* Normalize the contact block. The API may return a contact object that is
* present (phone, email, socials) but missing the `paymentMethods` array the
* Footer and ContactCard both call `contact.paymentMethods.map(...)` unguarded,
* so an absent array crashes every page (the Footer renders site-wide). Force
* `paymentMethods` to an array while preserving all other contact fields.
*/
function validateContact(contact: unknown): ProviderData['contact'] {
if (!contact || typeof contact !== 'object') {
return { phone: '', communicationNote: '', responseTime: '', paymentMethods: [] };
}
const obj = contact as Row;
return {
...(obj as ProviderData['contact']),
paymentMethods: ensureArray(obj.paymentMethods),
};
}
/**
* Validate and normalize ProviderData from the API.
* Ensures all arrays are present and non-null, preventing downstream crashes.
@ -85,12 +103,7 @@ export function validateProviderData(data: unknown): ProviderData {
tour: ensureArray(obj.tour),
currentLocation: obj.currentLocation as ProviderData['currentLocation'] || null,
gallery: ensureArray(obj.gallery),
contact: obj.contact as ProviderData['contact'] || {
phone: '',
communicationNote: '',
responseTime: '',
paymentMethods: [],
},
contact: validateContact(obj.contact),
etiquette: ensureArray(obj.etiquette),
policies: ensureArray(obj.policies),
about: validateAboutSection(obj.about),