Files
cannaiq/backend/src/platforms/dutchie/queries.ts
Kelly b7cfec0770 feat: AZ dispensary harmonization with Dutchie source of truth
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>
2025-12-08 10:19:49 -07:00

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;
}
}