384 lines
8.0 KiB
TypeScript
384 lines
8.0 KiB
TypeScript
import {
|
|
type AppEnv,
|
|
type Customer,
|
|
ErrCode,
|
|
type Organization,
|
|
ProcessorType,
|
|
} from "@autumn/shared";
|
|
import { StatusCodes } from "http-status-codes";
|
|
import type { Stripe } from "stripe";
|
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
|
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
|
import { CusService } from "@/internal/customers/CusService.js";
|
|
import RecaseError from "@/utils/errorUtils.js";
|
|
import type { TestContext } from "../../../tests/utils/testInitUtils/createTestContext";
|
|
|
|
export const getStripeCus = async ({
|
|
stripeCli,
|
|
stripeId,
|
|
}: {
|
|
stripeCli: Stripe;
|
|
stripeId: string;
|
|
}) => {
|
|
try {
|
|
const stripeCus = await stripeCli.customers.retrieve(stripeId);
|
|
return stripeCus as Stripe.Customer;
|
|
} catch (_error) {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
export const createStripeCusIfNotExists = async ({
|
|
db,
|
|
org,
|
|
env,
|
|
customer,
|
|
logger,
|
|
}: {
|
|
db: DrizzleCli;
|
|
org: Organization;
|
|
env: AppEnv;
|
|
customer: Customer;
|
|
logger: any;
|
|
}) => {
|
|
let createNew = false;
|
|
const stripeCli = createStripeCli({ org, env });
|
|
if (!customer.processor || !customer.processor.id) {
|
|
createNew = true;
|
|
} else {
|
|
try {
|
|
const stripeCus = await stripeCli.customers.retrieve(
|
|
customer.processor.id,
|
|
{
|
|
expand: ["test_clock", "invoice_settings.default_payment_method"],
|
|
},
|
|
);
|
|
if (!stripeCus.deleted) {
|
|
return stripeCus as Stripe.Customer;
|
|
} else {
|
|
createNew = true;
|
|
}
|
|
} catch (_error) {
|
|
createNew = true;
|
|
}
|
|
}
|
|
|
|
if (createNew) {
|
|
logger.info(`Creating new stripe customer for ${customer.id}`);
|
|
const stripeCustomer = await createStripeCustomer({
|
|
org,
|
|
env,
|
|
customer,
|
|
});
|
|
|
|
await CusService.update({
|
|
db,
|
|
idOrInternalId: customer.internal_id,
|
|
orgId: org.id,
|
|
env,
|
|
update: {
|
|
processor: {
|
|
id: stripeCustomer.id,
|
|
type: ProcessorType.Stripe,
|
|
},
|
|
},
|
|
});
|
|
|
|
customer.processor = {
|
|
id: stripeCustomer.id,
|
|
type: ProcessorType.Stripe,
|
|
};
|
|
|
|
return stripeCustomer;
|
|
}
|
|
};
|
|
|
|
export const createStripeCustomer = async ({
|
|
org,
|
|
env,
|
|
customer,
|
|
testClockId,
|
|
metadata,
|
|
}: {
|
|
org: Organization;
|
|
env: AppEnv;
|
|
customer: Customer;
|
|
testClockId?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}) => {
|
|
const stripeCli = createStripeCli({ org, env });
|
|
|
|
try {
|
|
const stripeCustomer = await stripeCli.customers.create({
|
|
name: customer.name || undefined,
|
|
email: customer.email || undefined,
|
|
metadata: {
|
|
...(metadata || {}),
|
|
autumn_id: customer.id || null,
|
|
autumn_internal_id: customer.internal_id,
|
|
},
|
|
test_clock: testClockId,
|
|
});
|
|
|
|
return stripeCustomer;
|
|
} catch (error: any) {
|
|
throw new RecaseError({
|
|
message: `Error creating customer in Stripe. ${error.message}`,
|
|
code: ErrCode.StripeCreateCustomerFailed,
|
|
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const deleteStripeCustomer = async ({
|
|
org,
|
|
env,
|
|
stripeId,
|
|
}: {
|
|
org: Organization;
|
|
env: AppEnv;
|
|
stripeId: string;
|
|
}) => {
|
|
const stripeCli = createStripeCli({ org, env });
|
|
|
|
const stripeCustomer = await stripeCli.customers.del(stripeId);
|
|
|
|
return stripeCustomer;
|
|
};
|
|
|
|
export const listCusPaymentMethods = async ({
|
|
stripeCli,
|
|
stripeId,
|
|
}: {
|
|
stripeCli: Stripe;
|
|
stripeId: string;
|
|
}) => {
|
|
const res = await stripeCli.paymentMethods.list({
|
|
customer: stripeId,
|
|
});
|
|
|
|
const paymentMethods = res.data;
|
|
paymentMethods.sort((a, b) => b.created - a.created);
|
|
|
|
return paymentMethods;
|
|
};
|
|
|
|
export const getCusPaymentMethod = async ({
|
|
stripeCli,
|
|
stripeId,
|
|
errorIfNone = false,
|
|
typeFilter,
|
|
}: {
|
|
stripeCli: Stripe;
|
|
stripeId?: string;
|
|
errorIfNone?: boolean;
|
|
typeFilter?: string;
|
|
}) => {
|
|
if (!stripeId) {
|
|
return null;
|
|
}
|
|
|
|
const stripeCustomer = (await stripeCli.customers.retrieve(
|
|
stripeId,
|
|
)) as Stripe.Customer;
|
|
|
|
const paymentMethodId =
|
|
stripeCustomer.invoice_settings?.default_payment_method;
|
|
|
|
if (!paymentMethodId) {
|
|
const res = await stripeCli.paymentMethods.list({
|
|
customer: stripeId,
|
|
});
|
|
|
|
let paymentMethods = res.data;
|
|
paymentMethods.sort((a, b) => b.created - a.created);
|
|
if (typeFilter) {
|
|
paymentMethods = paymentMethods.filter((pm) => pm.type === typeFilter);
|
|
}
|
|
|
|
if (paymentMethods.length === 0) {
|
|
if (errorIfNone) {
|
|
throw new RecaseError({
|
|
code: ErrCode.StripeGetPaymentMethodFailed,
|
|
message: `No payment method found for customer ${stripeId}`,
|
|
statusCode: 500,
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return paymentMethods[0];
|
|
} else {
|
|
const paymentMethod = await stripeCli.paymentMethods.retrieve(
|
|
paymentMethodId as string,
|
|
);
|
|
return paymentMethod;
|
|
}
|
|
};
|
|
|
|
// 2. Create a payment method and attach to customer
|
|
export const attachPmToCus = async ({
|
|
db,
|
|
customer,
|
|
org,
|
|
env,
|
|
willFail = false,
|
|
testClockId,
|
|
}: {
|
|
db: DrizzleCli;
|
|
customer: Customer;
|
|
org: Organization;
|
|
env: AppEnv;
|
|
willFail?: boolean;
|
|
testClockId?: string;
|
|
}) => {
|
|
// 1. Create stripe customer if not exists
|
|
|
|
let stripeCusId = customer.processor?.id;
|
|
if (!stripeCusId) {
|
|
const stripeCustomer = await createStripeCustomer({
|
|
org,
|
|
env,
|
|
customer,
|
|
testClockId,
|
|
});
|
|
|
|
await CusService.update({
|
|
db,
|
|
idOrInternalId: customer.internal_id,
|
|
orgId: org.id,
|
|
env,
|
|
update: {
|
|
processor: {
|
|
id: stripeCustomer.id,
|
|
type: ProcessorType.Stripe,
|
|
},
|
|
},
|
|
});
|
|
|
|
stripeCusId = stripeCustomer.id;
|
|
customer.processor = {
|
|
id: stripeCustomer.id,
|
|
type: "stripe",
|
|
};
|
|
}
|
|
|
|
const stripeCli = createStripeCli({ org, env });
|
|
|
|
try {
|
|
const token = willFail ? "tok_chargeCustomerFail" : "tok_visa";
|
|
const pm = await stripeCli.paymentMethods.create({
|
|
type: "card",
|
|
card: {
|
|
token,
|
|
},
|
|
});
|
|
await stripeCli.paymentMethods.attach(pm.id, {
|
|
customer: stripeCusId,
|
|
});
|
|
|
|
await stripeCli.customers.update(stripeCusId, {
|
|
invoice_settings: {
|
|
default_payment_method: pm.id,
|
|
},
|
|
});
|
|
// console.log(" - Payment method attached");
|
|
} catch (error) {
|
|
console.log(" - Error attaching payment method", error);
|
|
}
|
|
};
|
|
|
|
export const attachFailedPaymentMethod = async ({
|
|
stripeCli,
|
|
customer,
|
|
}: {
|
|
stripeCli: Stripe;
|
|
customer: Customer;
|
|
}) => {
|
|
// Delete existing payment method
|
|
const paymentMethods = await stripeCli.paymentMethods.list({
|
|
customer: customer.processor?.id,
|
|
});
|
|
for (const pm of paymentMethods.data) {
|
|
await stripeCli.paymentMethods.detach(pm.id);
|
|
}
|
|
|
|
const pm = await stripeCli.paymentMethods.create({
|
|
type: "card",
|
|
card: {
|
|
token: "tok_chargeCustomerFail",
|
|
},
|
|
});
|
|
await stripeCli.paymentMethods.attach(pm.id, {
|
|
customer: customer.processor?.id,
|
|
});
|
|
};
|
|
|
|
export const attachAuthenticatePaymentMethod = async ({
|
|
ctx,
|
|
customerId,
|
|
}: {
|
|
ctx: TestContext;
|
|
customerId: string;
|
|
}) => {
|
|
const { org, env, db } = ctx;
|
|
const stripeCli = createStripeCli({ org, env });
|
|
const autumnCustomer = await CusService.get({
|
|
db,
|
|
idOrInternalId: customerId,
|
|
orgId: org.id,
|
|
env: env,
|
|
});
|
|
|
|
const stripeCustomer = await stripeCli.customers.retrieve(
|
|
autumnCustomer!.processor?.id,
|
|
);
|
|
// Delete existing payment method
|
|
const paymentMethods = await stripeCli.paymentMethods.list({
|
|
customer: stripeCustomer.id,
|
|
});
|
|
for (const pm of paymentMethods.data) {
|
|
await stripeCli.paymentMethods.detach(pm.id);
|
|
}
|
|
|
|
await stripeCli.paymentMethods.attach("pm_card_authenticationRequired", {
|
|
customer: stripeCustomer.id,
|
|
});
|
|
};
|
|
|
|
export const deleteAllStripeCustomers = async ({
|
|
org,
|
|
env,
|
|
}: {
|
|
org: Organization;
|
|
env: AppEnv;
|
|
}) => {
|
|
const stripeCli = createStripeCli({ org, env });
|
|
|
|
const stripeCustomers = await stripeCli.customers.list({
|
|
limit: 100,
|
|
});
|
|
|
|
if (stripeCustomers.data.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const firstCustomer = stripeCustomers.data[0];
|
|
if (firstCustomer.livemode) {
|
|
throw new RecaseError({
|
|
message: "Cannot delete livemode customers",
|
|
code: ErrCode.StripeDeleteCustomerFailed,
|
|
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
|
});
|
|
}
|
|
|
|
const batchSize = 10;
|
|
for (let i = 0; i < stripeCustomers.data.length; i += batchSize) {
|
|
const batch = stripeCustomers.data.slice(i, i + batchSize);
|
|
await Promise.all(batch.map((c) => stripeCli.customers.del(c.id)));
|
|
console.log(
|
|
`Deleted ${i + batch.length}/${stripeCustomers.data.length} customers`,
|
|
);
|
|
}
|
|
};
|