@paykit-sdk/polar
Version:
Polar provider for PayKit
693 lines (687 loc) • 23.5 kB
JavaScript
'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;