import { isEncodable, type Encodable } from "@automate.ax/codec" import type { StripePublicValue } from "@automate.ax/integration-contracts/stripe" import { isPlainObject } from "@zachsents/zippy" import Stripe from "stripe" import * as z from "zod" import type { DefinedAction } from "../../automation/actions" import type { ResolvedIntegrationAccount } from "../../automation/integrations" const STRIPE_SECRET_SCHEMA = z.object({ apiKey: z.string().min(1) }) const STRIPE_OBJECT_SCHEMA = z.custom>( (value) => isPlainObject(value) && isEncodable(value), { message: "Expected an encodable object." }, ) const STRIPE_EXPAND_SCHEMA = z.string().min(1).array().optional() const STRIPE_IDEMPOTENCY_KEY_SCHEMA = z.string().min(1).max(255).optional() const STRIPE_PARAMETERS_SCHEMA = z .object({ idempotencyKey: STRIPE_IDEMPOTENCY_KEY_SCHEMA }) .loose() type InputObject = Record /** Runtime stripe-node Decimal type used by the normalization guard. */ type StripeDecimal = ReturnType<(typeof Stripe.Decimal)["from"]> /** Camel-cased, codec-safe form of an official Stripe SDK type. */ export type StripeValue = StripePublicValue & Encodable /** Camel-cased official Stripe input type accepted by an action. */ export type StripeInputValue = StripePublicValue & InputObject /** Mutation input with an optional Stripe idempotency key. */ export type StripeMutationInput = StripeInputValue & { /** Prevents duplicate Stripe mutations when the same request is retried. */ idempotencyKey?: string } /** Portable public type for one Stripe-backed action definition. */ export type StripeDefinedAction< TInput extends InputObject, TOutput, > = DefinedAction, z.ZodType>, "stripe"> /** * Keeps declaration emit on public Stripe SDK names instead of private resource * modules. * * @param action - Completed Stripe-backed action definition. * @returns The same action with a portable public type. */ export function stripeAction( action: StripeDefinedAction, ): StripeDefinedAction { return action } /** * Creates the official Stripe client for a resolved integration account. * * @param account - Resolved Stripe integration account. * @returns Authenticated Stripe client. */ export function getStripeApi(account: ResolvedIntegrationAccount<"stripe">) { const { apiKey } = STRIPE_SECRET_SCHEMA.parse(account.secret) return new Stripe(apiKey, { appInfo: { name: "Automate.ax", url: "https://automate.ax" }, }) } /** Runtime schema for a camel-cased official Stripe object or parameter type. */ export function stripeObjectSchema(): z.ZodType> { return z.custom>( (value) => STRIPE_OBJECT_SCHEMA.safeParse(value).success, { message: "Expected an encodable Stripe object." }, ) } /** Shared concrete schemas and factories for Stripe action inputs. */ export const stripeActionSchemas = { empty: z.object({ expand: STRIPE_EXPAND_SCHEMA }).loose(), id: (key: string, prefix?: string) => z .object({ [key]: prefix ? z.string().startsWith(prefix) : z.string().min(1), expand: STRIPE_EXPAND_SCHEMA, }) .loose(), list: z .object({ endingBefore: z.string().min(1).optional(), expand: STRIPE_EXPAND_SCHEMA, limit: z.int().min(1).max(100).optional(), startingAfter: z.string().min(1).optional(), }) .loose(), mutation: (shape: z.ZodRawShape = {}) => z .object({ ...shape, idempotencyKey: STRIPE_IDEMPOTENCY_KEY_SCHEMA }) .loose(), object: (shape: z.ZodRawShape = {}) => z.object(shape).loose(), search: z .object({ expand: STRIPE_EXPAND_SCHEMA, limit: z.int().min(1).max(100).optional(), page: z.string().min(1).optional(), query: z.string().min(1), }) .loose(), } /** * Runtime schema for an already public, codec-safe Stripe object type. * * @param schema - Concrete provider parameter validation. */ export function stripeSchema( schema: z.ZodType, ): z.ZodType { return z.custom( (value) => schema.safeParse(value).success && STRIPE_PARAMETERS_SCHEMA.safeParse(value).success && STRIPE_OBJECT_SCHEMA.safeParse(value).success, { message: "Expected valid Stripe parameters." }, ) } /** * Runtime schema for camel-cased official Stripe action parameters. * * @param schema - Concrete provider parameter validation. */ export function stripeInputSchema( schema: z.ZodType, ): z.ZodType> { return stripeSchema>(schema) } /** * Runtime schema for a Stripe mutation input with request idempotency. * * @param schema - Concrete provider parameter validation. */ export function stripeMutationSchema( schema: z.ZodType, ): z.ZodType> { return z.custom>( (value) => schema.safeParse(value).success && STRIPE_OBJECT_SCHEMA.safeParse(value).success && (!isPlainObject(value) || value.idempotencyKey === undefined || (typeof value.idempotencyKey === "string" && value.idempotencyKey.length > 0)), { message: "Expected Stripe parameters and a valid idempotency key." }, ) } /** * Converts public camelCase parameters to Stripe's provider-native keys. * * @param value - Public action parameters. * @returns Stripe provider parameters. */ export function toStripeParams(value: StripeInputValue | InputObject): T { return z .custom((input) => STRIPE_OBJECT_SCHEMA.safeParse(input).success) .parse(transformKeys(value, "snake")) } /** * Converts an official Stripe response to stable public camelCase keys. * * @param value - Stripe provider response. * @returns Codec-safe public response. */ export function fromStripe(value: T): StripeValue { return stripeObjectSchema().parse(transformKeys(value, "camel")) } /** * Splits public mutation input into Stripe parameters and request options. * * @param input - Public mutation input. * @returns Stripe parameters and optional request options. */ export function stripeMutationArguments(input: StripeMutationInput) { const { idempotencyKey, ...parameters } = z .object({ idempotencyKey: z.string().min(1).optional() }) .loose() .parse(input) return [ toStripeParams(parameters), idempotencyKey ? { idempotencyKey } : undefined, ] as const } /** * Makes durable replay conditional on one stable Stripe idempotency key. * * @param input - Validated Stripe mutation input. * @param input.idempotencyKey - Optional provider idempotency key. */ export function stripeMutationReplaySafety(input: { idempotencyKey?: string }) { return input.idempotencyKey ? ("safe" as const) : ("unsafe" as const) } /** * Converts record keys recursively while preserving metadata keys. * * @param value - Value to transform. * @param direction - Target key convention. * @returns Value with transformed record keys. */ function transformKeys(value: unknown, direction: "camel" | "snake"): unknown { if (isStripeDecimal(value)) return value.toString() if (Array.isArray(value)) { return value.map((item) => transformKeys(item, direction)) } if (!isPlainObject(value)) return value return Object.fromEntries( Object.entries(value).map(([key, item]) => { return [ direction === "camel" ? toCamelCase(key) : toSnakeCase(key), key === "metadata" ? item : transformKeys(item, direction), ] }), ) } /** * Returns whether a value uses stripe-node's private Decimal implementation. * * @param value - Candidate Stripe response value. */ function isStripeDecimal(value: unknown): value is StripeDecimal { return ( typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.getPrototypeOf(Stripe.Decimal.zero) ) } /** * Converts one provider key to camel case. * * @param value - Provider key. * @returns Public key. */ function toCamelCase(value: string) { return value.replace(/_([a-z0-9])/g, (_match, character: string) => character.toUpperCase(), ) } /** * Converts one public key to snake case. * * @param value - Public key. * @returns Provider key. */ function toSnakeCase(value: string) { return value.replace(/[A-Z]/g, (character) => `_${character.toLowerCase()}`) }