Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/apps/test-init-app
docs/superpowers
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
Expand Down
9 changes: 0 additions & 9 deletions e2e/cli/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { sql } from "drizzle-orm";
import { Pool } from "pg";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

import { getStripeAccountInfo } from "../../packages/paykit/src/cli/utils/format";
import { getPayKitConfig } from "../../packages/paykit/src/cli/utils/get-config";
import { createContext } from "../../packages/paykit/src/core/context";
import {
Expand Down Expand Up @@ -61,14 +60,6 @@ describe("paykitjs status", () => {
}
});

it("should connect to Stripe and retrieve account info", async () => {
const secretKey = process.env.STRIPE_SECRET_KEY!;
const info = await getStripeAccountInfo(secretKey);

expect(info.displayName).toBeTruthy();
expect(info.mode).toBe("test mode");
});

it("should report schema up to date after migration", async () => {
const config = await getPayKitConfig({ cwd: fixture.cwd });
const database = resolveDatabase(config.options.database);
Expand Down
23 changes: 7 additions & 16 deletions e2e/smoke/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,12 @@ export const extraMessagesPlan = plan({
price: { amount: 5, interval: "month" },
});

type SmokePlans = {
free: typeof freePlan;
pro: typeof proPlan;
premium: typeof premiumPlan;
ultra: typeof ultraPlan;
extra_messages: typeof extraMessagesPlan;
};
const smokePlans = [freePlan, proPlan, premiumPlan, ultraPlan, extraMessagesPlan] as const;

type SmokePayKit = ReturnType<
typeof createPayKit<{
database: Pool;
plans: SmokePlans;
plans: typeof smokePlans;
provider: ReturnType<typeof stripe>;
testing: { enabled: true };
}>
Expand Down Expand Up @@ -144,13 +138,7 @@ export async function createTestPayKit(): Promise<TestPayKit> {
const stripeProvider = stripe({ secretKey, webhookSecret });
const paykit = createPayKit({
database: pool,
plans: {
free: freePlan,
pro: proPlan,
premium: premiumPlan,
ultra: ultraPlan,
extra_messages: extraMessagesPlan,
},
plans: smokePlans,
provider: stripeProvider,
testing: { enabled: true },
});
Expand All @@ -160,7 +148,10 @@ export async function createTestPayKit(): Promise<TestPayKit> {
// Override createSubscription to use allow_incomplete. The default
// payment_behavior: "default_incomplete" requires client-side payment
// confirmation which isn't possible in automated tests.
ctx.stripe.createSubscription = async (data) => {
(ctx.provider as unknown as Record<string, unknown>).createSubscription = async (data: {
providerCustomerId: string;
providerPriceId: string;
}) => {
const sub = await stripeClient.subscriptions.create({
customer: data.providerCustomerId,
items: [{ price: data.providerPriceId }],
Expand Down
1 change: 0 additions & 1 deletion packages/paykit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"posthog-node": "^5.28.8",
"stripe": "^19.1.0",
"typescript": "^5.9.2",
"zod": "^4.0.0"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/paykit/src/api/define-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as z from "zod";

import type { PayKitContext } from "../core/context";
import { PayKitError, PAYKIT_ERROR_CODES } from "../core/errors";
import { getCustomerByIdOrThrow, syncCustomerWithDefaults } from "../customer/customer.service";
import { getCustomerByIdOrThrow, upsertCustomer } from "../customer/customer.service";
import type { Customer } from "../types/models";

const paykitMiddleware = createMiddleware(async () => {
Expand Down Expand Up @@ -437,7 +437,7 @@ async function resolveCustomer(
throw PayKitError.from("FORBIDDEN", PAYKIT_ERROR_CODES.CUSTOMER_ID_MISMATCH);
}

return syncCustomerWithDefaults(ctx, {
return upsertCustomer(ctx, {
id: identity.customerId,
email: identity.email,
name: identity.name,
Expand Down
14 changes: 7 additions & 7 deletions packages/paykit/src/cli/commands/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Command } from "commander";
import picocolors from "picocolors";

import {
checkStripe,
checkProvider,
createPool,
formatProductDiffs,
loadCliDeps,
Expand Down Expand Up @@ -37,14 +37,14 @@ async function pushAction(options: { config?: string; cwd: string; yes?: boolean
}

const connStr = deps.getConnectionString(database as never);
const [stripeResult, pendingMigrations] = await Promise.all([
checkStripe(deps, config.options.provider.secretKey),
const [providerResult, pendingMigrations] = await Promise.all([
checkProvider(config.options.provider),
deps.getPendingMigrationCount(database),
]);

if (!stripeResult.account.ok) {
if (!providerResult.account.ok) {
s.stop("");
p.log.error(`Stripe\n ${picocolors.red("✖")} ${stripeResult.account.message}`);
p.log.error(`Provider\n ${picocolors.red("✖")} ${providerResult.account.message}`);
p.cancel("Push failed");
process.exit(1);
}
Expand All @@ -71,7 +71,7 @@ async function pushAction(options: { config?: string; cwd: string; yes?: boolean
p.log.info(`Database\n ${picocolors.green("✔")} ${connStr}\n ${migrationStatus}`);

p.log.info(
`Stripe\n ${picocolors.green("✔")} ${stripeResult.account.displayName} (${stripeResult.account.mode})`,
`Provider\n ${picocolors.green("✔")} ${providerResult.account.displayName} (${providerResult.account.mode})`,
);

if (diffs.length > 0) {
Expand All @@ -95,7 +95,7 @@ async function pushAction(options: { config?: string; cwd: string; yes?: boolean
const changeCount = diffs.filter((d) => d.action !== "unchanged").length;
if (!options.yes) {
const shouldContinue = await p.confirm({
message: `Push ${String(changeCount)} product change${changeCount === 1 ? "" : "s"} to Stripe?`,
message: `Push ${String(changeCount)} product change${changeCount === 1 ? "" : "s"}?`,
});
if (p.isCancel(shouldContinue) || !shouldContinue) {
p.cancel("Aborted");
Expand Down
33 changes: 17 additions & 16 deletions packages/paykit/src/cli/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import picocolors from "picocolors";

import {
checkDatabase,
checkStripe,
checkProvider,
createPool,
formatProductDiffs,
loadCliDeps,
Expand Down Expand Up @@ -58,13 +58,13 @@ async function statusAction(options: {
process.exit(1);
}

// Database + Stripe in parallel
// Database + Provider in parallel
const database = createPool(deps, config.options.database);
const connStr = deps.getConnectionString(database as never);

const [dbResult, stripeResult] = await Promise.all([
const [dbResult, providerResult] = await Promise.all([
checkDatabase(database, deps),
checkStripe(deps, config.options.provider.secretKey),
checkProvider(config.options.provider),
]);

if (!dbResult.ok) {
Expand All @@ -75,26 +75,27 @@ async function statusAction(options: {
process.exit(1);
}

if (!stripeResult.account.ok) {
if (!providerResult.account.ok) {
s.stop("");
p.log.error(`Stripe\n ${picocolors.red("✖")} ${stripeResult.account.message}`);
p.outro("Fix Stripe issues before continuing");
p.log.error(`Provider\n ${picocolors.red("✖")} ${providerResult.account.message}`);
p.outro("Fix provider issues before continuing");
await database.end();
process.exit(1);
}

const pendingMigrations = dbResult.pendingMigrations;

let webhookStatus: string;
if (stripeResult.webhooks === null) {
if (providerResult.webhookEndpoints === null) {
webhookStatus = `${picocolors.dim("?")} Could not check webhook status`;
} else if (stripeResult.webhooks.length > 0) {
const lines = stripeResult.webhooks.map((ep) =>
picocolors.dim(`· Webhook endpoint registered (${ep.url})`),
);
} else if (providerResult.webhookEndpoints.length > 0) {
const lines = providerResult.webhookEndpoints.map((ep) => {
const label = ep.status === "enabled" ? "registered" : `status: ${ep.status}`;
return picocolors.dim(`· Webhook endpoint ${label} (${ep.url})`);
});
webhookStatus = lines.join("\n ");
} else {
webhookStatus = picocolors.dim("· No webhook endpoint (use Stripe CLI for local testing)");
webhookStatus = picocolors.dim("· No webhook endpoint (use provider CLI for local testing)");
}

// Products
Expand Down Expand Up @@ -135,14 +136,14 @@ async function statusAction(options: {
`Config\n` +
` ${picocolors.green("✔")} ${picocolors.dim(config.path)}\n` +
` ${picocolors.green("✔")} ${String(planCount)} plan${planCount === 1 ? "" : "s"} defined\n` +
` ${picocolors.green("✔")} Stripe provider configured`,
` ${picocolors.green("✔")} Provider configured`,
);

p.log.info(`Database\n ${picocolors.green("✔")} ${connStr}\n ${migrationStatus}`);

p.log.info(
`Stripe\n` +
` ${picocolors.green("✔")} ${stripeResult.account.displayName} (${stripeResult.account.mode})\n` +
`Provider\n` +
` ${picocolors.green("✔")} ${providerResult.account.displayName} (${providerResult.account.mode})\n` +
` ${webhookStatus}`,
);

Expand Down
25 changes: 0 additions & 25 deletions packages/paykit/src/cli/utils/format.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,4 @@
import picocolors from "picocolors";
import StripeSdk from "stripe";

export interface StripeAccountInfo {
displayName: string;
mode: "test mode" | "live mode";
}

export async function getStripeAccountInfo(secretKey: string): Promise<StripeAccountInfo> {
const mode = stripeMode(secretKey);
try {
const client = new StripeSdk(secretKey);
const account = await client.accounts.retrieve();
const name =
account.settings?.dashboard?.display_name || account.business_profile?.name || account.id;
return { displayName: name, mode };
} catch {
return { displayName: "unknown", mode };
}
}

export function maskConnectionString(url: string): string {
try {
Expand Down Expand Up @@ -46,12 +27,6 @@ export function formatPrice(amountCents: number, interval: string | null): strin
return `${formatted}/${interval}`;
}

export function stripeMode(secretKey: string): "test mode" | "live mode" {
return secretKey.startsWith("sk_test_") || secretKey.startsWith("rk_test_")
? "test mode"
: "live mode";
}

export function getConnectionString(pool: {
options?: {
connectionString?: string;
Expand Down
Loading