diff --git a/packages/payments/scripts/create-checkout-link.ts b/packages/payments/scripts/create-checkout-link.ts new file mode 100644 index 00000000..14b2bee0 --- /dev/null +++ b/packages/payments/scripts/create-checkout-link.ts @@ -0,0 +1,246 @@ +import { db } from '@openpanel/db'; +import { Polar } from '@polar-sh/sdk'; +import inquirer from 'inquirer'; +import inquirerAutocomplete from 'inquirer-autocomplete-prompt'; +import { getSuccessUrl } from '..'; + +// Register the autocomplete prompt +inquirer.registerPrompt('autocomplete', inquirerAutocomplete); + +interface Answers { + organizationId: string; + userId: string; + productId: string; +} + +async function promptForInput(polar: Polar) { + // Get all organizations first + const organizations = await db.organization.findMany({ + select: { + id: true, + name: true, + }, + }); + + // Fetch all products from Polar + let products: any[] = []; + try { + const productsResponse = await polar.products.list({ + limit: 100, + isArchived: false, + sorting: ['price_amount'], + }); + products = productsResponse.result.items; + + if (products.length === 0) { + console.warn('Warning: No products found in Polar'); + } + } catch (error) { + console.error('Error fetching products from Polar:', error); + throw new Error('Failed to fetch products. Please check your API key and try again.'); + } + + const answers = await inquirer.prompt([ + { + type: 'autocomplete', + name: 'productId', + message: 'Select product:', + source: (answersSoFar: any, input = '') => { + return products + .filter( + (product) => + product.name.toLowerCase().includes(input.toLowerCase()) || + product.id.toLowerCase().includes(input.toLowerCase()), + ) + .map((product) => { + const price = product.prices[0]; + const priceDisplay = price + ? `$${(price.priceAmount / 100).toFixed(2)}` + : 'Free'; + return { + name: `${product.name} - ${priceDisplay} (${product.id})`, + value: product.id, + }; + }); + }, + }, + { + type: 'autocomplete', + name: 'organizationId', + message: 'Select organization:', + source: (answersSoFar: any, input = '') => { + return organizations + .filter( + (org) => + org.name.toLowerCase().includes(input.toLowerCase()) || + org.id.toLowerCase().includes(input.toLowerCase()), + ) + .map((org) => ({ + name: `${org.name} (${org.id})`, + value: org.id, + })); + }, + }, + { + type: 'autocomplete', + name: 'userId', + message: 'Select user:', + source: async (answersSoFar: Answers, input = '') => { + if (!answersSoFar.organizationId) { + return []; + } + try { + const org = await db.organization.findFirst({ + where: { + id: answersSoFar.organizationId, + }, + include: { + members: { + select: { + role: true, + user: true, + }, + }, + }, + }); + + if (!org || !org.members || org.members.length === 0) { + return [{ name: 'No members found', value: '', disabled: true }]; + } + + return org.members + .filter( + (member) => + member.user?.email + .toLowerCase() + .includes(input.toLowerCase()) || + member.user?.firstName + ?.toLowerCase() + .includes(input.toLowerCase()), + ) + .map((member) => ({ + name: `${ + [member.user?.firstName, member.user?.lastName] + .filter(Boolean) + .join(' ') || 'No name' + } (${member.user?.email}) [${member.role}]`, + value: member.user?.id, + })); + } catch (error) { + console.error('Error fetching organization members:', error); + return [{ name: 'Error loading members', value: '', disabled: true }]; + } + }, + }, + ]); + + return answers; +} + +async function main() { + try { + console.log('Creating checkout link...'); + + // First, get environment and API key to initialize Polar client + const { isProduction, polarApiKey } = await inquirer.prompt([ + { + type: 'list', + name: 'isProduction', + message: 'Is this for production?', + choices: [ + { name: 'Yes', value: true }, + { name: 'No', value: false }, + ], + default: true, + }, + { + type: 'string', + name: 'polarApiKey', + message: 'Enter your Polar API key:', + validate: (input: string) => { + if (!input) return 'API key is required'; + return true; + }, + }, + ]); + + const polar = new Polar({ + accessToken: polarApiKey!, + server: isProduction ? 'production' : 'sandbox', + }); + + const input = await promptForInput(polar); + + const organization = await db.organization.findUniqueOrThrow({ + where: { + id: input.organizationId, + }, + select: { + id: true, + name: true, + }, + }); + + const user = await db.user.findUniqueOrThrow({ + where: { + id: input.userId, + }, + }); + + const product = await polar.products.get({ id: input.productId }); + + console.log('\nReview the following settings:'); + console.table({ + environment: isProduction ? 'Production' : 'Sandbox', + organization: organization.name, + product: product.name, + productId: product.id, + email: user.email, + name: + [user.firstName, user.lastName].filter(Boolean).join(' ') || 'No name', + }); + + const { confirmed } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirmed', + message: 'Do you want to proceed?', + default: false, + }, + ]); + + if (!confirmed) { + console.log('Operation canceled'); + return; + } + + const checkout = await polar.checkouts.create({ + products: [input.productId], + successUrl: getSuccessUrl( + isProduction + ? 'https://dashboard.openpanel.dev' + : 'http://localhost:3000', + organization.id, + ), + customerEmail: user.email, + customerName: [user.firstName, user.lastName].filter(Boolean).join(' '), + metadata: { + organizationId: organization.id, + userId: user.id, + }, + }); + + console.log('\nCheckout link created successfully!'); + console.table({ + url: checkout.url, + id: checkout.id, + }); + } catch (error) { + console.error('Error creating checkout link:', error); + throw error; + } +} + +main() + .catch(console.error) + .finally(() => db.$disconnect());