Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
309 lines
8.6 KiB
TypeScript
309 lines
8.6 KiB
TypeScript
import type { Product, StripeProduct, StripePrice } from '@/lib/products/types'
|
|
import { getStripeClient } from './client'
|
|
import { filterProducts } from '@/lib/products/filters'
|
|
|
|
/**
|
|
* Fetch all active products from Stripe
|
|
*/
|
|
export async function fetchAllProducts(): Promise<Product[]> {
|
|
const stripe = getStripeClient()
|
|
|
|
try {
|
|
// Fetch all products with their prices
|
|
const products = await stripe.products.list({
|
|
active: true,
|
|
expand: ['data.default_price'],
|
|
limit: 100,
|
|
})
|
|
|
|
// Map Stripe products to our Product format
|
|
const mappedProducts: Product[] = []
|
|
|
|
for (const product of products.data) {
|
|
// Get the default price
|
|
let price: StripePrice | null = null
|
|
let priceId: string | undefined
|
|
|
|
if (product.default_price) {
|
|
if (typeof product.default_price === 'string') {
|
|
priceId = product.default_price
|
|
const priceData = await stripe.prices.retrieve(priceId)
|
|
price = {
|
|
id: priceData.id,
|
|
unit_amount: priceData.unit_amount,
|
|
currency: priceData.currency,
|
|
type: priceData.type,
|
|
active: priceData.active,
|
|
}
|
|
} else {
|
|
price = {
|
|
id: product.default_price.id,
|
|
unit_amount: product.default_price.unit_amount,
|
|
currency: product.default_price.currency,
|
|
type: product.default_price.type,
|
|
active: product.default_price.active,
|
|
}
|
|
priceId = price.id
|
|
}
|
|
}
|
|
|
|
// Only include products with valid prices
|
|
if (price && price.unit_amount !== null && price.active) {
|
|
mappedProducts.push({
|
|
id: product.id,
|
|
stripeId: product.id,
|
|
name: product.name,
|
|
description: product.description,
|
|
price: price.unit_amount / 100, // Convert from cents to dollars
|
|
currency: price.currency,
|
|
images: product.images || [],
|
|
metadata: product.metadata,
|
|
priceId: priceId,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Apply filters to exclude service charges
|
|
return filterProducts(mappedProducts)
|
|
} catch (error) {
|
|
console.error('Error fetching products from Stripe:', error)
|
|
// Return empty array instead of throwing to prevent page crashes
|
|
if (error instanceof Error && error.message.includes('STRIPE_SECRET_KEY')) {
|
|
throw error // Re-throw configuration errors
|
|
}
|
|
return [] // Return empty array for other errors
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch a single product by Stripe ID
|
|
*/
|
|
export async function fetchProductById(id: string): Promise<Product | null> {
|
|
const stripe = getStripeClient()
|
|
|
|
try {
|
|
const product = await stripe.products.retrieve(id, {
|
|
expand: ['default_price'],
|
|
})
|
|
|
|
if (!product.active) {
|
|
return null
|
|
}
|
|
|
|
// Get the default price
|
|
let price: StripePrice | null = null
|
|
let priceId: string | undefined
|
|
|
|
if (product.default_price) {
|
|
if (typeof product.default_price === 'string') {
|
|
priceId = product.default_price
|
|
const priceData = await stripe.prices.retrieve(priceId)
|
|
price = {
|
|
id: priceData.id,
|
|
unit_amount: priceData.unit_amount,
|
|
currency: priceData.currency,
|
|
type: priceData.type,
|
|
active: priceData.active,
|
|
}
|
|
} else {
|
|
price = {
|
|
id: product.default_price.id,
|
|
unit_amount: product.default_price.unit_amount,
|
|
currency: product.default_price.currency,
|
|
type: product.default_price.type,
|
|
active: product.default_price.active,
|
|
}
|
|
priceId = price.id
|
|
}
|
|
}
|
|
|
|
if (!price || price.unit_amount === null || !price.active) {
|
|
return null
|
|
}
|
|
|
|
const mappedProduct: Product = {
|
|
id: product.id,
|
|
stripeId: product.id,
|
|
name: product.name,
|
|
description: product.description,
|
|
price: price.unit_amount / 100,
|
|
currency: price.currency,
|
|
images: product.images || [],
|
|
metadata: product.metadata,
|
|
priceId: priceId,
|
|
}
|
|
|
|
// Check if product should be excluded
|
|
const { shouldExcludeProduct } = await import('@/lib/products/config')
|
|
if (shouldExcludeProduct(mappedProduct.name)) {
|
|
return null
|
|
}
|
|
|
|
return mappedProduct
|
|
} catch (error) {
|
|
console.error(`Error fetching product ${id} from Stripe:`, error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new product in Stripe
|
|
*/
|
|
export async function createProductInStripe(productData: {
|
|
name: string
|
|
description?: string
|
|
price: number
|
|
currency?: string
|
|
images?: string[]
|
|
metadata?: Record<string, string>
|
|
}): Promise<{ product: Product; priceId: string } | null> {
|
|
const stripe = getStripeClient()
|
|
|
|
try {
|
|
const currency = productData.currency || 'usd'
|
|
|
|
// Create the product first
|
|
const stripeProduct = await stripe.products.create({
|
|
name: productData.name,
|
|
description: productData.description || '',
|
|
images: productData.images || [],
|
|
metadata: productData.metadata || {},
|
|
active: true,
|
|
})
|
|
|
|
// Create the price
|
|
const price = await stripe.prices.create({
|
|
product: stripeProduct.id,
|
|
unit_amount: Math.round(productData.price * 100), // Convert to cents
|
|
currency,
|
|
metadata: {
|
|
...productData.metadata,
|
|
created_by: 'admin',
|
|
},
|
|
})
|
|
|
|
// Update product with default price
|
|
await stripe.products.update(stripeProduct.id, {
|
|
default_price: price.id,
|
|
})
|
|
|
|
const mappedProduct: Product = {
|
|
id: stripeProduct.id,
|
|
stripeId: stripeProduct.id,
|
|
name: stripeProduct.name,
|
|
description: stripeProduct.description,
|
|
price: price.unit_amount / 100,
|
|
currency: price.currency,
|
|
images: stripeProduct.images || [],
|
|
metadata: stripeProduct.metadata,
|
|
priceId: price.id,
|
|
}
|
|
|
|
return { product: mappedProduct, priceId: price.id }
|
|
} catch (error) {
|
|
console.error('Error creating product in Stripe:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update an existing product in Stripe
|
|
*/
|
|
export async function updateProductInStripe(
|
|
productId: string,
|
|
updates: {
|
|
name?: string
|
|
description?: string
|
|
price?: number
|
|
images?: string[]
|
|
metadata?: Record<string, string>
|
|
}
|
|
): Promise<Product | null> {
|
|
const stripe = getStripeClient()
|
|
|
|
try {
|
|
// Update product fields
|
|
const updateData: any = {}
|
|
if (updates.name !== undefined) updateData.name = updates.name
|
|
if (updates.description !== undefined) updateData.description = updates.description
|
|
if (updates.images !== undefined) updateData.images = updates.images
|
|
if (updates.metadata !== undefined) updateData.metadata = updates.metadata
|
|
|
|
const updatedProduct = await stripe.products.update(productId, updateData)
|
|
|
|
// Update price if provided
|
|
let priceId = updatedProduct.default_price as string | undefined
|
|
if (updates.price !== undefined && priceId) {
|
|
const existingPrice = await stripe.prices.retrieve(priceId)
|
|
|
|
const updatedPrice = await stripe.prices.update(priceId, {
|
|
unit_amount: Math.round(updates.price * 100),
|
|
metadata: {
|
|
...existingPrice.metadata,
|
|
...updates.metadata,
|
|
updated_at: new Date().toISOString(),
|
|
},
|
|
})
|
|
|
|
priceId = updatedPrice.id
|
|
}
|
|
|
|
const mappedProduct: Product = {
|
|
id: updatedProduct.id,
|
|
stripeId: updatedProduct.id,
|
|
name: updatedProduct.name,
|
|
description: updatedProduct.description,
|
|
price: updates.price || 0,
|
|
currency: 'usd', // You may need to fetch the actual currency
|
|
images: updatedProduct.images || [],
|
|
metadata: updatedProduct.metadata,
|
|
priceId: priceId,
|
|
}
|
|
|
|
return mappedProduct
|
|
} catch (error) {
|
|
console.error(`Error updating product ${productId} in Stripe:`, error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deactivate a product in Stripe (soft delete)
|
|
*/
|
|
export async function deactivateProductInStripe(productId: string): Promise<boolean> {
|
|
const stripe = getStripeClient()
|
|
|
|
try {
|
|
await stripe.products.update(productId, { active: false })
|
|
return true
|
|
} catch (error) {
|
|
console.error(`Error deactivating product ${productId} in Stripe:`, error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch prices for a specific product
|
|
*/
|
|
export async function fetchProductPrices(productId: string): Promise<StripePrice[]> {
|
|
const stripe = getStripeClient()
|
|
|
|
try {
|
|
const prices = await stripe.prices.list({
|
|
product: productId,
|
|
active: true,
|
|
})
|
|
|
|
return prices.data.map((price) => ({
|
|
id: price.id,
|
|
unit_amount: price.unit_amount,
|
|
currency: price.currency,
|
|
type: price.type,
|
|
active: price.active,
|
|
}))
|
|
} catch (error) {
|
|
console.error(`Error fetching prices for product ${productId}:`, error)
|
|
return []
|
|
}
|
|
}
|
|
|