Is there any way to copy the products and pricing table to a sandbox environment?
Hi there, thanks for your question! Sandboxes does not support copying data other than settings from live to your sandbox. We’ve heard a similar feature request and will consider supporting this in our future roadmap!
We’ve just checked out the sandbox functionality - it’d be very beneficial to have an option to copy all products & pricing when creating a sandbox, or possibly adhoc after creation by product/price like the copy to live functionality - e.g. Copy to Sandbox
Seriously, what a hassle to have to duplicate all our products just to do testing in a new sandbox.
Yeah, this was a surprise to me. I just wrote a command for my ops CLI that syncs products and prices from the main account to a sandbox. Here’s the relevant source file (you’ll just have to adjust where you get sandboxSecretKey
and prodSecretKey
from, as well as the method for matching products (I’m using a metadata key quality
) and prices (I’m just using the recurrence interval).
Hopefully this is helpful for others trying to do the same thing.
import { isString } from "@shared/validator";
import inquirer from "inquirer";
import { Stripe } from "stripe";
import { getParameterValue } from "../aws/parameter";
function getPriceUpdatableDataForComparison(price: Stripe.Price) {
return {
lookup_key: price.lookup_key ?? undefined,
metadata: price.metadata,
nickname: price.nickname ?? undefined,
tax_behavior: price.tax_behavior ?? undefined,
};
}
function getPriceDataForComparison(price: Stripe.Price) {
return {
currency: price.currency,
active: price.active,
billing_scheme: price.billing_scheme,
//TODO: currency_options: ???,
custom_unit_amount: price.custom_unit_amount
? {
enabled: true,
minimum: price.custom_unit_amount.minimum ?? undefined,
maximum: price.custom_unit_amount.maximum ?? undefined,
preset: price.custom_unit_amount.preset ?? undefined,
}
: undefined,
recurring: price.recurring
? {
aggregate_usage: price.recurring.aggregate_usage ?? undefined,
interval: price.recurring.interval,
interval_count: price.recurring.interval_count,
meter: price.recurring.meter ?? undefined,
trial_period_days: price.recurring.trial_period_days ?? undefined,
usage_type: price.recurring.usage_type ?? undefined,
}
: undefined,
tiers:
price.tiers?.map(
(tier) =>
({
flat_amount_decimal: tier.flat_amount_decimal ?? undefined,
unit_amount_decimal: tier.unit_amount_decimal ?? undefined,
up_to: tier.up_to ?? "inf",
} as const)
) ?? undefined,
tiers_mode: price.tiers_mode ?? undefined,
transfer_lookup_key: true,
transform_quantity: price.transform_quantity ?? undefined,
unit_amount_decimal: price.unit_amount_decimal ?? undefined,
} as const;
}
function getProductDataForComparison(product: Stripe.Product) {
return {
name: product.name,
active: product.active,
description: product.description ?? undefined,
images: product.images,
metadata: product.metadata,
package_dimensions: product.package_dimensions ?? undefined,
shippable: product.shippable ?? undefined,
statement_descriptor: product.statement_descriptor ?? undefined,
tax_code: isString(product.tax_code) ? product.tax_code : undefined,
type: product.type,
unit_label: product.unit_label ?? undefined,
url: product.url ?? undefined,
} as const;
}
export async function setupStripeSandbox(sandboxSecretKey: string | undefined) {
const prodSecretKey = await getParameterValue("/stripe/prod/secret");
if (!prodSecretKey.startsWith("sk_live")) {
throw new Error("This is not a production key");
}
if (!sandboxSecretKey) {
({ sandboxSecretKey } = await inquirer.prompt({
type: "input",
name: "sandboxSecretKey",
message: "Enter sandbox Stripe key",
}));
}
if (!sandboxSecretKey?.startsWith("sk_test")) {
throw new Error("This is not a sandbox key");
}
const prodStripe = new Stripe(prodSecretKey);
const sandboxStripe = new Stripe(sandboxSecretKey);
const prodProducts = (
await prodStripe.products.list({
active: true,
})
).data;
const prodPrices = (
await prodStripe.prices.list({
active: true,
expand: ["data.tiers"],
})
).data;
console.log(
`Found ${prodProducts.length} products and ${prodPrices.length} prices in prod`
);
const sandboxProducts = (
await sandboxStripe.products.list({
active: true,
})
).data;
const sandboxPrices = (
await sandboxStripe.prices.list({
active: true,
expand: ["data.tiers"],
})
).data;
console.log(
`Found ${sandboxProducts.length} products and ${sandboxPrices.length} prices in sandbox`
);
const sandboxProductIdsUsed = new Set<string>();
const sandboxPriceIdsUsed = new Set<string>();
for (const prodProduct of prodProducts) {
const prodProductData = getProductDataForComparison(prodProduct);
let sandboxProduct = sandboxProducts.find(
(p) => p.metadata["quality"] === prodProduct.metadata["quality"]
);
if (sandboxProduct) {
if (
JSON.stringify(getProductDataForComparison(sandboxProduct)) ===
JSON.stringify(prodProductData)
) {
console.log(`Product ${prodProduct.name} in sandbox is up to date`);
} else {
// Update sandbox product to match all fields on prod product
console.log(`Updating product ${prodProduct.name} in sandbox`);
sandboxProduct = await sandboxStripe.products.update(
sandboxProduct.id,
prodProductData
);
}
} else {
// Create sandbox product
console.log(`Creating product ${prodProduct.name} in sandbox`);
sandboxProduct = await sandboxStripe.products.create(prodProductData);
}
sandboxProductIdsUsed.add(sandboxProduct.id);
for (const prodPrice of prodPrices.filter(
(p) => p.product === prodProduct.id
)) {
let prodPriceData = getPriceDataForComparison(prodPrice);
let prodUpdatableData = getPriceUpdatableDataForComparison(prodPrice);
let sandboxPrice = sandboxPrices.find(
(p) =>
p.product === sandboxProduct.id &&
p.recurring?.interval === prodPrice.recurring?.interval &&
p.recurring?.interval_count === prodPrice.recurring?.interval_count
);
if (
sandboxPrice &&
JSON.stringify(getPriceDataForComparison(sandboxPrice)) ===
JSON.stringify(prodPriceData)
) {
if (
JSON.stringify(getPriceUpdatableDataForComparison(sandboxPrice)) ===
JSON.stringify(prodUpdatableData)
) {
console.log(`Price ${prodPrice.id} in sandbox is up to date`);
} else {
console.log(`Updating price ${prodPrice.id} in sandbox`);
sandboxPrice = await sandboxStripe.prices.update(sandboxPrice.id, {
...prodUpdatableData,
});
}
} else {
console.log(`Creating/replacing price ${prodPrice.id} in sandbox`);
sandboxPrice = await sandboxStripe.prices.create({
product: sandboxProduct.id,
...prodPriceData,
...prodUpdatableData,
});
}
sandboxPriceIdsUsed.add(sandboxPrice.id);
}
}
for (const sandboxProduct of sandboxProducts) {
if (!sandboxProductIdsUsed.has(sandboxProduct.id)) {
console.log(`Deleting product ${sandboxProduct.id} from sandbox`);
await sandboxStripe.products.del(sandboxProduct.id);
}
}
for (const sandboxPrice of sandboxPrices) {
if (!sandboxPriceIdsUsed.has(sandboxPrice.id)) {
console.log(`Deleting price ${sandboxPrice.id} from sandbox`);
await sandboxStripe.prices.update(sandboxPrice.id, {
active: false,
});
}
}
}
I replied here with a script I wrote to sync products and pricing from our main account to a sandbox, but the spam filter removed it, so good luck I guess.