UNPKG

@paykit-sdk/polar

Version:
693 lines (687 loc) 23.5 kB
'use strict'; var core = require('@paykit-sdk/core'); var sdk = require('@polar-sh/sdk'); var refunds_js = require('@polar-sh/sdk/sdk/refunds.js'); var webhooks = require('@polar-sh/sdk/webhooks'); // src/index.ts var Checkout$inboundSchema = (checkout) => { return { id: checkout.id, payment_url: checkout.url, customer: checkout.customerId ? { id: checkout.customerId } : checkout.customerEmail ? { email: checkout.customerEmail } : null, session_type: checkout.subscriptionId ? "recurring" : "one_time", products: checkout.products.map((product) => ({ id: product.id, quantity: 1 })), metadata: core.omitInternalMetadata(checkout.metadata) ?? null, currency: checkout.currency, amount: checkout.amount }; }; var Customer$inboundSchema = (customer) => { const phone = JSON.parse( customer.metadata?.[core.PAYKIT_METADATA_KEY] ?? "{}" ).phone ?? null; return { id: customer.id, email: customer.email, name: customer.name ?? "", phone, metadata: core.omitInternalMetadata(customer.metadata ?? {}), created_at: customer.createdAt, updated_at: customer.modifiedAt ?? null, custom_fields: { emailVerified: customer.emailVerified, taxId: customer.taxId, avatarUrl: customer.avatarUrl, billingAddress: customer.billingAddress } }; }; var Subscription$inboundSchema = (subscription) => { const subscriptionStatusMap = { active: "active", past_due: "past_due", incomplete: "past_due", canceled: "canceled", unpaid: "past_due", incomplete_expired: "expired", trialing: "trialing" }; const status = subscriptionStatusMap[subscription.status]; return { id: subscription.id, customer: subscription.customerId ? { id: subscription.customerId } : subscription.customer.email ? { email: subscription.customer.email } : null, status, current_period_start: new Date(subscription.currentPeriodStart), current_period_end: new Date(subscription.currentPeriodEnd), metadata: core.omitInternalMetadata(subscription.metadata ?? {}), custom_fields: subscription.customFieldData ?? null, item_id: subscription.productId, billing_interval: subscription.recurringInterval, currency: subscription.currency, amount: subscription.amount, requires_action: false, payment_url: null }; }; var Invoice$inboundSchema = (invoice) => { const status = (() => { if (invoice.status == "paid") return "paid"; return "open"; })(); return { id: invoice.id, amount_paid: invoice.totalAmount, currency: invoice.currency, metadata: core.omitInternalMetadata(invoice.metadata ?? {}), customer: invoice.customerId ? { id: invoice.customerId } : invoice.customer.email ? { email: invoice.customer.email } : null, billing_mode: invoice.billingMode, custom_fields: invoice.customFieldData ?? null, status, subscription_id: invoice.subscription?.id ?? null, paid_at: new Date(invoice.createdAt).toISOString(), line_items: invoice.items.map(({ productPriceId }) => ({ id: productPriceId ?? "", quantity: invoice.items.length })) }; }; var Payment$inboundSchema = (checkout) => { const statusMap = { open: "pending", expired: "canceled", confirmed: "requires_capture", succeeded: "succeeded", failed: "failed" }; return { id: checkout.id, amount: checkout.amount, currency: checkout.currency, customer: checkout.customerId ? { id: checkout.customerId } : checkout.customerEmail ? { email: checkout.customerEmail } : null, status: statusMap[checkout.status], metadata: core.omitInternalMetadata(checkout.metadata) ?? {}, item_id: checkout.products.length > 0 ? checkout.products[0].id : null, requires_action: checkout.status === "open" ? true : false, payment_url: checkout.status === "open" ? checkout.url : null }; }; var Refund$inboundSchema = (refund) => { return { id: refund.id, amount: refund.amount, currency: refund.currency, reason: refund.reason, metadata: core.omitInternalMetadata(refund.metadata ?? {}) }; }; // src/polar-provider.ts var polarOptionsSchema = core.schema()( core.Schema.object({ accessToken: core.Schema.string(), isSandbox: core.Schema.boolean(), debug: core.Schema.boolean().optional(), userAgent: core.Schema.string().optional(), retryConfig: core.Schema.any().optional(), timeoutMs: core.Schema.number().optional() }) ); var providerName = "polar"; var PolarProvider = class extends core.AbstractPayKitProvider { constructor(config) { super(polarOptionsSchema, config, providerName); this.config = config; this.providerName = providerName; this.productionURL = sdk.ServerList["production"]; this.sandboxURL = sdk.ServerList["sandbox"]; /** * Checkout management */ this.createCheckout = async (params) => { const { error, data } = core.createCheckoutSchema.safeParse(params); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "createCheckout" ); } const { metadata, item_id, provider_metadata } = data; const checkoutMetadata = core.stringifyMetadataValues(metadata ?? {}); const checkoutCreateOptions = { ...provider_metadata, metadata: checkoutMetadata, products: [item_id], successUrl: data.success_url }; if (typeof data.customer === "object" && "email" in data.customer) { checkoutCreateOptions.customerEmail = data.customer.email; } else if (typeof data.customer === "string") { checkoutCreateOptions.customerId = data.customer; } if (data.billing) { checkoutCreateOptions.customerBillingAddress = { line1: data.billing.address.line1, line2: data.billing.address.line2, postalCode: data.billing.address.postal_code, city: data.billing.address.city, country: data.billing.address.country, state: data.billing.address.state }; checkoutCreateOptions.metadata = { ...checkoutMetadata, _shipping_phone: data.billing.address.phone ?? "", _shipping_carrier: data.billing.carrier ?? "" }; } const response = await this.polar.checkouts.create( checkoutCreateOptions ); return Checkout$inboundSchema(response); }; this.updateCheckout = async (id, params) => { const { error, data } = core.updateCheckoutSchema.safeParse(params); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "updateCheckout" ); } const { metadata, item_id, provider_metadata, ...restData } = data; const response = await this.polar.checkouts.update({ id, checkoutUpdate: { ...restData, ...metadata && { metadata: core.stringifyMetadataValues(metadata ?? {}) }, ...item_id && { products: [item_id] }, ...provider_metadata } }); return Checkout$inboundSchema(response); }; this.retrieveCheckout = async (id) => { const { error } = core.retrieveCheckoutSchema.safeParse({ id }); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "retrieveCheckout" ); } const response = await this.polar.checkouts.get({ id }); return Checkout$inboundSchema(response); }; this.deleteCheckout = async (id) => { throw new core.ProviderNotSupportedError("deleteCheckout", "Polar", { reason: "Polar does not support deleting checkouts" }); }; /** * Customer management */ this.createCustomer = async (params) => { const { error, data } = core.createCustomerSchema.safeParse(params); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "createCustomer" ); } const { email, metadata } = data; const { fullName: name } = core.parseCustomerName({ name: data.name, email }); const response = await this.polar.customers.create({ email, name, metadata: { ...metadata, [core.PAYKIT_METADATA_KEY]: JSON.stringify({ phone: data?.phone ?? null }) }, ...data.provider_metadata }); return Customer$inboundSchema(response); }; this.updateCustomer = async (id, params) => { const { error, data } = core.updateCustomerSchema.safeParse(params); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "retrieveCustomer" ); } const { email, name, metadata, provider_metadata } = data; const response = await this.polar.customers.update({ id, customerUpdate: { ...email && { email }, ...name && { name }, ...metadata && { metadata }, ...provider_metadata } }); return Customer$inboundSchema(response); }; this.retrieveCustomer = async (id) => { const { error } = core.retrieveCustomerSchema.safeParse({ id }); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "retrieveCustomer" ); } const response = await this.polar.customers.get({ id }); return Customer$inboundSchema(response); }; this.deleteCustomer = async (id) => { const customer = await this.polar.customers.get({ id }); if (customer) await this.polar.customers.delete({ id }); return null; }; /** * Subscription management */ this.createSubscription = async (params) => { throw new core.ProviderNotSupportedError( "createSubscription", "Polar", { reason: "Subscriptions can only be created through checkouts" } ); }; this.cancelSubscription = async (id) => { const { error } = core.retrieveSubscriptionSchema.safeParse({ id }); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "retrieveSubscription" ); } const subscription = await this.polar.subscriptions.revoke({ id }); return Subscription$inboundSchema(subscription); }; this.retrieveSubscription = async (id) => { const { error } = core.retrieveSubscriptionSchema.safeParse({ id }); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "retrieveSubscription" ); } const response = await this.polar.subscriptions.get({ id }); return Subscription$inboundSchema(response); }; this.updateSubscription = async (id, params) => { const { error, data } = core.updateSubscriptionSchema.safeParse({ id, ...params }); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "updateSubscription" ); } if (!data.provider_metadata || Object.keys(data.provider_metadata).length === 0) { throw new core.ValidationError( "Polar requires specific update type via provider_metadata. Use one of: { productId: string } | { discountId: string } | { trialEnd: Date }", { provider: this.providerName, method: "updateSubscription" } ); } const response = await this.polar.subscriptions.update({ id, subscriptionUpdate: data.provider_metadata }); return Subscription$inboundSchema(response); }; this.deleteSubscription = async (id) => { const { error } = core.retrieveSubscriptionSchema.safeParse({ id }); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "deleteSubscription" ); } return await this.cancelSubscription(id) === null ? null : null; }; this.createPayment = async (params) => { const { error, data } = core.createPaymentSchema.safeParse(params); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "createPayment" ); } const paymentMetadata = core.stringifyMetadataValues( data.metadata ?? {} ); const checkoutCreateOptions = { ...data.provider_metadata && { ...data.provider_metadata }, amount: data.amount, metadata: paymentMetadata, products: data.item_id ? [data.item_id] : [] }; if (core.isIdCustomer(data.customer)) { checkoutCreateOptions.customerId = String(data.customer.id); } else if (core.isEmailCustomer(data.customer)) { checkoutCreateOptions.customerEmail = data.customer.email; } if (data.billing) { checkoutCreateOptions.customerBillingAddress = { line1: data.billing.address.line1, line2: data.billing.address.line2, postalCode: data.billing.address.postal_code, city: data.billing.address.city, country: data.billing.address.country, state: data.billing.address.state }; checkoutCreateOptions.metadata = { ...paymentMetadata, [core.PAYKIT_METADATA_KEY]: JSON.stringify({ _shipping_phone: data.billing.address.phone ?? "", _shipping_carrier: data.billing.carrier ?? "" }) }; } const checkoutResponse = await this.polar.checkouts.create( checkoutCreateOptions ); return Payment$inboundSchema(checkoutResponse); }; this.updatePayment = async (id, params) => { const { error, data } = core.updatePaymentSchema.safeParse(params); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "updatePayment" ); } const { provider_metadata, ...rest } = data; const paymentMetadata = core.stringifyMetadataValues( rest.metadata ?? {} ); const checkoutResponse = await this.polar.checkouts.update({ id, checkoutUpdate: { ...provider_metadata, ...rest.metadata && { metadata: paymentMetadata }, ...rest.item_id && { products: [rest.item_id] }, ...rest.amount && { amount: rest.amount }, ...rest.currency && { currency: rest.currency } } }); return Payment$inboundSchema(checkoutResponse); }; this.capturePayment = async (id, params) => { const { data: _, error } = core.capturePaymentSchema.safeParse(params); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "capturePayment" ); } return this.retrievePayment(id); }; this.cancelPayment = async (id) => { throw new core.ProviderNotSupportedError( "cancelPayment", this.providerName, { reason: "Polar does not support canceling payments" } ); }; this.deletePayment = async (id) => { throw new core.ProviderNotSupportedError( "deletePayment", this.providerName, { reason: "Polar does not support deleting payments" } ); }; this.retrievePayment = async (id) => { const response = await this.polar.checkouts.get({ id }); return Payment$inboundSchema(response); }; this.createRefund = async (params) => { const { error, data } = core.createRefundSchema.safeParse(params); if (error) { throw core.ValidationError.fromZodError( error, this.providerName, "createRefund" ); } const order = await this.polar.orders.get({ id: data.payment_id }); if (!order) { throw new core.ResourceNotFoundError( "Order", data.payment_id, this.providerName ); } const matched = core.refundReasonMatcher(data.reason ?? ""); const reasonMap = { duplicate: "duplicate", fraudulent: "fraudulent", requested_by_customer: "customer_request", customer_request: "customer_request" }; const reason = reasonMap[matched] ?? "other"; if (reason === "other") { console.warn( `[Polar Provider] Unmapped refund reason: "${data.reason}" -> defaulting to "other"` ); } const refund = await this.refunds.create({ orderId: order.id, reason, amount: data.amount, ...data.provider_metadata && { ...data.provider_metadata } }); if (!refund) { throw new core.OperationFailedError( "Failed to create refund", this.providerName ); } return Refund$inboundSchema(refund); }; this.handleWebhook = async (params, webhookSecret) => { if (!webhookSecret) { throw new core.WebhookError( "webhookSecret is required for Polar webhook verification", { provider: this.providerName } ); } const { body, headersAsObject } = params; const webhookId = headersAsObject["webhook-id"] || ""; const webhookTimestamp = headersAsObject["webhook-timestamp"] || "0"; const { data, type } = webhooks.validateEvent( body, headersAsObject, webhookSecret ); const results = []; results.push({ id: webhookId, type: `polar.${type}`, created: parseInt(webhookTimestamp), data, is_raw: true }); const processStandard = () => { switch (type) { case "order.paid": { const polarOrder = data; const isSubscription = [ "subscription_create", "subscription_cycle" ].includes(polarOrder.billingReason); return [ core.paykitEvent$InboundSchema({ type: "payment.succeeded", created: parseInt(webhookTimestamp), id: webhookId, data: { id: polarOrder.id, amount: polarOrder.totalAmount, currency: polarOrder.currency, customer: polarOrder.customerId ? { id: polarOrder.customerId } : polarOrder.customer?.email ? { email: polarOrder.customer.email } : null, status: "succeeded", metadata: core.stringifyMetadataValues( polarOrder.metadata ?? {} ), item_id: polarOrder.product.id, requires_action: false, payment_url: null } }), core.paykitEvent$InboundSchema({ type: "invoice.generated", created: parseInt(webhookTimestamp), id: webhookId, data: Invoice$inboundSchema({ ...polarOrder, billingMode: core.billingModeSchema.parse( isSubscription ? "recurring" : "one_time" ), metadata: { ...polarOrder.metadata ?? {} } }) }) ]; } case "order.created": { const polarOrder = data; return [ core.paykitEvent$InboundSchema({ type: "payment.created", created: parseInt(webhookTimestamp), id: webhookId, data: { id: polarOrder.id, amount: polarOrder.totalAmount, currency: polarOrder.currency, customer: polarOrder.customerId ? { id: polarOrder.customerId } : polarOrder.customer?.email ? { email: polarOrder.customer.email } : null, status: polarOrder.status === "paid" ? "succeeded" : "pending", metadata: core.stringifyMetadataValues( polarOrder.metadata ?? {} ), item_id: polarOrder.product.id, requires_action: polarOrder.status !== "paid", payment_url: null } }) ]; } case "customer.created": case "customer.updated": return [ core.paykitEvent$InboundSchema({ type: type === "customer.created" ? "customer.created" : "customer.updated", created: parseInt(webhookTimestamp), id: webhookId, data: Customer$inboundSchema(data) }) ]; case "customer.deleted": return [ core.paykitEvent$InboundSchema({ type: "customer.deleted", created: parseInt(webhookTimestamp), id: webhookId, data: null }) ]; case "subscription.created": case "subscription.updated": return [ core.paykitEvent$InboundSchema({ type: type === "subscription.created" ? "subscription.created" : "subscription.updated", created: parseInt(webhookTimestamp), id: webhookId, data: Subscription$inboundSchema( data ) }) ]; case "subscription.revoked": return [ core.paykitEvent$InboundSchema({ type: "subscription.canceled", created: parseInt(webhookTimestamp), id: webhookId, data: Subscription$inboundSchema( data ) }) ]; case "refund.created": return [ core.paykitEvent$InboundSchema({ type: "refund.created", created: parseInt(webhookTimestamp), id: webhookId, data: Refund$inboundSchema(data) }) ]; default: return null; } }; const standardMapped = processStandard(); if (standardMapped) results.push(...standardMapped); return results; }; const { accessToken, isSandbox, debug = true, ...rest } = config; const serverURL = isSandbox ? this.sandboxURL : this.productionURL; this.polar = new sdk.Polar({ accessToken, serverURL, ...rest }); this.refunds = new refunds_js.Refunds({ accessToken, serverURL, ...rest }); this.isSandbox = isSandbox; } get _native() { return this.polar; } }; // src/index.ts var createPolar = (config) => { return new PolarProvider(config); }; var polar = () => { const envVars = core.validateRequiredKeys( ["POLAR_ACCESS_TOKEN", "POLAR_SANDBOX"], process.env ?? {}, "Missing required environment variables: {keys}" ); return createPolar({ debug: true, accessToken: envVars.POLAR_ACCESS_TOKEN, isSandbox: envVars.POLAR_SANDBOX == "true" }); }; exports.createPolar = createPolar; exports.polar = polar;