From deb3c3d20c17f2ce593f8066f5eedeb7bbc297a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Wed, 21 Jan 2026 21:56:20 +0100 Subject: [PATCH] chore: add assign product to org script --- .../payments/scripts/assign-product-to-org.ts | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 packages/payments/scripts/assign-product-to-org.ts diff --git a/packages/payments/scripts/assign-product-to-org.ts b/packages/payments/scripts/assign-product-to-org.ts new file mode 100644 index 00000000..862c8ee3 --- /dev/null +++ b/packages/payments/scripts/assign-product-to-org.ts @@ -0,0 +1,221 @@ +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 { + isProduction: boolean; + polarApiKey: string; + productId: string; + organizationId: string; +} + +async function promptForInput() { + // Get all organizations first + const organizations = await db.organization.findMany({ + select: { + id: true, + name: true, + }, + }); + + // Step 1: Collect Polar credentials first + const polarCredentials = await inquirer.prompt<{ + isProduction: boolean; + polarApiKey: string; + polarOrganizationId: string; + }>([ + { + 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; + }, + }, + ]); + + // Step 2: Initialize Polar client and fetch products + const polar = new Polar({ + accessToken: polarCredentials.polarApiKey, + server: polarCredentials.isProduction ? 'production' : 'sandbox', + }); + + console.log('Fetching products from Polar...'); + const productsResponse = await polar.products.list({ + limit: 100, + isArchived: false, + sorting: ['price_amount'], + }); + + const products = productsResponse.result.items; + + if (products.length === 0) { + throw new Error('No products found in Polar'); + } + + // Step 3: Continue with product selection and organization selection + const restOfAnswers = await inquirer.prompt<{ + productId: string; + organizationId: string; + }>([ + { + 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 priceStr = + price && 'priceAmount' in price && price.priceAmount + ? `$${(price.priceAmount / 100).toFixed(2)}/${price.recurringInterval || 'month'}` + : 'No price'; + return { + name: `${product.name} (${priceStr})`, + 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, + })); + }, + }, + ]); + + return { + ...polarCredentials, + ...restOfAnswers, + }; +} + +async function main() { + console.log('Assigning existing product to organization...'); + const input = await promptForInput(); + + const polar = new Polar({ + accessToken: input.polarApiKey, + server: input.isProduction ? 'production' : 'sandbox', + }); + + const organization = await db.organization.findUniqueOrThrow({ + where: { + id: input.organizationId, + }, + select: { + id: true, + name: true, + createdBy: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + }, + projects: { + select: { + id: true, + }, + }, + }, + }); + + if (!organization.createdBy) { + throw new Error( + `Organization ${organization.name} does not have a creator. Cannot proceed.`, + ); + } + + const user = organization.createdBy; + + // Fetch product details for review + const product = await polar.products.get({ id: input.productId }); + const price = product.prices?.[0]; + const priceStr = + price && 'priceAmount' in price && price.priceAmount + ? `$${(price.priceAmount / 100).toFixed(2)}/${price.recurringInterval || 'month'}` + : 'No price'; + + console.log('\nReview the following settings:'); + console.table({ + product: product.name, + price: priceStr, + organization: organization.name, + 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 checkoutLink = await polar.checkoutLinks.create({ + paymentProcessor: 'stripe', + productId: input.productId, + allowDiscountCodes: false, + metadata: { + organizationId: organization.id, + userId: user.id, + }, + successUrl: getSuccessUrl( + input.isProduction + ? 'https://dashboard.openpanel.dev' + : 'http://localhost:3000', + organization.id, + ), + }); + + console.log('\nCheckout link created:'); + console.table(checkoutLink); + console.log('\nProduct assigned successfully!'); +} + +main() + .catch(console.error) + .finally(() => db.$disconnect());