import db from "../db.server"; import { planCodeFromSubscription, type PlanCode } from "./plans"; import { fetchActiveSubscription, partnerApiConfigured } from "./partner.server"; type GraphQL = (query: string, options?: { variables?: Record }) => Promise; export interface AdminLike { graphql: GraphQL } export async function gql(admin: AdminLike, query: string, variables?: Record): Promise { const res = await admin.graphql(query, variables ? { variables } : undefined); const json = (await res.json()) as { data?: T; errors?: unknown }; if (json.errors) throw new Error(`GraphQL error: ${JSON.stringify(json.errors)}`); return json.data as T; } const SHOP_QUERY = `#graphql query BundleOffersShop { shop { id currencyCode ianaTimezone currencyFormats { moneyFormat } } currentAppInstallation { id activeSubscriptions { name status test } } }`; /** Plan code from Shopify App Pricing (Partner API). Without Partner API credentials (local dev) it falls back * to the Admin API's activeSubscriptions; if the Partner API fails it keeps the stored plan. */ interface PlanState { planCode: PlanCode; pendingPlanCode: PlanCode | null; planChangesAt: string | null } async function resolvePlan(data: any, domain: string, fresh: boolean): Promise { if (partnerApiConfigured()) { try { const sub = await fetchActiveSubscription(data.shop.id, { fresh }); return { planCode: planCodeFromSubscription(sub?.handles.join(" ")), pendingPlanCode: sub?.pendingHandles.length ? planCodeFromSubscription(sub.pendingHandles.join(" ")) : null, planChangesAt: sub?.changesAt || null, }; } catch (e) { console.error("plan lookup failed, keeping the stored plan", e); const row = await getShop(domain); return { planCode: (row?.planCode as PlanCode) || "free", pendingPlanCode: null, planChangesAt: null }; } } const subs = (data.currentAppInstallation?.activeSubscriptions || []) as { name: string; status: string }[]; return { planCode: planCodeFromSubscription(subs.find((s) => s.status === "ACTIVE")?.name), pendingPlanCode: null, planChangesAt: null }; } /** Creates/refreshes the Shop row from the Admin API (currency, timezone) and the plan from App Pricing. */ export async function syncShop(admin: AdminLike, domain: string, { fresh = false } = {}) { const data = await gql(admin, SHOP_QUERY); const { planCode, pendingPlanCode, planChangesAt } = await resolvePlan(data, domain, fresh); const values = { currency: data.shop.currencyCode, timezone: data.shop.ianaTimezone || "UTC", moneyFormat: data.shop.currencyFormats?.moneyFormat || "${{amount}}", appInstallationId: data.currentAppInstallation?.id, planCode, uninstalledAt: null, }; const row = await db.shop.upsert({ where: { domain }, create: { domain, ...values }, update: values }); return { ...row, pendingPlanCode, planChangesAt }; } export async function getShop(domain: string) { return db.shop.findUnique({ where: { domain } }); } /** Shop row for an admin request; syncs from Shopify the first time (or when forced). */ export async function ensureShop(admin: AdminLike, domain: string, force = false) { const shop = await getShop(domain); if (shop && !force && shop.appInstallationId && !shop.uninstalledAt) return shop; return syncShop(admin, domain); }