Major changes: - Add harmonize-az-dispensaries.ts script to sync dispensaries with Dutchie API - Add migration 057 for crawl_enabled and dutchie_verified fields - Remove legacy dutchie-az module (replaced by platforms/dutchie) - Clean up deprecated crawlers, scrapers, and orchestrator code - Update location-discovery to not fallback to slug when ID is missing - Add crawl-rotator service for proxy rotation - Add types/index.ts for shared type definitions - Add woodpecker-agent k8s manifest Harmonization script: - Queries ConsumerDispensaries API for all 32 AZ cities - Matches dispensaries by platform_dispensary_id (not slug) - Updates existing records with full Dutchie data - Creates new records for unmatched Dutchie dispensaries - Disables dispensaries not found in Dutchie 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
188 lines
5.9 KiB
TypeScript
188 lines
5.9 KiB
TypeScript
/**
|
|
* Dutchie GraphQL Queries
|
|
*
|
|
* High-level GraphQL operations built on top of the client.
|
|
*/
|
|
|
|
import { executeGraphQL, GRAPHQL_HASHES, DUTCHIE_CONFIG } from './client';
|
|
|
|
// ============================================================
|
|
// TYPES
|
|
// ============================================================
|
|
|
|
export interface ResolveDispensaryResult {
|
|
dispensaryId: string | null;
|
|
httpStatus?: number;
|
|
error?: string;
|
|
source?: 'graphql' | 'html';
|
|
}
|
|
|
|
// ============================================================
|
|
// DISPENSARY ID RESOLUTION
|
|
// ============================================================
|
|
|
|
/**
|
|
* Resolve a dispensary slug to its internal platform ID via GraphQL
|
|
*/
|
|
export async function resolveDispensaryId(slug: string): Promise<string | null> {
|
|
const result = await resolveDispensaryIdWithDetails(slug);
|
|
return result.dispensaryId;
|
|
}
|
|
|
|
/**
|
|
* Resolve with full details for error handling
|
|
*/
|
|
export async function resolveDispensaryIdWithDetails(slug: string): Promise<ResolveDispensaryResult> {
|
|
console.log(`[Dutchie Queries] Resolving dispensary ID for slug: ${slug}`);
|
|
|
|
try {
|
|
const variables = {
|
|
dispensaryFilter: {
|
|
cNameOrID: slug,
|
|
},
|
|
};
|
|
|
|
const result = await executeGraphQL(
|
|
'GetAddressBasedDispensaryData',
|
|
variables,
|
|
GRAPHQL_HASHES.GetAddressBasedDispensaryData,
|
|
{ cName: slug, maxRetries: 3, retryOn403: true }
|
|
);
|
|
|
|
const dispensaryId = result?.data?.dispensaryBySlug?.id ||
|
|
result?.data?.dispensary?.id ||
|
|
result?.data?.getAddressBasedDispensaryData?.dispensary?.id;
|
|
|
|
if (dispensaryId) {
|
|
console.log(`[Dutchie Queries] Resolved ${slug} -> ${dispensaryId}`);
|
|
return { dispensaryId, source: 'graphql' };
|
|
}
|
|
|
|
console.log(`[Dutchie Queries] No dispensaryId in response for ${slug}`);
|
|
return {
|
|
dispensaryId: null,
|
|
error: 'Could not extract dispensaryId from GraphQL response',
|
|
};
|
|
|
|
} catch (error: any) {
|
|
const status = error.message?.match(/HTTP (\d+)/)?.[1];
|
|
if (status === '403' || status === '404') {
|
|
return {
|
|
dispensaryId: null,
|
|
httpStatus: parseInt(status),
|
|
error: `HTTP ${status}: Store may be removed or blocked`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
dispensaryId: null,
|
|
error: error.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// DISPENSARY INFO
|
|
// ============================================================
|
|
|
|
export interface DispensaryInfo {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
isOpen: boolean;
|
|
timezone: string;
|
|
address: string;
|
|
city: string;
|
|
state: string;
|
|
zip: string;
|
|
phone: string;
|
|
email: string;
|
|
hours: {
|
|
monday?: { open: string; close: string } | null;
|
|
tuesday?: { open: string; close: string } | null;
|
|
wednesday?: { open: string; close: string } | null;
|
|
thursday?: { open: string; close: string } | null;
|
|
friday?: { open: string; close: string } | null;
|
|
saturday?: { open: string; close: string } | null;
|
|
sunday?: { open: string; close: string } | null;
|
|
};
|
|
acceptsCredit: boolean;
|
|
offersCurbside: boolean;
|
|
offersDelivery: boolean;
|
|
offersPickup: boolean;
|
|
featureFlags: string[];
|
|
}
|
|
|
|
/**
|
|
* Get dispensary info including business hours
|
|
*/
|
|
export async function getDispensaryInfo(cNameOrSlug: string): Promise<DispensaryInfo | null> {
|
|
console.log(`[Dutchie Queries] Getting dispensary info for: ${cNameOrSlug}`);
|
|
|
|
try {
|
|
const variables = {
|
|
dispensaryFilter: {
|
|
cNameOrID: cNameOrSlug,
|
|
},
|
|
};
|
|
|
|
const result = await executeGraphQL(
|
|
'GetAddressBasedDispensaryData',
|
|
variables,
|
|
GRAPHQL_HASHES.GetAddressBasedDispensaryData,
|
|
{ cName: cNameOrSlug, maxRetries: 2, retryOn403: true }
|
|
);
|
|
|
|
const dispensary = result?.data?.dispensary ||
|
|
result?.data?.dispensaryBySlug ||
|
|
result?.data?.getAddressBasedDispensaryData?.dispensary;
|
|
|
|
if (!dispensary) {
|
|
console.log(`[Dutchie Queries] No dispensary data found for ${cNameOrSlug}`);
|
|
return null;
|
|
}
|
|
|
|
const hoursSettings = dispensary.hoursSettings || dispensary.operatingHours || {};
|
|
|
|
const parseHours = (dayHours: any) => {
|
|
if (!dayHours || dayHours.isClosed) return null;
|
|
return {
|
|
open: dayHours.openTime || dayHours.open || '',
|
|
close: dayHours.closeTime || dayHours.close || '',
|
|
};
|
|
};
|
|
|
|
return {
|
|
id: dispensary.id || dispensary._id || '',
|
|
name: dispensary.name || '',
|
|
slug: dispensary.cName || dispensary.slug || cNameOrSlug,
|
|
isOpen: dispensary.isOpen ?? dispensary.openNow ?? false,
|
|
timezone: dispensary.timezone || '',
|
|
address: dispensary.address || dispensary.location?.address || '',
|
|
city: dispensary.city || dispensary.location?.city || '',
|
|
state: dispensary.state || dispensary.location?.state || '',
|
|
zip: dispensary.zip || dispensary.zipcode || dispensary.location?.zip || '',
|
|
phone: dispensary.phone || dispensary.phoneNumber || '',
|
|
email: dispensary.email || '',
|
|
hours: {
|
|
monday: parseHours(hoursSettings.monday),
|
|
tuesday: parseHours(hoursSettings.tuesday),
|
|
wednesday: parseHours(hoursSettings.wednesday),
|
|
thursday: parseHours(hoursSettings.thursday),
|
|
friday: parseHours(hoursSettings.friday),
|
|
saturday: parseHours(hoursSettings.saturday),
|
|
sunday: parseHours(hoursSettings.sunday),
|
|
},
|
|
acceptsCredit: dispensary.acceptsCreditCards ?? dispensary.creditCardAccepted ?? false,
|
|
offersCurbside: dispensary.offersCurbside ?? dispensary.curbsidePickup ?? false,
|
|
offersDelivery: dispensary.offersDelivery ?? dispensary.delivery ?? false,
|
|
offersPickup: dispensary.offersPickup ?? dispensary.pickup ?? true,
|
|
featureFlags: dispensary.featureFlags || [],
|
|
};
|
|
|
|
} catch (error: any) {
|
|
console.error(`[Dutchie Queries] Error getting dispensary info: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|