openvibers/openvibe.reviews Commits

NYC restaurants: the street address is part of the place, and a same-named entity takes a record only when it holds no record of that register yet, so a chain's restaurants in one borough stay apart (24 Manhattan/Brooklyn chains were merged on the first import) (#81)

OP OpenVibers committed 27 days ago · f0468692b0 · 2 files, +25 −6

Changes

ddiff --git a/server/reviews/service.js b/server/reviews/service.js
iindex 5e255a0..054ea45 100644
---- a/server/reviews/service.js
++++ b/server/reviews/service.js
@@@ -141,6 +141,7 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
recordsOf: db.prepare(`SELECT kind, occurred_on, fields, retrieved_at, source_key FROM review_records WHERE entity_id = ANY(?::text[]) AND status = 'active'
ORDER BY kind, occurred_on DESC NULLS LAST, retrieved_at DESC LIMIT 400`),
externalAlias: db.prepare("SELECT entity_id FROM review_entity_aliases WHERE type = 'external' AND norm = ? AND removed_at IS NULL LIMIT 1"),
++ externalOfScheme: db.prepare("SELECT 1 FROM review_entity_aliases WHERE entity_id = ? AND type = 'external' AND norm LIKE ? AND removed_at IS NULL LIMIT 1"),
// Ratings other sites show, as members saw them (migrations/0006).
seenRatingsOf: db.prepare(`SELECT site, page_url, rating_value::float8 AS rating_value, rating_best::float8 AS rating_best, rating_count, seen_at, via, id, seen_by
FROM review_seen_ratings WHERE entity_id = ? AND status = 'shown' ORDER BY seen_at DESC LIMIT 200`),
@@@ -959,16 +960,25 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
const camis = String(f.camis || '').trim();
if (!/^\d{1,12}$/.test(camis) || !f.dba) return null;
const boro = BOROUGHS[String(f.boro || '').trim().toLowerCase()];
-- return { alias: `nyc-camis:${camis}`, name: recordName(f.dba), kind: 'restaurant', place: boro ? `${boro}, NY` : 'New York, NY', create: true };
++ // The street address is part of the place: a chain has many restaurants of one name in one borough.
++ const street = [String(f.building || '').trim(), recordName(String(f.street || '').trim())].filter(Boolean).join(' ');
++ return { alias: `nyc-camis:${camis}`, name: recordName(f.dba), kind: 'restaurant', place: `${street ? `${street}, ` : ''}${boro || 'New York'}, NY`.slice(0, 120), create: true };
}
async function recordEntity(target, actor) {
const norm1 = norm.alias('external', target.alias);
const hit = await q.externalAlias.get(norm1);
if (hit) return await canonicalOf(hit.entity_id);
const kinds = target.kind === 'organization' ? ['organization', 'employer', 'business', 'service'] : [target.kind];
++ // A same-named entity takes the record only when no record of the same register is on it already: two NYC
++ // restaurants (or two CFPB companies) are never one entity.
++ const scheme = `${target.alias.slice(0, target.alias.indexOf(':') + 1)}%`;
for (const k of kinds) {
-- const same = (await q.sameNamedOfKind.all(norm.alias('name', target.name), k))
-- .filter((x) => !LOCAL_KINDS.has(k) || String(x.description || '').toLowerCase() === String(target.place || '').toLowerCase());
++ const same = [];
++ for (const x of await q.sameNamedOfKind.all(norm.alias('name', target.name), k)) {
++ if (LOCAL_KINDS.has(k) && String(x.description || '').toLowerCase() !== String(target.place || '').toLowerCase()) continue;
++ if (await q.externalOfScheme.get(x.id, scheme)) continue;
++ same.push(x);
++ }
if (same.length) { const id = await canonicalOf(same[0].id); await insertAlias(id, 'external', target.alias, actor); return id; }
}
if (!target.create) return null;
ddiff --git a/test/official-records.test.js b/test/official-records.test.js
iindex f10a922..a269e27 100644
---- a/test/official-records.test.js
++++ b/test/official-records.test.js
@@@ -59,8 +59,8 @@ t('CFPB counts and NYC inspections land on entities, shown as official records,
const clean = nyc({ inspection_date: '2026-10-15T00:00:00.000', inspection_type: 'Cycle Inspection / Re-inspection', action: 'No violations were recorded at the time of this inspection.', score: '7', grade: 'A', critical_flag: 'Not Applicable' });
await importItem(h, clean);
const rest = await h.db.prepare("SELECT slug, name, kind, description FROM review_entities WHERE kind = 'restaurant'").all();
-- assert.deepStrictEqual(rest.map((e) => [e.slug, e.name, e.description]), [['the-coffee-club-brooklyn-ny', 'The Coffee Club', 'Brooklyn, NY']]);
-- const rp = await req(h, 'GET', '/e/the-coffee-club-brooklyn-ny');
++ assert.deepStrictEqual(rest.map((e) => [e.slug, e.name, e.description]), [['the-coffee-club-9526-4-avenue-brooklyn-ny', 'The Coffee Club', '9526 4 Avenue, Brooklyn, NY']]);
++ const rp = await req(h, 'GET', '/e/the-coffee-club-9526-4-avenue-brooklyn-ny');
assert.match(rp.text, /NYC Health inspections of The Coffee Club at 9526 4 AVENUE, Brooklyn 11209 \(Coffee\/Tea\), newest first/);
const firstLi = rp.text.indexOf('rv-grade-a'), olderLi = rp.text.indexOf('Food contact surface not properly washed.');
assert.ok(firstLi > 0 && olderLi > firstLi, 'the newest inspection (grade A) comes first');
@@@ -68,6 +68,15 @@ t('CFPB counts and NYC inspections land on entities, shown as official records,
assert.match(rp.text, /score 20/);
assert.match(rp.text, /No violations were recorded at the time of this inspection\./);
++ // Two restaurants of one chain in one borough stay two (their own address and inspections each).
++ await importItem(h, nyc({ camis: '40000001', dba: 'STARBUCKS', boro: 'Manhattan', building: '38', street: 'PARK ROW', zipcode: '10038', inspection_date: '2026-10-14T00:00:00.000', violation_code: '04N', violation: 'Flies.' }));
++ await importItem(h, nyc({ camis: '40000002', dba: 'STARBUCKS', boro: 'Manhattan', building: '1', street: 'BROADWAY', zipcode: '10004', inspection_date: '2026-10-13T00:00:00.000', violation_code: '10F', violation: 'Surface.' }));
++ await importItem(h, nyc({ camis: '40000001', dba: 'STARBUCKS', boro: 'Manhattan', building: '38', street: 'PARK ROW', zipcode: '10038', inspection_date: '2026-10-14T00:00:00.000', violation_code: '06C', violation: 'Food not protected.' }));
++ const sb = await h.db.prepare("SELECT slug, description FROM review_entities WHERE name = 'Starbucks' ORDER BY slug").all();
++ assert.deepStrictEqual(sb.map((e) => e.description), ['1 Broadway, Manhattan, NY', '38 Park Row, Manhattan, NY']);
++ const parkRow = (await req(h, 'GET', '/e/starbucks-38-park-row-manhattan-ny')).text;
++ assert.ok(parkRow.includes('Flies.') && parkRow.includes('Food not protected.') && !parkRow.includes('Surface.'), 'each keeps its own inspections');
++
// Search carries the facts.
const docs = (await outbox(h)).filter((x) => x.event_type === 'reviews.index_document.upserted');
assert.ok(docs.some((d) => /CFPB consumer complaints: 34100/.test(d.payload.body || '')));
@@@ -76,7 +85,7 @@ t('CFPB counts and NYC inspections land on entities, shown as official records,
// The source removes the clean inspection's row: the page goes back to the older inspection.
h.sources.put({ ...clean, revision: 2, removed: { at: new Date().toISOString(), reason: 'withdrawn by the city' } });
assert.strictEqual((await req(h, 'POST', '/api/v1/signals/import', { token: require('./helpers').IMPORT(), body: { source_item_id: clean.id } })).status, 200);
-- assert.ok(!/rv-grade-a/.test((await req(h, 'GET', '/e/the-coffee-club-brooklyn-ny')).text));
++ assert.ok(!/rv-grade-a/.test((await req(h, 'GET', '/e/the-coffee-club-9526-4-avenue-brooklyn-ny')).text));
// /k/organization and /k/restaurant list them; the home offers both kinds and the new ones.
// Lists order by members' reviews, then by the records, and say what the record is.