codalware-auth
Version:
Complete authentication system with enterprise security, attack protection, team workspaces, waitlist, billing, UI components, 2FA, and account recovery - production-ready in 5 minutes. Enhanced CLI with verification, rollback, and App Router scaffolding.
257 lines (224 loc) • 8.04 kB
text/typescript
import { CheckoutStatus, Prisma } from '@prisma/client'
import { prisma } from './db'
export type CheckoutLineItemInput = {
id?: string
label: string
description?: string
amount: number
quantity?: number
imageUrl?: string
metadata?: Record<string, unknown>
}
export type CreateCheckoutSessionInput = {
amount: number
currency: string
organizationId?: string | null
tenantId?: string | null
customerEmail?: string | null
customerName?: string | null
successUrl?: string | null
cancelUrl?: string | null
createdBy?: string | null
metadata?: Record<string, unknown>
lineItems?: CheckoutLineItemInput[]
provider: string
paymentMethod?: string | null
}
export type ProviderCreateSessionInput = CreateCheckoutSessionInput & {
sessionId: string
}
export type ProviderCreateSessionResult = {
providerSessionId?: string | null
paymentUrl?: string | null
status?: CheckoutStatus
expiresAt?: Date | null
metadata?: Record<string, unknown>
}
export type ProviderRetrieveSessionResult = {
status: CheckoutStatus
providerSessionId?: string | null
paymentUrl?: string | null
raw?: unknown
}
export type ProviderCancelSessionResult = {
status: CheckoutStatus
canceledAt?: Date
}
export interface PaymentProviderAdapter {
id: string
label: string
description?: string
supportsRecurring?: boolean
supportsOneTime?: boolean
supportedCurrencies?: string[]
createSession?: (
input: ProviderCreateSessionInput,
context: { prisma: typeof prisma }
) => Promise<ProviderCreateSessionResult>
retrieveSession?: (
session: { id: string; providerSessionId?: string | null },
context: { prisma: typeof prisma }
) => Promise<ProviderRetrieveSessionResult>
cancelSession?: (
session: { id: string; providerSessionId?: string | null },
context: { prisma: typeof prisma }
) => Promise<ProviderCancelSessionResult>
metadata?: Record<string, unknown>
}
const providerRegistry = new Map<string, PaymentProviderAdapter>()
const manualProvider: PaymentProviderAdapter = {
id: 'manual',
label: 'Manual / Invoiced',
description: 'Collect intent details now and complete payment manually or via emailed invoice.',
supportsOneTime: true,
supportsRecurring: false,
createSession: async () => ({
status: CheckoutStatus.PENDING,
}),
}
providerRegistry.set(manualProvider.id, manualProvider)
export function registerPaymentProvider(provider: PaymentProviderAdapter) {
if (!provider.id) {
throw new Error('Payment provider must include an "id" field')
}
providerRegistry.set(provider.id, provider)
}
export function unregisterPaymentProvider(id: string) {
providerRegistry.delete(id)
}
export function listPaymentProviders() {
return Array.from(providerRegistry.values()).map(provider => ({
id: provider.id,
label: provider.label,
description: provider.description,
supportsRecurring: provider.supportsRecurring ?? false,
supportsOneTime: provider.supportsOneTime ?? true,
supportedCurrencies: provider.supportedCurrencies,
metadata: provider.metadata,
}))
}
export async function createCheckoutSession(input: CreateCheckoutSessionInput) {
const amount = Math.round(input.amount)
if (!Number.isFinite(amount) || amount <= 0) {
throw new Error('Amount must be a positive integer representing the smallest currency unit (e.g. cents)')
}
if (!input.currency) {
throw new Error('Currency is required')
}
const provider = providerRegistry.get(input.provider) || manualProvider
const session = await prisma.checkoutSession.create({
data: {
amount,
currency: input.currency.toLowerCase(),
organizationId: input.organizationId || undefined,
tenantId: input.tenantId || undefined,
customerEmail: input.customerEmail || undefined,
customerName: input.customerName || undefined,
successUrl: input.successUrl || undefined,
cancelUrl: input.cancelUrl || undefined,
metadata: (input.metadata || undefined) as Prisma.JsonObject | undefined,
lineItems: (input.lineItems || undefined) as Prisma.JsonArray | undefined,
provider: provider.id,
paymentMethod: input.paymentMethod || undefined,
createdBy: input.createdBy || undefined,
},
})
if (provider.createSession) {
try {
const providerResult = await provider.createSession({
...input,
amount,
currency: input.currency.toLowerCase(),
sessionId: session.id,
}, { prisma })
if (providerResult) {
const updated = await prisma.checkoutSession.update({
where: { id: session.id },
data: {
providerSessionId: providerResult.providerSessionId || undefined,
paymentUrl: providerResult.paymentUrl || undefined,
status: providerResult.status || CheckoutStatus.PENDING,
expiresAt: providerResult.expiresAt || undefined,
metadata: mergeMetadata(session.metadata, providerResult.metadata),
},
})
return updated
}
} catch (error) {
await prisma.checkoutSession.update({
where: { id: session.id },
data: { status: CheckoutStatus.FAILED },
})
throw error
}
}
return session
}
export async function retrieveCheckoutSession(sessionId: string) {
const session = await prisma.checkoutSession.findUnique({ where: { id: sessionId } })
if (!session) {
return null
}
const provider = session.provider ? providerRegistry.get(session.provider) : undefined
if (provider?.retrieveSession && session.providerSessionId) {
try {
const upstream = await provider.retrieveSession({
id: session.id,
providerSessionId: session.providerSessionId,
}, { prisma })
if (upstream && upstream.status !== session.status) {
return prisma.checkoutSession.update({
where: { id: session.id },
data: {
status: upstream.status,
paymentUrl: upstream.paymentUrl || session.paymentUrl || undefined,
},
})
}
} catch (error) {
console.warn('Failed to synchronize checkout session', session.id, error)
}
}
return session
}
export async function updateCheckoutStatus(sessionId: string, status: CheckoutStatus, data?: {
providerSessionId?: string | null
paymentUrl?: string | null
metadata?: Record<string, unknown>
completedAt?: Date | null
}) {
return prisma.checkoutSession.update({
where: { id: sessionId },
data: {
status,
providerSessionId: data?.providerSessionId || undefined,
paymentUrl: data?.paymentUrl || undefined,
metadata: mergeMetadata(undefined, data?.metadata),
completedAt: data?.completedAt || undefined,
},
})
}
export async function cancelCheckoutSession(sessionId: string) {
const session = await prisma.checkoutSession.findUnique({ where: { id: sessionId } })
if (!session) {
throw new Error('Session not found')
}
const provider = session.provider ? providerRegistry.get(session.provider) : undefined
if (provider?.cancelSession) {
await provider.cancelSession({
id: session.id,
providerSessionId: session.providerSessionId,
}, { prisma })
}
return prisma.checkoutSession.update({
where: { id: sessionId },
data: { status: CheckoutStatus.CANCELED },
})
}
function mergeMetadata(existing: Prisma.JsonValue | null | undefined, incoming?: Record<string, unknown>) {
if (!incoming || Object.keys(incoming).length === 0) {
return existing as Prisma.InputJsonValue | undefined
}
const base = (existing && typeof existing === 'object') ? existing : {}
return { ...(base as Record<string, unknown>), ...incoming } as Prisma.InputJsonValue
}