UNPKG

create-mf2-app

Version:

The stack AI moves fast with.

257 lines (232 loc) 7.45 kB
import { Polar } from "@convex-dev/polar"; import { api, components, internal } from "../_generated/api"; import { action, internalAction } from "../_generated/server"; import { Polar as PolarSdk } from "@polar-sh/sdk"; import { type Product } from "@polar-sh/sdk/models/components/product"; export const polar = new Polar(components.polar, { getUserInfo: async (ctx): Promise<{ userId: string; email: string }> => { const user = await ctx.runQuery(api.auth.users.currentUser); if (!user) { throw new Error("User not found"); } return { userId: user._id, email: user.clerkUser.email_addresses?.[0]?.email_address || "", }; }, products: { starter: process.env.NEXT_PUBLIC_POLAR_STARTER_PRODUCT_ID || "", pro: process.env.NEXT_PUBLIC_POLAR_PRO_PRODUCT_ID || "", }, }); export const { changeCurrentSubscription, cancelCurrentSubscription, getConfiguredProducts, listAllProducts, generateCheckoutLink, generateCustomerPortalUrl, } = polar.api(); export const _syncPolarProducts = internalAction({ handler: async ( ctx ): Promise<{ created: number; archived: number; synced: number; skipped: number; polarArchivedCount?: number; }> => { await ctx.runAction(internal.payments.cleanup.cleanupDuplicateProducts); const stats = { created: 0, archived: 0, synced: 0, skipped: 0, polarArchivedCount: 0, }; const polarSdk = new PolarSdk({ accessToken: process.env.POLAR_ORGANIZATION_TOKEN!, server: (process.env.POLAR_SERVER as "sandbox" | "production") || "production", }); const archivedPages = await polarSdk.products.list({ isArchived: true }); for await (const page of archivedPages) { if (page.result.items) { stats.polarArchivedCount += page.result.items.length; } } const polarProducts: Product[] = []; const pages = await polarSdk.products.list({ isArchived: false }); for await (const page of pages) { if (page.result.items) { polarProducts.push(...page.result.items); } } const convexProducts = await ctx.runQuery( components.polar.lib.listProducts, { includeArchived: false, } ); for (const convexProduct of convexProducts) { if (!polarProducts.some((p) => p.id === convexProduct.id)) { await ctx.runMutation(components.polar.lib.updateProduct, { product: { ...convexProduct, isArchived: true, }, }); stats.archived++; } } for (const polarProduct of polarProducts) { if (!polarProduct.isRecurring) { console.log(`Skipping non-recurring product: ${polarProduct.name}`); stats.skipped++; continue; } const validPrices = polarProduct.prices.filter( (price: any) => price.amountType !== "custom" && !price.isArchived ); if (validPrices.length === 0) { console.log( `Skipping product with only custom pricing: ${polarProduct.name}` ); stats.skipped++; continue; } const product = { id: polarProduct.id, name: polarProduct.name, description: polarProduct.description, createdAt: polarProduct.createdAt.toISOString(), modifiedAt: polarProduct.modifiedAt?.toISOString() ?? null, isArchived: polarProduct.isArchived, isRecurring: polarProduct.isRecurring, organizationId: polarProduct.organizationId, metadata: polarProduct.metadata, recurringInterval: polarProduct.recurringInterval, medias: polarProduct.medias.map((media) => ({ ...media, createdAt: media.createdAt.toISOString(), lastModifiedAt: media.lastModifiedAt?.toISOString() ?? null, })), prices: validPrices.map((price: any) => ({ id: price.id, productId: price.productId, amountType: price.amountType, isArchived: price.isArchived, createdAt: price.createdAt.toISOString(), modifiedAt: price.modifiedAt?.toISOString() ?? null, recurringInterval: price.type === "recurring" ? (price.recurringInterval ?? undefined) : undefined, priceAmount: price.priceAmount, priceCurrency: price.priceCurrency, type: price.type, })), }; const existingConvexProduct = convexProducts.find( (p) => p.id === polarProduct.id ); try { if (existingConvexProduct) { await ctx.runMutation(components.polar.lib.updateProduct, { product, }); stats.synced++; } else { await ctx.runMutation(components.polar.lib.createProduct, { product, }); stats.created++; } } catch (error) { console.error(`Failed to sync product ${polarProduct.name}:`, error); if (error instanceof Error && error.message.includes("unique()")) { try { await ctx.runMutation(components.polar.lib.updateProduct, { product, }); stats.synced++; } catch (updateError) { console.error( `Failed to update product ${polarProduct.name} after duplicate error:`, updateError ); } } } } const statsString = Object.entries(stats) .filter(([_, value]) => value > 0) .map(([key, value]) => `${key}: ${value}`) .join(", "); console.log(`Polar products sync - ${statsString}`); if (stats.polarArchivedCount > 0) { console.log( `Note: ${stats.polarArchivedCount} archived products in Polar were not synced` ); } return stats; }, }); export const syncProducts = action({ args: {}, handler: async ( ctx ): Promise<{ success: boolean; compatibleProductCount: number; skippedProductCount: number; archivedProductCount: number; polarArchivedCount: number; }> => { if (!process.env.POLAR_ORGANIZATION_TOKEN) { throw new Error("POLAR_ORGANIZATION_TOKEN not set"); } const stats = await ctx.runAction( internal.payments.polar._syncPolarProducts ); return { success: true, compatibleProductCount: stats.created + stats.synced, skippedProductCount: stats.skipped, archivedProductCount: stats.archived, polarArchivedCount: stats.polarArchivedCount || 0, }; }, }); export const clearAllProducts = action({ args: {}, handler: async ( ctx ): Promise<{ archivedCount: number; cleanedDuplicates: number }> => { const cleanupResult = await ctx.runAction( internal.payments.cleanup.cleanupDuplicateProducts ); const products = await ctx.runQuery(components.polar.lib.listProducts, { includeArchived: false, }); let archivedCount = 0; for (const product of products) { try { await ctx.runMutation(components.polar.lib.updateProduct, { product: { ...product, isArchived: true, }, }); archivedCount++; } catch (error) { console.error(`Failed to archive product ${product.id}:`, error); } } return { archivedCount, cleanedDuplicates: cleanupResult.deletedCount, }; }, });