chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { createCustomerInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const createCustomer: IntegrationProps['actions']['createCustomer'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = createCustomerInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response
|
||||
try {
|
||||
const params = {
|
||||
email: validatedInput.email,
|
||||
name: validatedInput.name,
|
||||
phone: validatedInput.phone,
|
||||
description: validatedInput.description,
|
||||
payment_method: validatedInput.paymentMethodId,
|
||||
address: validatedInput.address ? JSON.parse(validatedInput.address) : undefined,
|
||||
}
|
||||
const customer = await stripeClient.createCustomer(params)
|
||||
|
||||
response = {
|
||||
customer,
|
||||
}
|
||||
logger.forBot().info(`Successful - Create Customer - ${customer.id}`)
|
||||
} catch (error) {
|
||||
response = {}
|
||||
logger.forBot().debug(`'Create Customer' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createOrRetrieveCustomerInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const createOrRetrieveCustomer: IntegrationProps['actions']['createOrRetrieveCustomer'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = createOrRetrieveCustomerInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response
|
||||
try {
|
||||
const customers = await stripeClient.searchCustomers(validatedInput.email)
|
||||
let customer
|
||||
if (customers.length === 0) {
|
||||
const params = {
|
||||
email: validatedInput.email,
|
||||
name: validatedInput.name,
|
||||
phone: validatedInput.phone,
|
||||
description: validatedInput.description,
|
||||
payment_method: validatedInput.paymentMethodId,
|
||||
address: validatedInput.address ? JSON.parse(validatedInput.address) : undefined,
|
||||
}
|
||||
customer = await stripeClient.createCustomer(params)
|
||||
response = { customer }
|
||||
} else {
|
||||
response = customers.length === 1 ? { customer: customers[0] } : { customers }
|
||||
}
|
||||
logger.forBot().info(`Successful - Create or Retrieve Customer ${customer ? customer.id : ''}`)
|
||||
} catch (error) {
|
||||
response = {}
|
||||
logger.forBot().debug(`'Create or Retrieve Customer' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { createPaymentLinkInputSchema } from '../misc/custom-schemas'
|
||||
import type { ProductBasic } from '../misc/stripe-client'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
const findOrCreateProduct = async (stripeClient: StripeClient, productName: string) => {
|
||||
const products = await stripeClient.listAllProductsBasic()
|
||||
let product = products.find((p: ProductBasic) => p.name === productName)
|
||||
|
||||
if (!product) {
|
||||
product = await stripeClient.createProduct(productName)
|
||||
}
|
||||
|
||||
return product
|
||||
}
|
||||
|
||||
const findOrCreatePrice = async (
|
||||
stripeClient: StripeClient,
|
||||
productId: string,
|
||||
unitAmount: number,
|
||||
currency: string
|
||||
) => {
|
||||
const prices = await stripeClient.listPrices(productId)
|
||||
|
||||
if (!unitAmount) {
|
||||
return prices.data[0] || (await stripeClient.createPrice(productId, 0, currency))
|
||||
}
|
||||
|
||||
let price = prices.data.find((p) => p.unit_amount === unitAmount && p.currency === currency)
|
||||
|
||||
if (!price) {
|
||||
price = await stripeClient.createPrice(productId, unitAmount, currency)
|
||||
}
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
const buildLineItem = (
|
||||
priceId: string,
|
||||
quantity: number,
|
||||
adjustableQuantity: boolean,
|
||||
adjustableQuantityMaximum: number,
|
||||
adjustableQuantityMinimum: number
|
||||
) => {
|
||||
return {
|
||||
price: priceId,
|
||||
quantity: quantity || 1,
|
||||
adjustable_quantity: {
|
||||
enabled: adjustableQuantity || false,
|
||||
maximum: adjustableQuantity ? adjustableQuantityMaximum || 99 : undefined,
|
||||
minimum: adjustableQuantity ? adjustableQuantityMinimum || 1 : undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const createPaymentLink: IntegrationProps['actions']['createPaymentLink'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = createPaymentLinkInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
|
||||
try {
|
||||
const product = await findOrCreateProduct(stripeClient, validatedInput.productName)
|
||||
const price = await findOrCreatePrice(stripeClient, product.id, validatedInput.unit_amount, validatedInput.currency)
|
||||
|
||||
const lineItem = buildLineItem(
|
||||
price.id,
|
||||
validatedInput.quantity,
|
||||
validatedInput.adjustableQuantity,
|
||||
validatedInput.adjustableQuantityMaximum,
|
||||
validatedInput.adjustableQuantityMinimum
|
||||
)
|
||||
|
||||
const paymentLink = await stripeClient.createPaymentLink(lineItem)
|
||||
|
||||
logger.forBot().info(`Successful - Create Payment Link - ${paymentLink.id}`)
|
||||
|
||||
return {
|
||||
id: paymentLink.id,
|
||||
url: paymentLink.url,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.forBot().debug(`'Create Payment Link' exception ${JSON.stringify(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import Stripe from 'stripe'
|
||||
import { createSubsLinkInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const createSubsLink: IntegrationProps['actions']['createSubsLink'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = createSubsLinkInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response
|
||||
try {
|
||||
const products = await stripeClient.listAllProductsBasic()
|
||||
let product = products.find((p) => p.name === validatedInput.productName)
|
||||
|
||||
if (!product) {
|
||||
product = await stripeClient.createProduct(validatedInput.productName)
|
||||
}
|
||||
|
||||
const prices = await stripeClient.listPrices(product.id)
|
||||
let price
|
||||
if (!validatedInput.unit_amount) {
|
||||
price = prices.data.find((p) => p.recurring)
|
||||
} else {
|
||||
price = prices.data.find(
|
||||
(p) => p.unit_amount === validatedInput.unit_amount && p.currency === validatedInput.currency && p.recurring
|
||||
)
|
||||
}
|
||||
|
||||
const validIntervals = ['day', 'week', 'month', 'year']
|
||||
const interval: Stripe.PriceCreateParams.Recurring.Interval = validIntervals.includes(
|
||||
validatedInput.chargingInterval
|
||||
)
|
||||
? (validatedInput.chargingInterval as Stripe.PriceCreateParams.Recurring.Interval)
|
||||
: 'month'
|
||||
|
||||
if (!price) {
|
||||
price = await stripeClient.createSubsPrice(product.id, validatedInput.unit_amount, validatedInput.currency, {
|
||||
interval,
|
||||
})
|
||||
}
|
||||
|
||||
const adjustableQuantity = validatedInput.adjustableQuantity || false
|
||||
const lineItem = {
|
||||
price: price.id,
|
||||
quantity: validatedInput.quantity || 1,
|
||||
adjustable_quantity: {
|
||||
enabled: adjustableQuantity,
|
||||
maximum: adjustableQuantity ? validatedInput.adjustableQuantityMaximum || 99 : undefined,
|
||||
minimum: adjustableQuantity ? validatedInput.adjustableQuantityMinimum || 1 : undefined,
|
||||
},
|
||||
}
|
||||
const subscriptionData = validatedInput.trial_period_days
|
||||
? {
|
||||
description: validatedInput.description,
|
||||
trial_period_days: validatedInput.trial_period_days || 1,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const paymentLink = await stripeClient.createSubsLink(lineItem, subscriptionData)
|
||||
|
||||
response = {
|
||||
id: paymentLink.id,
|
||||
url: paymentLink.url,
|
||||
}
|
||||
|
||||
logger.forBot().info(`Successful - Create Subscription Link - ${paymentLink.id}`)
|
||||
} catch (error) {
|
||||
response = {}
|
||||
|
||||
logger.forBot().debug(`'Create Subscription Link' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { deactivatePaymentLinkInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const deactivatePaymentLink: IntegrationProps['actions']['deactivatePaymentLink'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = deactivatePaymentLinkInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response
|
||||
try {
|
||||
const paymentLink = await stripeClient.deactivatePaymentLink(validatedInput.id)
|
||||
|
||||
response = {
|
||||
id: paymentLink.id,
|
||||
url: paymentLink.url,
|
||||
active: paymentLink.active,
|
||||
}
|
||||
|
||||
logger.forBot().info(`Successful - Deactivate Payment Link - ${paymentLink?.id}`)
|
||||
} catch (error) {
|
||||
response = {}
|
||||
logger.forBot().debug(`'Deactivate Payment Link' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { findPaymentLinkInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const findPaymentLink: IntegrationProps['actions']['findPaymentLink'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = findPaymentLinkInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response = {}
|
||||
try {
|
||||
const paymentLinks = await stripeClient.listAllPaymentLinksBasic()
|
||||
const paymentLink = paymentLinks.find((link) => link.url === validatedInput.url)
|
||||
|
||||
if (paymentLink) {
|
||||
response = {
|
||||
id: paymentLink.id,
|
||||
}
|
||||
}
|
||||
|
||||
logger.forBot().info(`Successful - Find Payment Link - ${paymentLink?.id}`)
|
||||
} catch (error) {
|
||||
logger.forBot().debug(`'Find Payment Link' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createCustomer } from './create-customer'
|
||||
import { createOrRetrieveCustomer } from './create-or-retrieve-customer'
|
||||
import { createPaymentLink } from './create-paymentlink'
|
||||
import { createSubsLink } from './create-subslink'
|
||||
import { deactivatePaymentLink } from './deactivate-paymentlink'
|
||||
import { findPaymentLink } from './find-paymentlink'
|
||||
import { listCustomers } from './list-customers'
|
||||
import { listPaymentLinks } from './list-paymentlinks'
|
||||
import { listProductPrices } from './list-product-prices'
|
||||
import { retrieveCustomerById } from './retrieve-customer'
|
||||
import { searchCustomers } from './search-customers'
|
||||
|
||||
export default {
|
||||
createPaymentLink,
|
||||
listProductPrices,
|
||||
createSubsLink,
|
||||
listPaymentLinks,
|
||||
findPaymentLink,
|
||||
deactivatePaymentLink,
|
||||
listCustomers,
|
||||
searchCustomers,
|
||||
createCustomer,
|
||||
createOrRetrieveCustomer,
|
||||
retrieveCustomerById,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Customer } from 'src/misc/custom-types'
|
||||
import { listCustomersInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const listCustomers: IntegrationProps['actions']['listCustomers'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = listCustomersInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response
|
||||
try {
|
||||
const customers = await stripeClient.listAllCustomerBasic(validatedInput.email || undefined)
|
||||
|
||||
const customerByEmails: Record<string, Customer[]> = {}
|
||||
|
||||
for (const customer of customers) {
|
||||
const emailKey = customer.email || 'null_email'
|
||||
if (customerByEmails[emailKey]) {
|
||||
customerByEmails[emailKey]?.push(customer)
|
||||
} else {
|
||||
customerByEmails[emailKey] = [customer]
|
||||
}
|
||||
}
|
||||
|
||||
response = {
|
||||
customers: customerByEmails,
|
||||
}
|
||||
logger.forBot().info('Successful - List Customers')
|
||||
} catch (error) {
|
||||
response = {}
|
||||
logger.forBot().debug(`'List Customers' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const listPaymentLinks: IntegrationProps['actions']['listPaymentLinks'] = async ({ ctx, client, logger }) => {
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response
|
||||
try {
|
||||
const paymentLinks = await stripeClient.listAllPaymentLinksBasic()
|
||||
|
||||
response = {
|
||||
paymentLinks,
|
||||
}
|
||||
logger.forBot().info(`Successful - List Payment Links - Total Active: ${paymentLinks.length}`)
|
||||
} catch (error) {
|
||||
response = {}
|
||||
logger.forBot().debug(`'List Payment Links' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Product } from 'src/misc/custom-types'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const listProductPrices: IntegrationProps['actions']['listProductPrices'] = async ({ ctx, client, logger }) => {
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response
|
||||
try {
|
||||
const prices = await stripeClient.listAllPricesBasic(undefined, true)
|
||||
|
||||
prices.map((price) => {
|
||||
return {
|
||||
productName: (price.product as any).name,
|
||||
}
|
||||
})
|
||||
|
||||
const products: Record<string, Product> = {}
|
||||
|
||||
for (const price of prices) {
|
||||
if (typeof price.product !== 'string' && 'id' in price.product) {
|
||||
const product = products[price.product.id]
|
||||
if (product) {
|
||||
product.prices.push({
|
||||
unit_amount: price.unit_amount,
|
||||
currency: price.currency,
|
||||
recurring: price.recurring,
|
||||
})
|
||||
} else {
|
||||
products[price.product.id] = {
|
||||
name: (price.product as any).name,
|
||||
prices: [
|
||||
{
|
||||
unit_amount: price.unit_amount,
|
||||
currency: price.currency,
|
||||
recurring: price.recurring,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = {
|
||||
products,
|
||||
}
|
||||
logger.forBot().info('Successful - List Product Prices')
|
||||
} catch (error) {
|
||||
response = {}
|
||||
logger.forBot().debug(`'List Product Prices' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { retrieveCustomerByIdInputSchema } from '../misc/custom-schemas'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const retrieveCustomerById: bp.IntegrationProps['actions']['retrieveCustomerById'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = retrieveCustomerByIdInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
|
||||
try {
|
||||
const customer = await stripeClient.retrieveCustomer(validatedInput.id)
|
||||
if (customer.deleted) {
|
||||
logger.forBot().info(`Customer not found - Retrieve Customer - ${validatedInput.id}`)
|
||||
return {}
|
||||
}
|
||||
|
||||
logger.forBot().info(`Successful - Retrieve Customer - ${customer.id}`)
|
||||
return customer
|
||||
} catch (error) {
|
||||
logger.forBot().debug(`'Create or Retrieve Customer' exception ${JSON.stringify(error)}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { searchCustomersInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const searchCustomers: IntegrationProps['actions']['searchCustomers'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = searchCustomersInputSchema.parse(input)
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
let response
|
||||
try {
|
||||
const customers = await stripeClient.searchCustomers(
|
||||
validatedInput.email,
|
||||
validatedInput.name,
|
||||
validatedInput.phone
|
||||
)
|
||||
|
||||
response = {
|
||||
customers,
|
||||
}
|
||||
logger.forBot().info('Successful - Search Customers')
|
||||
} catch (error) {
|
||||
response = {}
|
||||
logger.forBot().debug(`'Search Customers' exception ${JSON.stringify(error)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe'
|
||||
import { getUserIdFromCustomer } from './utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Client = bp.Client
|
||||
type Events = bp.events.Events
|
||||
type IntegrationLogger = bp.Logger
|
||||
|
||||
export const fireChargeFailed = async ({
|
||||
stripeEvent,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
stripeEvent: Stripe.ChargeFailedEvent
|
||||
client: Client
|
||||
logger: IntegrationLogger
|
||||
}) => {
|
||||
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
|
||||
|
||||
logger.forBot().debug('Triggering charge failed event')
|
||||
|
||||
const payload = {
|
||||
origin: 'stripe',
|
||||
userId,
|
||||
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
|
||||
} satisfies Events['chargeFailed']
|
||||
|
||||
await client.createEvent({
|
||||
type: 'chargeFailed',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { fireChargeFailed } from './charge-failed'
|
||||
import { fireInvoicePaymentFailed } from './invoice-payment-failed'
|
||||
import { firePaymentIntentFailed } from './payment-intent-failed'
|
||||
import { fireSubscriptionCreated } from './subscription-created'
|
||||
import { fireSubscriptionDeleted } from './subscription-deleted'
|
||||
import { fireSubscriptionUpdated } from './subscription-updated'
|
||||
|
||||
export default {
|
||||
fireChargeFailed,
|
||||
fireInvoicePaymentFailed,
|
||||
firePaymentIntentFailed,
|
||||
fireSubscriptionCreated,
|
||||
fireSubscriptionDeleted,
|
||||
fireSubscriptionUpdated,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe'
|
||||
import { getUserIdFromCustomer } from './utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Client = bp.Client
|
||||
type Events = bp.events.Events
|
||||
type IntegrationLogger = bp.Logger
|
||||
|
||||
export const fireInvoicePaymentFailed = async ({
|
||||
stripeEvent,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
stripeEvent: Stripe.InvoicePaymentFailedEvent
|
||||
client: Client
|
||||
logger: IntegrationLogger
|
||||
}) => {
|
||||
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
|
||||
|
||||
logger.forBot().debug('Triggering invoice payment failed event')
|
||||
|
||||
const payload = {
|
||||
origin: 'stripe',
|
||||
userId,
|
||||
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
|
||||
} satisfies Events['invoicePaymentFailed']
|
||||
|
||||
await client.createEvent({
|
||||
type: 'invoicePaymentFailed',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe'
|
||||
import { getUserIdFromCustomer } from './utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Client = bp.Client
|
||||
type Events = bp.events.Events
|
||||
type IntegrationLogger = bp.Logger
|
||||
|
||||
export const firePaymentIntentFailed = async ({
|
||||
stripeEvent,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
stripeEvent: Stripe.PaymentIntentPaymentFailedEvent
|
||||
client: Client
|
||||
logger: IntegrationLogger
|
||||
}) => {
|
||||
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
|
||||
|
||||
logger.forBot().debug('Triggering payment intent failed event')
|
||||
|
||||
const payload = {
|
||||
origin: 'stripe',
|
||||
userId,
|
||||
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
|
||||
} satisfies Events['paymentIntentFailed']
|
||||
|
||||
await client.createEvent({
|
||||
type: 'paymentIntentFailed',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe'
|
||||
import { getUserIdFromCustomer } from './utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Client = bp.Client
|
||||
type Events = bp.events.Events
|
||||
type IntegrationLogger = bp.Logger
|
||||
|
||||
export const fireSubscriptionCreated = async ({
|
||||
stripeEvent,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
stripeEvent: Stripe.CustomerSubscriptionCreatedEvent
|
||||
client: Client
|
||||
logger: IntegrationLogger
|
||||
}) => {
|
||||
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
|
||||
|
||||
logger.forBot().debug('Triggering subscription created event')
|
||||
|
||||
const payload = {
|
||||
origin: 'stripe',
|
||||
userId,
|
||||
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
|
||||
} satisfies Events['subscriptionCreated']
|
||||
|
||||
await client.createEvent({
|
||||
type: 'subscriptionCreated',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe'
|
||||
import { getUserIdFromCustomer } from './utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Client = bp.Client
|
||||
type Events = bp.events.Events
|
||||
type IntegrationLogger = bp.Logger
|
||||
|
||||
export const fireSubscriptionDeleted = async ({
|
||||
stripeEvent,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
stripeEvent: Stripe.CustomerSubscriptionDeletedEvent
|
||||
client: Client
|
||||
logger: IntegrationLogger
|
||||
}) => {
|
||||
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
|
||||
|
||||
logger.forBot().debug('Triggering subscription deleted event')
|
||||
|
||||
const payload = {
|
||||
origin: 'stripe',
|
||||
userId,
|
||||
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
|
||||
} satisfies Events['subscriptionDeleted']
|
||||
|
||||
await client.createEvent({
|
||||
type: 'subscriptionDeleted',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe'
|
||||
import { getUserIdFromCustomer } from './utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Client = bp.Client
|
||||
type Events = bp.events.Events
|
||||
type IntegrationLogger = bp.Logger
|
||||
|
||||
export const fireSubscriptionScheduleCreated = async ({
|
||||
stripeEvent,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
stripeEvent: Stripe.SubscriptionScheduleCreatedEvent
|
||||
client: Client
|
||||
logger: IntegrationLogger
|
||||
}) => {
|
||||
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
|
||||
|
||||
logger.forBot().debug('Triggering subscription schedule created event')
|
||||
|
||||
const payload = {
|
||||
origin: 'stripe',
|
||||
userId,
|
||||
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
|
||||
} satisfies Events['subscriptionScheduleCreated']
|
||||
|
||||
await client.createEvent({
|
||||
type: 'subscriptionScheduleCreated',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe'
|
||||
import { getUserIdFromCustomer } from './utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Client = bp.Client
|
||||
type Events = bp.events.Events
|
||||
type IntegrationLogger = bp.Logger
|
||||
|
||||
export const fireSubscriptionScheduleUpdated = async ({
|
||||
stripeEvent,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
stripeEvent: Stripe.SubscriptionScheduleUpdatedEvent
|
||||
client: Client
|
||||
logger: IntegrationLogger
|
||||
}) => {
|
||||
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
|
||||
|
||||
logger.forBot().debug('Triggering subscription schedule created event')
|
||||
|
||||
const payload = {
|
||||
origin: 'stripe',
|
||||
userId,
|
||||
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
|
||||
} satisfies Events['subscriptionScheduleUpdated']
|
||||
|
||||
await client.createEvent({
|
||||
type: 'subscriptionScheduleUpdated',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe'
|
||||
import { getUserIdFromCustomer } from './utils'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type Client = bp.Client
|
||||
type Events = bp.events.Events
|
||||
type IntegrationLogger = bp.Logger
|
||||
|
||||
export const fireSubscriptionUpdated = async ({
|
||||
stripeEvent,
|
||||
client,
|
||||
logger,
|
||||
}: {
|
||||
stripeEvent: Stripe.CustomerSubscriptionUpdatedEvent
|
||||
client: Client
|
||||
logger: IntegrationLogger
|
||||
}) => {
|
||||
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
|
||||
|
||||
logger.forBot().debug('Triggering subscription updated event')
|
||||
|
||||
const payload = {
|
||||
origin: 'stripe',
|
||||
userId,
|
||||
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
|
||||
} satisfies Events['subscriptionUpdated']
|
||||
|
||||
await client.createEvent({
|
||||
type: 'subscriptionUpdated',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Stripe from 'stripe'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const NO_USER_ID = 'no user'
|
||||
type Customer = string | Stripe.Customer | Stripe.DeletedCustomer | null
|
||||
|
||||
const isCustomerObject = (obj: unknown): obj is Stripe.Customer => {
|
||||
return obj !== null && typeof obj === 'object' && 'id' in obj && typeof obj.id === 'string'
|
||||
}
|
||||
|
||||
const isDeletedCustomerObject = (obj: unknown): obj is Stripe.DeletedCustomer => {
|
||||
return (
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'id' in obj &&
|
||||
typeof obj.id === 'string' &&
|
||||
'deleted' in obj &&
|
||||
obj.deleted === true
|
||||
)
|
||||
}
|
||||
|
||||
const getOrCreateUserFromCustomer = async (client: bp.Client, customer: Customer) => {
|
||||
if (isDeletedCustomerObject(customer)) {
|
||||
return await client.getOrCreateUser({ tags: { id: customer.id } })
|
||||
} else if (isCustomerObject(customer)) {
|
||||
return await client.getOrCreateUser({ tags: { id: customer.id } })
|
||||
} else if (typeof customer === 'string') {
|
||||
return await client.getOrCreateUser({ tags: { id: customer } })
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserIdFromCustomer = async (client: bp.Client, customer: Customer): Promise<string> => {
|
||||
const userResponse = await getOrCreateUserFromCustomer(client, customer)
|
||||
return userResponse?.user.id ?? NO_USER_ID
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { isOAuthWizardUrl } from '@botpress/common/src/oauth-wizard'
|
||||
import actions from './actions'
|
||||
import { oauthWizardHandler } from './oauth-wizard'
|
||||
import { register, unregister, handler } from './setup'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default new bp.Integration({
|
||||
register,
|
||||
unregister,
|
||||
actions,
|
||||
channels: {},
|
||||
handler: async (props) => {
|
||||
if (isOAuthWizardUrl(props.req.path)) {
|
||||
return await oauthWizardHandler(props)
|
||||
}
|
||||
return await handler(props)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,403 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
const partialCustomer = z
|
||||
.object({
|
||||
id: z.string().describe('The id of the customer').title('ID'),
|
||||
email: z.string().nullable().describe('The email of the customer').title('Email'),
|
||||
name: z.string().nullable().optional().describe('The name of the customer').title('Name'),
|
||||
description: z.string().nullable().describe('The description of the customer').title('Description'),
|
||||
phone: z.string().nullable().optional().describe('The phone of the customer').title('Phone'),
|
||||
address: z.object({}).passthrough().nullable().optional().describe('The addess of the customer').title('Address'),
|
||||
created: z.number().describe('The creation timestamp (UNIX) of the customer').title('Created'),
|
||||
delinquent: z.boolean().nullable().optional().describe('If the customer is delinquent').title('Delinquent'),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export const createPaymentLinkInputSchema = z.object({
|
||||
productName: z
|
||||
.string()
|
||||
.placeholder('ex: T-Shirt')
|
||||
.title('Product Name')
|
||||
.describe('The name of the product to be sold'),
|
||||
unit_amount: z
|
||||
.number()
|
||||
.min(50, 'Amount must be at least 50 cents ($0.50 USD)')
|
||||
.optional()
|
||||
.default(50)
|
||||
.title('Unit Price')
|
||||
.describe('The unit price in cents (minimum 50 cents for USD)'),
|
||||
currency: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('usd')
|
||||
.title('Currency')
|
||||
.describe('The currency in which the price will be expressed'),
|
||||
quantity: z.number().title('Quantity').optional().default(1).describe('The quantity of the product being purchased'),
|
||||
adjustableQuantity: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.title('Adjustable Quantity')
|
||||
.describe('Whether or not the quantity can be adjusted'),
|
||||
adjustableQuantityMaximum: z
|
||||
.number()
|
||||
.min(2)
|
||||
.max(999)
|
||||
.optional()
|
||||
.default(99)
|
||||
.title('Max Quantity')
|
||||
.describe('The maximum quantity the customer can purchase, up to 999'),
|
||||
adjustableQuantityMinimum: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(998)
|
||||
.optional()
|
||||
.default(1)
|
||||
.title('Min Quantity')
|
||||
.describe('The minimum quantity the customer can purchase'),
|
||||
})
|
||||
|
||||
export const createPaymentLinkOutputSchema = z
|
||||
.object({
|
||||
id: z.string().describe('The ID of the created payment link').title('ID'),
|
||||
url: z.string().describe('The URL of the created payment link').title('URL'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const listProductPricesInputSchema = z.object({})
|
||||
|
||||
export const listProductPricesOutputSchema = z
|
||||
.object({
|
||||
products: z
|
||||
.record(
|
||||
z.object({
|
||||
name: z.string().describe('The name of the product').title('Name'),
|
||||
prices: z
|
||||
.array(
|
||||
z.object({
|
||||
unit_amount: z.number().nullable().describe('The unit amount for the product').title('Unit Amount'),
|
||||
currency: z.string().describe('The currency for the product').title('Currency'),
|
||||
recurring: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe('Recurring price details')
|
||||
.title('Recurring'),
|
||||
})
|
||||
)
|
||||
.describe('A list of prices for the product')
|
||||
.title('Prices'),
|
||||
})
|
||||
)
|
||||
.describe('A record of products and their prices')
|
||||
.title('Products'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const createSubsLinkInputSchema = z.object({
|
||||
productName: z.string().title('Product Name').describe('The name of the subscription product'),
|
||||
unit_amount: z.number().title('Unit Amount').optional().default(0).describe('The unit price in cents'),
|
||||
currency: z
|
||||
.string()
|
||||
.title('Currency')
|
||||
.optional()
|
||||
.default('usd')
|
||||
.describe('The currency in which the price is expressed'),
|
||||
quantity: z
|
||||
.number()
|
||||
.title('Quantity')
|
||||
.optional()
|
||||
.default(1)
|
||||
.describe('The quantity of the subscription being purchased'),
|
||||
adjustableQuantity: z
|
||||
.boolean()
|
||||
.title('Adjustable Quantity')
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Whether or not the quantity can be adjusted'),
|
||||
adjustableQuantityMaximum: z
|
||||
.number()
|
||||
.title('Max Quantity')
|
||||
.min(2)
|
||||
.max(999)
|
||||
.optional()
|
||||
.default(99)
|
||||
.describe('The maximum quantity the customer can purchase, up to 999'),
|
||||
adjustableQuantityMinimum: z
|
||||
.number()
|
||||
.title('Min Quantity')
|
||||
.min(1)
|
||||
.max(998)
|
||||
.optional()
|
||||
.default(1)
|
||||
.describe('The minimum quantity the customer can purchase'),
|
||||
chargingInterval: z // change to .enum(['day', 'week', 'month', 'year'])
|
||||
.string()
|
||||
.title('Payment Interval')
|
||||
.optional()
|
||||
.default('month')
|
||||
.describe('The interval at which the customer will be charged'),
|
||||
trial_period_days: z
|
||||
.number()
|
||||
.title('Trial Period Days')
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe('The number of free trial days for the subscription'),
|
||||
description: z.string().title('Description').optional().describe('A description of the subscription product'),
|
||||
})
|
||||
|
||||
export const createSubsLinkOutputSchema = z
|
||||
.object({
|
||||
id: z.string().describe('The ID of the created subscription link').title('ID'),
|
||||
url: z.string().describe('The URL of the created subscription link').title('URL'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const listPaymentLinksInputSchema = z.object({})
|
||||
|
||||
export const listPaymentLinksOutputSchema = z
|
||||
.object({
|
||||
paymentLinks: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().describe('The ID of the payment link').title('ID'),
|
||||
url: z.string().describe('The URL of the payment link').title('URL'),
|
||||
})
|
||||
)
|
||||
.describe('A list of payment links')
|
||||
.title('Payment Links'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const findPaymentLinkInputSchema = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.title('Payment link')
|
||||
.describe('The URL of the payment link')
|
||||
.placeholder('ex: https://buy.stripe.com/test_b0tPr3sS5w3sOm3'),
|
||||
})
|
||||
|
||||
export const findPaymentLinkOutputSchema = z
|
||||
.object({
|
||||
id: z.string().describe('The ID of the found payment link').title('ID'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const deactivatePaymentLinkInputSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.title('Payment Link ID')
|
||||
.describe('The payment link ID to deactivate')
|
||||
.placeholder('ex: test_aEUdTEdRP95RdvaaEJ'),
|
||||
})
|
||||
|
||||
export const deactivatePaymentLinkOutputSchema = z
|
||||
.object({
|
||||
id: z.string().describe('The ID of the deactivated payment link').title('ID'),
|
||||
url: z.string().describe('The URL of the deactivated payment link').title('URL'),
|
||||
active: z.boolean().describe('Whether the payment link is active').title('Active'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const listCustomersInputSchema = z.object({
|
||||
email: z.string().email().max(512).optional().describe('The e-mail of the customer to search for').title('Email'),
|
||||
})
|
||||
|
||||
export const listCustomersOutputSchema = z
|
||||
.object({
|
||||
customers: z.record(z.array(partialCustomer)).describe('A record of customers grouped by email').title('Customers'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const searchCustomersInputSchema = z.object({
|
||||
email: z.string().title('Email').max(512).optional().describe('Search by query on customer emails').title('Email'),
|
||||
name: z.string().title('Name').optional().describe('Search by query on customer name').title('Name'),
|
||||
phone: z.string().title('Phone').optional().describe('Search by query on customer phone number').title('Phone'),
|
||||
})
|
||||
|
||||
export const searchCustomersOutputSchema = z
|
||||
.object({
|
||||
customers: z.array(partialCustomer).describe('A list of customers matching the search criteria').title('Customers'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const createCustomerInputSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.max(512)
|
||||
.title('Email')
|
||||
.describe('The email of the customer')
|
||||
.placeholder('johndoe@botpress.com'),
|
||||
name: z.string().optional().describe('The name of the customer').placeholder('John Doe').title('Name'),
|
||||
phone: z.string().optional().describe('The phone number of the customer').placeholder('+1234567890').title('Phone'),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Description')
|
||||
.describe('A description for the customer')
|
||||
.placeholder('Customer Description'),
|
||||
paymentMethodId: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Payment Method ID')
|
||||
.describe('The ID of the payment method to attach to the customer')
|
||||
.placeholder('payment-method-id'),
|
||||
address: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Address')
|
||||
.describe('The address of the customer')
|
||||
.placeholder('123 Bot Street, Bot City, Botland, 12345'),
|
||||
})
|
||||
|
||||
export const createCustomerOutputSchema = z
|
||||
.object({
|
||||
customer: partialCustomer.describe('The created customer object').title('Customer'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const createOrRetrieveCustomerInputSchema = createCustomerInputSchema
|
||||
|
||||
export const createOrRetrieveCustomerOutputSchema = z
|
||||
.object({
|
||||
customer: partialCustomer.optional().describe('The created or retrieved customer object').title('Customer'),
|
||||
customers: z
|
||||
.array(partialCustomer)
|
||||
.optional()
|
||||
.describe('A list of customers matching the criteria')
|
||||
.title('Customers'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const retrieveCustomerByIdInputSchema = z.object({
|
||||
id: z.string().describe('The ID of the customer to retrieve').title('Customer ID').placeholder('cus_1234567890'),
|
||||
})
|
||||
|
||||
export const retrieveCustomerByIdOutputSchema = z
|
||||
.object({
|
||||
id: z.string().describe('The ID of the retrieved customer').title('Customer ID'),
|
||||
email: z.string().describe('The email of the retrieved customer').title('Customer Email').nullable(),
|
||||
description: z
|
||||
.string()
|
||||
.describe('The description of the retrieved customer')
|
||||
.title('Customer Description')
|
||||
.nullable(),
|
||||
created: z.number().describe('The creation timestamp (UNIX) of the customer').title('Customer Created Timestamp'),
|
||||
})
|
||||
.passthrough()
|
||||
.partial()
|
||||
|
||||
const baseSchema = z.object({
|
||||
origin: z.literal('stripe').describe('The origin of the event trigger').title('Origin'),
|
||||
userId: z.string().describe('Botpress User ID').title('User ID'),
|
||||
})
|
||||
|
||||
export const chargeFailedSchema = baseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
type: z.string().default('charge.failed').describe('The data type').title('Type'),
|
||||
object: z.object({}).passthrough().describe('The object of the failed charge').title('Charge Object'),
|
||||
})
|
||||
.describe('The data to send with the event')
|
||||
.title('Data'),
|
||||
})
|
||||
|
||||
export const subscriptionCreatedSchema = baseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
type: z.string().default('customer.subscription.created').describe('The data type').title('Type'),
|
||||
object: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('The object of the created subscription')
|
||||
.title('Subscription Object'),
|
||||
})
|
||||
.describe('The data to send with the event')
|
||||
.title('Data'),
|
||||
})
|
||||
|
||||
export const subscriptionDeletedSchema = baseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
type: z.string().default('customer.subscription.deleted').describe('The data type').title('Type'),
|
||||
object: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('The object of the deleted subscription')
|
||||
.title('Subscription Object'),
|
||||
})
|
||||
.describe('The data to send with the event')
|
||||
.title('Data'),
|
||||
})
|
||||
|
||||
export const subscriptionUpdatedSchema = baseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
type: z.string().default('customer.subscription.updated').describe('The data type').title('Type'),
|
||||
object: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('The object of the updated subscription')
|
||||
.title('Subscription Object'),
|
||||
})
|
||||
.describe('The data to send with the event')
|
||||
.title('Data'),
|
||||
})
|
||||
|
||||
export const invoicePaymentFailedSchema = baseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
type: z.string().default('invoice.payment_failed').describe('The data type').title('Type'),
|
||||
object: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('The object of the invoice whose payment failed')
|
||||
.title('Invoice Object'),
|
||||
})
|
||||
.describe('The data to send with the event')
|
||||
.title('Data'),
|
||||
})
|
||||
|
||||
export const paymentIntentFailedSchema = baseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
type: z.string().default('payment_intent.payment_failed').describe('The data type').title('Type'),
|
||||
object: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('The object of the payment intent that failed')
|
||||
.title('Payment Intent Object'),
|
||||
})
|
||||
.describe('The data to send with the event')
|
||||
.title('Data'),
|
||||
})
|
||||
|
||||
export const subscriptionScheduleCreatedSchema = baseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
type: z.string().default('subscription_schedule.created').describe('The data type').title('Type'),
|
||||
object: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('The object of the created subscription schedule')
|
||||
.title('Subscription Schedule Object'),
|
||||
})
|
||||
.describe('The data to send with the event')
|
||||
.title('Data'),
|
||||
})
|
||||
|
||||
export const subscriptionScheduleUpdatedSchema = baseSchema.extend({
|
||||
data: z
|
||||
.object({
|
||||
type: z.string().default('subscription_schedule.updated').describe('The data type').title('Type'),
|
||||
object: z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.describe('The object of the updated subscription schedule')
|
||||
.title('Subscription Schedule Object'),
|
||||
})
|
||||
.describe('The data to send with the event')
|
||||
.title('Data'),
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
type Product = {
|
||||
name: string
|
||||
prices: Array<{
|
||||
unit_amount: number | null
|
||||
currency: string
|
||||
recurring?: object | null
|
||||
}>
|
||||
}
|
||||
|
||||
type Address = {
|
||||
city: string | null
|
||||
country: string | null
|
||||
line1: string | null
|
||||
line2: string | null
|
||||
postal_code: string | null
|
||||
state: string | null
|
||||
}
|
||||
|
||||
type Customer = {
|
||||
id: string
|
||||
email: string | null
|
||||
name?: string | null
|
||||
description: string | null
|
||||
phone?: string | null
|
||||
address?: Address | null
|
||||
created: number
|
||||
delinquent?: boolean | null
|
||||
}
|
||||
|
||||
export { Product, Customer }
|
||||
@@ -0,0 +1,35 @@
|
||||
import type Stripe from 'stripe'
|
||||
|
||||
export type ProductBasic = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type PriceBasic = {
|
||||
id: string
|
||||
unit_amount: number | null
|
||||
currency: string
|
||||
recurring?: Stripe.Price.Recurring
|
||||
product: ProductBasic
|
||||
}
|
||||
|
||||
export type CustomerBasic = {
|
||||
id: string
|
||||
email: string | null
|
||||
name: string | null | undefined
|
||||
description: string | null
|
||||
phone: string | null | undefined
|
||||
address: Stripe.Address | null | undefined
|
||||
created: number
|
||||
delinquent: boolean | null | undefined
|
||||
}
|
||||
|
||||
export type PaymentLinkBasic = {
|
||||
id: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export type WebhookBasic = {
|
||||
id: string
|
||||
url: string
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export type Configuration = bp.configuration.Configuration
|
||||
export type IntegrationProps = bp.IntegrationProps
|
||||
export type IntegrationCtx = bp.Context
|
||||
|
||||
export type RegisterFunction = IntegrationProps['register']
|
||||
export type UnregisterFunction = IntegrationProps['unregister']
|
||||
export type Channels = IntegrationProps['channels']
|
||||
export type Handler = IntegrationProps['handler']
|
||||
export type Client = bp.Client
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard'
|
||||
import * as wizard from './wizard'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const oauthWizardHandler: bp.IntegrationProps['handler'] = async (props) => {
|
||||
const { req, logger } = props
|
||||
|
||||
if (!isOAuthWizardUrl(req.path)) {
|
||||
return {
|
||||
status: 404,
|
||||
body: 'Invalid OAuth wizard endpoint',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await wizard.handler(props)
|
||||
} catch (thrown: unknown) {
|
||||
const error = thrown instanceof Error ? thrown : Error(String(thrown))
|
||||
const errorMessage = 'OAuth wizard error: ' + error.message
|
||||
logger.forBot().error(errorMessage)
|
||||
return generateRedirection(getInterstitialUrl(false, errorMessage))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as oauthWizard from '@botpress/common/src/oauth-wizard'
|
||||
import { Response, z } from '@botpress/sdk'
|
||||
import { StripeOAuthClient } from '../stripe-api/stripe-oauth-client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type WizardHandler = oauthWizard.WizardStepHandler<bp.HandlerProps>
|
||||
|
||||
const _getRedirectUri = () => `${process.env.BP_WEBHOOK_URL}/oauth/wizard/oauth-callback`
|
||||
|
||||
const _buildStripeAuthorizeUrl = ({ webhookId }: { webhookId: string }): string => {
|
||||
const params = new URLSearchParams({
|
||||
client_id: bp.secrets.CLIENT_ID,
|
||||
redirect_uri: _getRedirectUri(),
|
||||
response_type: 'code',
|
||||
state: webhookId,
|
||||
})
|
||||
return `https://marketplace.stripe.com/oauth/v2/authorize?${params.toString()}`
|
||||
}
|
||||
|
||||
const _manualCredentialsSchema = z.object({
|
||||
apiKey: z
|
||||
.string()
|
||||
.secret()
|
||||
.min(1)
|
||||
.title('Stripe API Key')
|
||||
.describe('Your Stripe Secret Key (sk_live_/sk_test_) or a Restricted Key'),
|
||||
})
|
||||
|
||||
const _manualCredentialsForm = {
|
||||
pageTitle: 'Stripe API Key',
|
||||
htmlOrMarkdownPageContents:
|
||||
'Enter a Stripe Secret Key (or Restricted Key). You can create one at <a href="https://dashboard.stripe.com/apikeys" target="_blank">https://dashboard.stripe.com/apikeys</a>.',
|
||||
schema: _manualCredentialsSchema,
|
||||
nextStepId: 'save-manual-credentials',
|
||||
}
|
||||
|
||||
export const handler = async (props: bp.HandlerProps): Promise<Response> => {
|
||||
const wizard = new oauthWizard.OAuthWizardBuilder(props)
|
||||
.addStep({ id: 'start', handler: _startHandler })
|
||||
.addStep({ id: 'route-choice', handler: _routeChoiceHandler })
|
||||
.addStep({ id: 'oauth-redirect', handler: _oauthRedirectHandler })
|
||||
.addStep({ id: 'oauth-callback', handler: _oauthCallbackHandler })
|
||||
.addStep({ id: 'get-manual-credentials', handler: _getManualCredentialsHandler })
|
||||
.addStep({ id: 'save-manual-credentials', handler: _saveManualCredentialsHandler })
|
||||
.build()
|
||||
|
||||
return await wizard.handleRequest()
|
||||
}
|
||||
|
||||
const _startHandler: WizardHandler = ({ responses }) => {
|
||||
return responses.displayChoices({
|
||||
pageTitle: 'Stripe Integration Setup',
|
||||
htmlOrMarkdownPageContents: 'Choose how you would like to configure your Stripe integration:',
|
||||
choices: [
|
||||
{ label: 'Connect with OAuth', value: 'oauth' },
|
||||
{ label: 'Use a Stripe API Key', value: 'manual' },
|
||||
],
|
||||
nextStepId: 'route-choice',
|
||||
})
|
||||
}
|
||||
|
||||
const _routeChoiceHandler: WizardHandler = ({ selectedChoice, responses }) => {
|
||||
switch (selectedChoice) {
|
||||
case 'manual':
|
||||
return responses.redirectToStep('get-manual-credentials')
|
||||
case 'oauth':
|
||||
default:
|
||||
return responses.redirectToStep('oauth-redirect')
|
||||
}
|
||||
}
|
||||
|
||||
const _oauthRedirectHandler: WizardHandler = async ({ ctx, responses }) => {
|
||||
return responses.redirectToExternalUrl(_buildStripeAuthorizeUrl({ webhookId: ctx.webhookId }))
|
||||
}
|
||||
|
||||
const _oauthCallbackHandler: WizardHandler = async ({ ctx, client, logger, responses, query }) => {
|
||||
const code = query.get('code')
|
||||
if (!code) {
|
||||
return responses.endWizard({ success: false, errorMessage: 'Stripe did not return an authorization code' })
|
||||
}
|
||||
|
||||
const state = query.get('state')
|
||||
if (!state || state !== ctx.webhookId) {
|
||||
return responses.endWizard({ success: false, errorMessage: 'Invalid OAuth state parameter' })
|
||||
}
|
||||
|
||||
const oauth = new StripeOAuthClient({ client, ctx, logger })
|
||||
await oauth.requestShortLivedCredentials.fromAuthorizationCode(code)
|
||||
|
||||
return responses.endWizard({ success: true })
|
||||
}
|
||||
|
||||
const _getManualCredentialsHandler: WizardHandler = ({ responses }) => {
|
||||
return responses.displayForm(_manualCredentialsForm)
|
||||
}
|
||||
|
||||
const _saveManualCredentialsHandler: WizardHandler = async ({ ctx, client, logger, formValues, responses }) => {
|
||||
if (!formValues) {
|
||||
return responses.redirectToStep('get-manual-credentials')
|
||||
}
|
||||
|
||||
const parsed = _manualCredentialsSchema.safeParse(formValues)
|
||||
if (!parsed.success) {
|
||||
return responses.displayForm({
|
||||
..._manualCredentialsForm,
|
||||
errors: parsed.error,
|
||||
previousValues: formValues as z.input<typeof _manualCredentialsSchema>,
|
||||
})
|
||||
}
|
||||
|
||||
const oauth = new StripeOAuthClient({ client, ctx, logger })
|
||||
await oauth.saveManualApiKey(parsed.data.apiKey)
|
||||
|
||||
return responses.endWizard({ success: true })
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { fireChargeFailed } from 'src/events/charge-failed'
|
||||
import { fireInvoicePaymentFailed } from 'src/events/invoice-payment-failed'
|
||||
import { firePaymentIntentFailed } from 'src/events/payment-intent-failed'
|
||||
import { fireSubscriptionCreated } from 'src/events/subscription-created'
|
||||
import { fireSubscriptionDeleted } from 'src/events/subscription-deleted'
|
||||
import { fireSubscriptionScheduleCreated } from 'src/events/subscription-schedule-created'
|
||||
import { fireSubscriptionScheduleUpdated } from 'src/events/subscription-schedule-updated'
|
||||
import { fireSubscriptionUpdated } from 'src/events/subscription-updated'
|
||||
import Stripe from 'stripe'
|
||||
import type { Handler } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
import { ENABLED_EVENTS } from './register'
|
||||
|
||||
// Stand-in Stripe instance only used for webhook signature verification (no API calls).
|
||||
// @ts-ignore
|
||||
const _signatureVerifier = new Stripe('placeholder', { apiVersion: '2023-08-16' })
|
||||
|
||||
const _recreateWebhookWithSecret = async ({
|
||||
client,
|
||||
ctx,
|
||||
logger,
|
||||
priorWebhookId,
|
||||
}: {
|
||||
client: Parameters<Handler>[0]['client']
|
||||
ctx: Parameters<Handler>[0]['ctx']
|
||||
logger: Parameters<Handler>[0]['logger']
|
||||
priorWebhookId?: string
|
||||
}) => {
|
||||
if (!process.env.BP_WEBHOOK_URL) {
|
||||
throw new Error('BP_WEBHOOK_URL is not configured')
|
||||
}
|
||||
|
||||
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
const { id: stripeWebhookId, secret: stripeWebhookSecret } = await stripeClient.createWebhookEndpointWithSecret({
|
||||
url: `${process.env.BP_WEBHOOK_URL}/${ctx.webhookId}`,
|
||||
enabled_events: ENABLED_EVENTS,
|
||||
})
|
||||
|
||||
await client.setState({
|
||||
type: 'integration',
|
||||
id: ctx.integrationId,
|
||||
name: 'stripeIntegrationInfo',
|
||||
payload: { stripeWebhookId, stripeWebhookSecret },
|
||||
})
|
||||
|
||||
if (priorWebhookId) {
|
||||
try {
|
||||
await stripeClient.deleteWebhook(priorWebhookId)
|
||||
} catch (error) {
|
||||
logger.forBot().warn(`Failed to delete legacy Stripe webhook ${priorWebhookId}`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const handler: Handler = async ({ req, client, ctx, logger }) => {
|
||||
if (!req.body) {
|
||||
logger.forBot().warn('Stripe webhook handler received an empty body')
|
||||
return { status: 400, body: 'Empty body' }
|
||||
}
|
||||
|
||||
const sigHeader = req.headers['stripe-signature']
|
||||
if (!sigHeader) {
|
||||
logger.forBot().warn('Stripe webhook missing stripe-signature header')
|
||||
return { status: 400, body: 'Missing stripe-signature header' }
|
||||
}
|
||||
|
||||
const integrationInfo = await client
|
||||
.getState({ type: 'integration', id: ctx.integrationId, name: 'stripeIntegrationInfo' })
|
||||
.then(({ state }) => state.payload)
|
||||
.catch(() => undefined)
|
||||
const webhookSecret = integrationInfo?.stripeWebhookSecret
|
||||
|
||||
if (!webhookSecret) {
|
||||
try {
|
||||
await _recreateWebhookWithSecret({
|
||||
client,
|
||||
ctx,
|
||||
logger,
|
||||
priorWebhookId: integrationInfo?.stripeWebhookId,
|
||||
})
|
||||
logger.forBot().warn('Recreated Stripe webhook because the stored signing secret was missing')
|
||||
return { status: 202, body: 'Stripe webhook recreated; delivery skipped because it cannot be verified' }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logger.forBot().error(`No Stripe webhook signing secret found and migration failed: ${message}`)
|
||||
return { status: 500, body: 'Webhook signing secret not configured' }
|
||||
}
|
||||
}
|
||||
|
||||
let stripeEvent: Stripe.Event
|
||||
try {
|
||||
stripeEvent = _signatureVerifier.webhooks.constructEvent(req.body, sigHeader, webhookSecret)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logger.forBot().warn(`Stripe webhook signature verification failed: ${message}`)
|
||||
return { status: 400, body: 'Invalid signature' }
|
||||
}
|
||||
|
||||
switch (stripeEvent.type) {
|
||||
case 'charge.failed':
|
||||
await fireChargeFailed({ stripeEvent, client, logger })
|
||||
break
|
||||
case 'invoice.payment_failed':
|
||||
await fireInvoicePaymentFailed({ stripeEvent, client, logger })
|
||||
break
|
||||
case 'payment_intent.payment_failed':
|
||||
await firePaymentIntentFailed({ stripeEvent, client, logger })
|
||||
break
|
||||
case 'customer.subscription.created':
|
||||
await fireSubscriptionCreated({ stripeEvent, client, logger })
|
||||
break
|
||||
case 'customer.subscription.deleted':
|
||||
await fireSubscriptionDeleted({ stripeEvent, client, logger })
|
||||
break
|
||||
case 'customer.subscription.updated':
|
||||
await fireSubscriptionUpdated({ stripeEvent, client, logger })
|
||||
break
|
||||
case 'subscription_schedule.created':
|
||||
await fireSubscriptionScheduleCreated({ stripeEvent, client, logger })
|
||||
break
|
||||
case 'subscription_schedule.updated':
|
||||
await fireSubscriptionScheduleUpdated({ stripeEvent, client, logger })
|
||||
break
|
||||
default:
|
||||
logger.forBot().warn(`Unhandled Stripe event type ${stripeEvent.type}`)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { register } from './register'
|
||||
export { unregister } from './unregister'
|
||||
export { handler } from './handler'
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import Stripe from 'stripe'
|
||||
import type { RegisterFunction } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const ENABLED_EVENTS: Stripe.WebhookEndpointCreateParams.EnabledEvent[] = [
|
||||
'charge.failed',
|
||||
'customer.subscription.deleted',
|
||||
'customer.subscription.updated',
|
||||
'customer.subscription.created',
|
||||
'invoice.payment_failed',
|
||||
'payment_intent.payment_failed',
|
||||
'subscription_schedule.created',
|
||||
'subscription_schedule.updated',
|
||||
]
|
||||
|
||||
export const register: RegisterFunction = async ({ ctx, client, webhookUrl, logger }) => {
|
||||
let stripeClient: StripeClient
|
||||
try {
|
||||
stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
throw new sdk.RuntimeError(`Failed to load Stripe credentials. Re-run the setup wizard. (${message})`)
|
||||
}
|
||||
|
||||
let accountId: string
|
||||
try {
|
||||
const account = await stripeClient.retrieveAccount()
|
||||
accountId = account.id
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
throw new sdk.RuntimeError(`Failed to connect to Stripe. (${message})`)
|
||||
}
|
||||
|
||||
await client.configureIntegration({ identifier: accountId })
|
||||
|
||||
const prior = await client
|
||||
.getState({ type: 'integration', id: ctx.integrationId, name: 'stripeIntegrationInfo' })
|
||||
.then(({ state }) => state.payload)
|
||||
.catch(() => undefined)
|
||||
|
||||
const { id: stripeWebhookId, secret: stripeWebhookSecret } = await stripeClient.createWebhookEndpointWithSecret({
|
||||
url: webhookUrl,
|
||||
enabled_events: ENABLED_EVENTS,
|
||||
})
|
||||
|
||||
await client.setState({
|
||||
type: 'integration',
|
||||
id: ctx.integrationId,
|
||||
name: 'stripeIntegrationInfo',
|
||||
payload: { stripeWebhookId, stripeWebhookSecret },
|
||||
})
|
||||
|
||||
if (prior?.stripeWebhookId && prior.stripeWebhookId !== stripeWebhookId) {
|
||||
try {
|
||||
await stripeClient.deleteWebhook(prior.stripeWebhookId)
|
||||
} catch (error) {
|
||||
logger.forBot().warn(`Failed to delete prior Stripe webhook ${prior.stripeWebhookId}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
logger.forBot().info('Connection to Stripe successful')
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { UnregisterFunction } from '../misc/types'
|
||||
import { StripeClient } from '../stripe-api/stripe-client'
|
||||
|
||||
export const unregister: UnregisterFunction = async ({ ctx, client, logger }) => {
|
||||
let stripeClient: StripeClient
|
||||
try {
|
||||
stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
|
||||
} catch {
|
||||
logger.forBot().warn('No Stripe credentials available; skipping webhook teardown')
|
||||
return
|
||||
}
|
||||
|
||||
const stateStripeIntegrationInfo = await client
|
||||
.getState({ id: ctx.integrationId, name: 'stripeIntegrationInfo', type: 'integration' })
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!stateStripeIntegrationInfo) return
|
||||
|
||||
const { stripeWebhookId } = stateStripeIntegrationInfo.state.payload
|
||||
if (stripeWebhookId) {
|
||||
const response = await stripeClient.deleteWebhook(stripeWebhookId)
|
||||
if (response.deleted) {
|
||||
logger.forBot().info(`Webhook successfully deleted - ${response.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import Stripe from 'stripe'
|
||||
import { StripeOAuthClient } from './stripe-oauth-client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type CreateProps = {
|
||||
client: bp.Client
|
||||
ctx: bp.Context
|
||||
logger: bp.Logger
|
||||
}
|
||||
|
||||
type LegacyConfiguration = bp.Context['configuration'] & {
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
export class StripeClient {
|
||||
protected _stripe: Stripe
|
||||
|
||||
public constructor(apiKey: string, apiVersion?: string) {
|
||||
// @ts-ignore
|
||||
this._stripe = new Stripe(apiKey, { apiVersion: apiVersion || '2023-08-16' })
|
||||
}
|
||||
|
||||
public static async createFromStates({ client, ctx, logger }: CreateProps): Promise<StripeClient> {
|
||||
const oauth = new StripeOAuthClient({ client, ctx, logger })
|
||||
|
||||
// essential for old API key support
|
||||
let accessToken: string
|
||||
try {
|
||||
accessToken = (await oauth.getAuthState()).accessToken
|
||||
} catch (error) {
|
||||
const legacyApiKey = (ctx.configuration as LegacyConfiguration).apiKey
|
||||
if (!legacyApiKey) {
|
||||
throw error
|
||||
}
|
||||
|
||||
await oauth.saveManualApiKey(legacyApiKey)
|
||||
accessToken = legacyApiKey
|
||||
}
|
||||
return new StripeClient(accessToken, ctx.configuration.apiVersion)
|
||||
}
|
||||
|
||||
public async retrieveAccount(): Promise<Stripe.Account> {
|
||||
return await this._stripe.accounts.retrieve()
|
||||
}
|
||||
|
||||
public async createProduct(name: string) {
|
||||
const product = await this._stripe.products.create({ name })
|
||||
return product
|
||||
}
|
||||
|
||||
public async listProducts(startingAfter?: string) {
|
||||
return await this._stripe.products.list({
|
||||
active: true,
|
||||
limit: 100,
|
||||
starting_after: startingAfter,
|
||||
})
|
||||
}
|
||||
|
||||
public async listAllProductsBasic(startingAfter?: string) {
|
||||
let products = await this.listProducts(startingAfter)
|
||||
const productsBasic = products.data.map((product) => ({ id: product.id, name: product.name }))
|
||||
while (products.has_more) {
|
||||
products = await this.listProducts(productsBasic[productsBasic.length - 1]?.id)
|
||||
for (const product of products.data) {
|
||||
productsBasic.push({ id: product.id, name: product.name })
|
||||
}
|
||||
}
|
||||
return productsBasic
|
||||
}
|
||||
|
||||
public async createPrice(product: string, unit_amount: number, currency: string) {
|
||||
return await this._stripe.prices.create({ product, unit_amount, currency })
|
||||
}
|
||||
|
||||
public async createSubsPrice(
|
||||
product: string,
|
||||
unit_amount: number,
|
||||
currency: string,
|
||||
recurring: Stripe.PriceCreateParams.Recurring
|
||||
) {
|
||||
return await this._stripe.prices.create({ product, unit_amount, currency, recurring })
|
||||
}
|
||||
|
||||
public async listPrices(productId?: string, isExpand: boolean = false, startingAfter?: string) {
|
||||
return await this._stripe.prices.list({
|
||||
active: true,
|
||||
limit: 100,
|
||||
product: productId,
|
||||
expand: isExpand ? ['data.product'] : undefined,
|
||||
starting_after: startingAfter,
|
||||
})
|
||||
}
|
||||
|
||||
public async listAllPricesBasic(productId?: string, isExpand: boolean = false, startingAfter?: string) {
|
||||
let prices = await this.listPrices(productId, isExpand, startingAfter)
|
||||
const pricesBasic = prices.data.map((price) => ({
|
||||
id: price.id,
|
||||
unit_amount: price.unit_amount,
|
||||
currency: price.currency,
|
||||
recurring: price.recurring || undefined,
|
||||
product: {
|
||||
id: (price.product as Stripe.Product).id,
|
||||
name: (price.product as Stripe.Product).name,
|
||||
},
|
||||
}))
|
||||
while (prices.has_more) {
|
||||
prices = await this.listPrices(productId, isExpand, pricesBasic[pricesBasic.length - 1]?.id)
|
||||
for (const price of prices.data) {
|
||||
pricesBasic.push({
|
||||
id: price.id,
|
||||
unit_amount: price.unit_amount,
|
||||
currency: price.currency,
|
||||
recurring: price.recurring || undefined,
|
||||
product: {
|
||||
id: (price.product as Stripe.Product).id,
|
||||
name: (price.product as Stripe.Product).name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return pricesBasic
|
||||
}
|
||||
|
||||
public async createPaymentLink(lineItem: Stripe.PaymentLinkCreateParams.LineItem) {
|
||||
return await this._stripe.paymentLinks.create({ line_items: [lineItem] })
|
||||
}
|
||||
|
||||
public async listPaymentLink(startingAfter?: string) {
|
||||
return await this._stripe.paymentLinks.list({
|
||||
active: true,
|
||||
limit: 100,
|
||||
starting_after: startingAfter,
|
||||
})
|
||||
}
|
||||
|
||||
public async listAllPaymentLinksBasic(startingAfter?: string) {
|
||||
let paymentLinks = await this.listPaymentLink(startingAfter)
|
||||
const paymentLinksBasic = paymentLinks.data.map((paymentLink) => ({
|
||||
id: paymentLink.id,
|
||||
url: paymentLink.url,
|
||||
}))
|
||||
while (paymentLinks.has_more) {
|
||||
paymentLinks = await this.listPaymentLink(paymentLinksBasic[paymentLinksBasic.length - 1]?.id)
|
||||
for (const paymentLink of paymentLinks.data) {
|
||||
paymentLinksBasic.push({ id: paymentLink.id, url: paymentLink.url })
|
||||
}
|
||||
}
|
||||
return paymentLinksBasic
|
||||
}
|
||||
|
||||
public async createSubsLink(
|
||||
lineItem: Stripe.PaymentLinkCreateParams.LineItem,
|
||||
subscriptionData: Stripe.PaymentLinkCreateParams.SubscriptionData | undefined
|
||||
) {
|
||||
return await this._stripe.paymentLinks.create({
|
||||
line_items: [lineItem],
|
||||
subscription_data: subscriptionData,
|
||||
})
|
||||
}
|
||||
|
||||
public async deactivatePaymentLink(paymentLinkId: string) {
|
||||
return this._stripe.paymentLinks.update(paymentLinkId, { active: false })
|
||||
}
|
||||
|
||||
public async listCustomer(email?: string, startingAfter?: string) {
|
||||
return await this._stripe.customers.list({ email, limit: 100, starting_after: startingAfter })
|
||||
}
|
||||
|
||||
public async listAllCustomerBasic(email?: string, startingAfter?: string) {
|
||||
let customers = await this.listCustomer(email, startingAfter)
|
||||
const customersBasic = customers.data.map((customer) => ({
|
||||
id: customer.id,
|
||||
email: customer.email,
|
||||
name: customer.name,
|
||||
description: customer.description,
|
||||
phone: customer.phone,
|
||||
address: customer.address,
|
||||
created: customer.created,
|
||||
delinquent: customer.delinquent,
|
||||
}))
|
||||
while (customers.has_more) {
|
||||
customers = await this.listCustomer(email, customersBasic[customersBasic.length - 1]?.id)
|
||||
for (const customer of customers.data) {
|
||||
customersBasic.push({
|
||||
id: customer.id,
|
||||
email: customer.email,
|
||||
name: customer.name,
|
||||
description: customer.description,
|
||||
phone: customer.phone,
|
||||
address: customer.address,
|
||||
created: customer.created,
|
||||
delinquent: customer.delinquent,
|
||||
})
|
||||
}
|
||||
}
|
||||
return customersBasic
|
||||
}
|
||||
|
||||
public async searchCustomers(email?: string, name?: string, phone?: string) {
|
||||
const queryParts: string[] = []
|
||||
if (email) queryParts.push(`email~'${email}'`)
|
||||
if (name) queryParts.push(`name~'${name}'`)
|
||||
if (phone) queryParts.push(`phone~'${phone}'`)
|
||||
const query = queryParts.join(' AND ')
|
||||
const limit = 100
|
||||
|
||||
let response = await this._stripe.customers.search({ query, limit })
|
||||
const customers = response.data
|
||||
while (response.has_more) {
|
||||
const page = response.next_page || undefined
|
||||
response = await this._stripe.customers.search({ query, limit, page })
|
||||
customers.push(...response.data)
|
||||
}
|
||||
return customers
|
||||
}
|
||||
|
||||
public async createCustomer(params: Stripe.CustomerCreateParams) {
|
||||
return await this._stripe.customers.create(params)
|
||||
}
|
||||
|
||||
public async createWebhook(webhookData: Stripe.WebhookEndpointCreateParams) {
|
||||
return await this._stripe.webhookEndpoints.create(webhookData)
|
||||
}
|
||||
|
||||
public async listWebhooks(startingAfter?: string) {
|
||||
return await this._stripe.webhookEndpoints.list({ limit: 100, starting_after: startingAfter })
|
||||
}
|
||||
|
||||
public async listAllWebhooksBasic(startingAfter?: string) {
|
||||
let webhooks = await this.listWebhooks(startingAfter)
|
||||
const webhooksBasic = webhooks.data.map((webhook) => ({ id: webhook.id, url: webhook.url }))
|
||||
while (webhooks.has_more) {
|
||||
webhooks = await this.listWebhooks(webhooksBasic[webhooksBasic.length - 1]?.id)
|
||||
for (const webhook of webhooks.data) {
|
||||
webhooksBasic.push({ id: webhook.id, url: webhook.url })
|
||||
}
|
||||
}
|
||||
return webhooksBasic
|
||||
}
|
||||
|
||||
public async createWebhookEndpointWithSecret(
|
||||
webhookData: Stripe.WebhookEndpointCreateParams
|
||||
): Promise<{ id: string; secret: string }> {
|
||||
const webhook = await this.createWebhook(webhookData)
|
||||
if (!webhook.secret) {
|
||||
throw new Error('Stripe did not return a webhook signing secret on creation')
|
||||
}
|
||||
return { id: webhook.id, secret: webhook.secret }
|
||||
}
|
||||
|
||||
public async deleteWebhook(webhookId: string) {
|
||||
return await this._stripe.webhookEndpoints.del(webhookId)
|
||||
}
|
||||
|
||||
public async retrieveCustomer(id: string) {
|
||||
return await this._stripe.customers.retrieve(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type StripeTokenResponse = {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
token_type: string
|
||||
stripe_user_id: string
|
||||
stripe_publishable_key?: string
|
||||
livemode: boolean
|
||||
scope?: string
|
||||
expires_in?: number
|
||||
refresh_expires_in?: number
|
||||
}
|
||||
|
||||
type PrivateAuthState = {
|
||||
readonly accessToken: {
|
||||
readonly expiresAt: Date
|
||||
readonly token: string
|
||||
}
|
||||
readonly refreshToken: {
|
||||
readonly expiresAt: Date
|
||||
readonly token: string
|
||||
}
|
||||
readonly scopes: string[]
|
||||
readonly stripeUserId: string
|
||||
readonly livemode: boolean
|
||||
}
|
||||
|
||||
type PublicAuthState = {
|
||||
readonly accessToken: string
|
||||
readonly scopes: string[]
|
||||
readonly stripeUserId?: string
|
||||
readonly livemode?: boolean
|
||||
}
|
||||
|
||||
const STRIPE_TOKEN_URL = 'https://api.stripe.com/v1/oauth/token'
|
||||
const MINIMUM_TOKEN_VALIDITY_SECONDS = 3_600
|
||||
const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 3_600
|
||||
const DEFAULT_REFRESH_TOKEN_TTL_SECONDS = 365 * 24 * 60 * 60
|
||||
|
||||
export class StripeOAuthClient {
|
||||
private readonly _client: bp.Client
|
||||
private readonly _ctx: bp.Context
|
||||
private readonly _logger: bp.Logger
|
||||
private readonly _clientSecret: string
|
||||
private _currentAuthState: PrivateAuthState | undefined = undefined
|
||||
|
||||
public constructor({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
clientSecretOverride,
|
||||
}: {
|
||||
client: bp.Client
|
||||
ctx: bp.Context
|
||||
logger: bp.Logger
|
||||
clientSecretOverride?: string
|
||||
}) {
|
||||
this._clientSecret = clientSecretOverride ?? bp.secrets.CLIENT_SECRET
|
||||
this._client = client
|
||||
this._ctx = ctx
|
||||
this._logger = logger
|
||||
}
|
||||
|
||||
public async getAuthState(): Promise<PublicAuthState> {
|
||||
const manual = await this._getManualCredentialsState()
|
||||
if (manual && manual.apiKey !== '') {
|
||||
return { accessToken: manual.apiKey, scopes: [] }
|
||||
}
|
||||
|
||||
await this._refreshAuthStateIfNeeded()
|
||||
|
||||
if (!this._currentAuthState) {
|
||||
throw new sdk.RuntimeError('No Stripe credentials found. Re-run the integration setup wizard.')
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: this._currentAuthState.accessToken.token,
|
||||
scopes: this._currentAuthState.scopes,
|
||||
stripeUserId: this._currentAuthState.stripeUserId,
|
||||
livemode: this._currentAuthState.livemode,
|
||||
}
|
||||
}
|
||||
|
||||
public readonly requestShortLivedCredentials = {
|
||||
fromAuthorizationCode: async (authorizationCode: string) => {
|
||||
this._logger.forBot().debug('Exchanging Stripe authorization code for credentials...')
|
||||
const response = await this._postToken({ grant_type: 'authorization_code', code: authorizationCode })
|
||||
this._currentAuthState = this._parseStripeTokenResponse(response)
|
||||
await this._saveOAuthCredentials()
|
||||
await this._clearManualCredentials()
|
||||
this._logger.forBot().debug('Successfully exchanged Stripe authorization code')
|
||||
},
|
||||
|
||||
fromRefreshToken: async (refreshToken: string) => {
|
||||
this._logger.forBot().debug('Refreshing Stripe access token...')
|
||||
const response = await this._postToken({ grant_type: 'refresh_token', refresh_token: refreshToken })
|
||||
this._currentAuthState = this._parseStripeTokenResponse(response)
|
||||
await this._saveOAuthCredentials()
|
||||
this._logger.forBot().debug('Successfully refreshed Stripe access token')
|
||||
},
|
||||
}
|
||||
|
||||
public async saveManualApiKey(apiKey: string): Promise<void> {
|
||||
await this._client.setState({
|
||||
type: 'integration',
|
||||
name: 'manualCredentials',
|
||||
id: this._ctx.integrationId,
|
||||
payload: { apiKey },
|
||||
})
|
||||
await this._clearOAuthCredentials()
|
||||
}
|
||||
|
||||
private async _postToken(body: Record<string, string>): Promise<StripeTokenResponse> {
|
||||
const basicAuth = Buffer.from(`${this._clientSecret}:`).toString('base64')
|
||||
const response = await fetch(STRIPE_TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${basicAuth}`,
|
||||
},
|
||||
body: new URLSearchParams(body).toString(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
this._logger.forBot().error(`Stripe token endpoint returned ${response.status}: ${errorBody}`)
|
||||
throw new sdk.RuntimeError(`Stripe token request failed (${response.status})`)
|
||||
}
|
||||
|
||||
return (await response.json()) as StripeTokenResponse
|
||||
}
|
||||
|
||||
private async _refreshAuthStateIfNeeded(): Promise<void> {
|
||||
if (this._isTokenStillValid()) {
|
||||
return
|
||||
}
|
||||
|
||||
const credentials = await this._getOAuthCredentialsState()
|
||||
if (!credentials) {
|
||||
throw new sdk.RuntimeError('No Stripe credentials found. Re-run the integration setup wizard.')
|
||||
}
|
||||
|
||||
await this._refreshAuth(credentials)
|
||||
}
|
||||
|
||||
private _isTokenStillValid() {
|
||||
return this._currentAuthState && this._currentAuthState.accessToken.expiresAt > this._getMinExpiryDate()
|
||||
}
|
||||
|
||||
private _getMinExpiryDate() {
|
||||
return new Date(Date.now() + MINIMUM_TOKEN_VALIDITY_SECONDS * 1000)
|
||||
}
|
||||
|
||||
private async _getManualCredentialsState() {
|
||||
return this._client
|
||||
.getState({ type: 'integration', id: this._ctx.integrationId, name: 'manualCredentials' })
|
||||
.then(({ state }) => state.payload)
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
private async _getOAuthCredentialsState() {
|
||||
return this._client
|
||||
.getState({ type: 'integration', id: this._ctx.integrationId, name: 'oAuthCredentials' })
|
||||
.then(({ state }) => state.payload)
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
private async _refreshAuth(credentials: bp.states.oAuthCredentials.OAuthCredentials['payload']) {
|
||||
const accessTokenExpiresAt = new Date(credentials.expiresAt)
|
||||
const refreshTokenExpiresAt = new Date(credentials.refreshExpiresAt)
|
||||
|
||||
if (refreshTokenExpiresAt <= new Date()) {
|
||||
throw new sdk.RuntimeError('Stripe refresh token has expired. Please re-run the integration setup wizard.')
|
||||
}
|
||||
|
||||
if (accessTokenExpiresAt > new Date()) {
|
||||
this._currentAuthState = {
|
||||
accessToken: { expiresAt: accessTokenExpiresAt, token: credentials.accessToken },
|
||||
refreshToken: { expiresAt: refreshTokenExpiresAt, token: credentials.refreshToken },
|
||||
scopes: credentials.scopes,
|
||||
stripeUserId: credentials.stripeUserId,
|
||||
livemode: credentials.livemode,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await this.requestShortLivedCredentials.fromRefreshToken(credentials.refreshToken)
|
||||
}
|
||||
|
||||
private _parseStripeTokenResponse(response: StripeTokenResponse): PrivateAuthState {
|
||||
if (!response.access_token || !response.refresh_token || !response.stripe_user_id) {
|
||||
this._logger.forBot().error('Stripe OAuth response is missing required fields')
|
||||
throw new sdk.RuntimeError('Stripe OAuth response is missing required fields')
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const accessTtl = response.expires_in ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS
|
||||
const refreshTtl = response.refresh_expires_in ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS
|
||||
|
||||
return {
|
||||
accessToken: { expiresAt: new Date(now + accessTtl * 1000), token: response.access_token },
|
||||
refreshToken: { expiresAt: new Date(now + refreshTtl * 1000), token: response.refresh_token },
|
||||
scopes: response.scope ? response.scope.split(' ') : [],
|
||||
stripeUserId: response.stripe_user_id,
|
||||
livemode: response.livemode ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
private async _clearManualCredentials() {
|
||||
await this._client.setState({
|
||||
type: 'integration',
|
||||
name: 'manualCredentials',
|
||||
id: this._ctx.integrationId,
|
||||
payload: { apiKey: '' },
|
||||
})
|
||||
}
|
||||
|
||||
private async _clearOAuthCredentials() {
|
||||
const epoch = new Date(0).toISOString()
|
||||
await this._client.setState({
|
||||
type: 'integration',
|
||||
name: 'oAuthCredentials',
|
||||
id: this._ctx.integrationId,
|
||||
payload: {
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
expiresAt: epoch,
|
||||
refreshExpiresAt: epoch,
|
||||
scopes: [],
|
||||
stripeUserId: '',
|
||||
livemode: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async _saveOAuthCredentials() {
|
||||
if (!this._currentAuthState) {
|
||||
throw new sdk.RuntimeError('No credentials to save')
|
||||
}
|
||||
|
||||
await this._client.setState({
|
||||
type: 'integration',
|
||||
name: 'oAuthCredentials',
|
||||
id: this._ctx.integrationId,
|
||||
payload: {
|
||||
accessToken: this._currentAuthState.accessToken.token,
|
||||
refreshToken: this._currentAuthState.refreshToken.token,
|
||||
expiresAt: this._currentAuthState.accessToken.expiresAt.toISOString(),
|
||||
refreshExpiresAt: this._currentAuthState.refreshToken.expiresAt.toISOString(),
|
||||
scopes: this._currentAuthState.scopes,
|
||||
stripeUserId: this._currentAuthState.stripeUserId,
|
||||
livemode: this._currentAuthState.livemode,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user