(input: CreateProductInput)
| 1572 | * Creates both the product and an associated price with lookup key |
| 1573 | */ |
| 1574 | export async function createProduct(input: CreateProductInput): Promise<BillingProduct | null> { |
| 1575 | if (!stripe) { |
| 1576 | logger.warn('Stripe not initialized - cannot create product'); |
| 1577 | return null; |
| 1578 | } |
| 1579 | |
| 1580 | // Validate lookup key format |
| 1581 | if (!input.lookupKey.startsWith('aao_')) { |
| 1582 | throw new Error('Lookup key must start with "aao_"'); |
| 1583 | } |
| 1584 | |
| 1585 | try { |
| 1586 | // Build metadata |
| 1587 | const metadata: Record<string, string> = { |
| 1588 | category: input.category, |
| 1589 | }; |
| 1590 | if (input.displayName) metadata.display_name = input.displayName; |
| 1591 | if (input.customerTypes?.length) metadata.customer_types = input.customerTypes.join(','); |
| 1592 | if (input.revenueTiers?.length) metadata.revenue_tiers = input.revenueTiers.join(','); |
| 1593 | if (input.invoiceable !== undefined) metadata.invoiceable = String(input.invoiceable); |
| 1594 | if (input.sortOrder !== undefined) metadata.sort_order = String(input.sortOrder); |
| 1595 | |
| 1596 | // Create the product |
| 1597 | const product = await stripe.products.create({ |
| 1598 | name: input.name, |
| 1599 | description: input.description, |
| 1600 | metadata, |
| 1601 | }); |
| 1602 | |
| 1603 | // Create the price with lookup key |
| 1604 | const price = await stripe.prices.create({ |
| 1605 | product: product.id, |
| 1606 | unit_amount: input.amountCents, |
| 1607 | currency: input.currency || 'usd', |
| 1608 | lookup_key: input.lookupKey, |
| 1609 | transfer_lookup_key: true, |
| 1610 | ...(input.billingType === 'subscription' ? { |
| 1611 | recurring: { interval: input.billingInterval || 'year' }, |
| 1612 | } : {}), |
| 1613 | }); |
| 1614 | |
| 1615 | logger.info({ |
| 1616 | productId: product.id, |
| 1617 | priceId: price.id, |
| 1618 | lookupKey: input.lookupKey, |
| 1619 | }, 'Created product and price in Stripe'); |
| 1620 | |
| 1621 | // Clear cache so new product appears |
| 1622 | clearProductsCache(); |
| 1623 | |
| 1624 | // Return the billing product |
| 1625 | return { |
| 1626 | lookup_key: input.lookupKey, |
| 1627 | price_id: price.id, |
| 1628 | product_id: product.id, |
| 1629 | product_name: product.name, |
| 1630 | display_name: input.displayName || product.name, |
| 1631 | description: product.description, |
no test coverage detected