Compare commits

..

1 Commits

Author SHA1 Message Date
Matti Nannt
046c56b9dd fix: sync legacy stripe payment methods 2026-03-16 17:02:01 +01:00
5 changed files with 88 additions and 8 deletions

View File

@@ -33,14 +33,14 @@ describe("Password Management", () => {
const hashedPassword = await hashPassword(password);
const isValid = await verifyPassword(password, hashedPassword);
expect(isValid).toBe(true);
});
}, 15000);
test("verifyPassword should reject an incorrect password", async () => {
const password = "testPassword123";
const hashedPassword = await hashPassword(password);
const isValid = await verifyPassword("wrongPassword", hashedPassword);
expect(isValid).toBe(false);
});
}, 15000);
});
describe("Organization Access", () => {

View File

@@ -34,7 +34,7 @@ describe("Crypto Utils", () => {
const isValid = await verifySecret(secret, hash);
expect(isValid).toBe(true);
});
}, 15000);
test("should reject wrong secrets", async () => {
const secret = "test-secret-123";
@@ -43,7 +43,7 @@ describe("Crypto Utils", () => {
const isValid = await verifySecret(wrongSecret, hash);
expect(isValid).toBe(false);
});
}, 15000);
test("should generate different hashes for the same secret (due to salt)", async () => {
const secret = "test-secret-123";
@@ -64,7 +64,7 @@ describe("Crypto Utils", () => {
// Verify the cost factor is in the hash
expect(hash).toMatch(/^\$2[aby]\$10\$/);
expect(await verifySecret(secret, hash)).toBe(true);
});
}, 15000);
test("should return false for invalid hash format", async () => {
const secret = "test-secret-123";

View File

@@ -1021,6 +1021,6 @@ describe("updateSurveyDraftAction", () => {
// Expect validation error (skipValidation = false)
await expect(updateSurveyInternal(incompleteSurvey, false)).rejects.toThrow();
});
}, 15000);
});
});

View File

@@ -159,6 +159,12 @@ describe("organization-billing", () => {
mocks.getCloudPlanFromProduct.mockReturnValue("pro");
mocks.subscriptionsList.mockResolvedValue({ data: [] });
mocks.customersList.mockResolvedValue({ data: [] });
mocks.customersRetrieve.mockResolvedValue({
id: "cus_1",
deleted: false,
invoice_settings: { default_payment_method: null },
default_source: null,
});
mocks.prismaMembershipFindFirst.mockResolvedValue(null);
mocks.productsList.mockResolvedValue({
data: [
@@ -639,6 +645,64 @@ describe("organization-billing", () => {
expect(mocks.cacheDel).toHaveBeenCalledWith(["billing-cache-key"]);
});
test("syncOrganizationBillingFromStripe marks migrated customers with customer-level payment methods", async () => {
mocks.prismaOrganizationBillingFindUnique.mockResolvedValue({
stripeCustomerId: "cus_1",
limits: {
projects: 3,
monthly: {
responses: 1500,
},
},
usageCycleAnchor: new Date(),
stripe: { lastSyncedEventId: null },
});
mocks.subscriptionsList.mockResolvedValue({
data: [
{
id: "sub_1",
status: "active",
default_payment_method: null,
billing_cycle_anchor: 1739923200,
items: {
data: [
{
price: {
metadata: {},
product: { id: "prod_pro", metadata: { formbricks_plan: "pro" } },
recurring: { usage_type: "licensed", interval: "month" },
},
},
],
},
},
],
});
mocks.customersRetrieve.mockResolvedValue({
id: "cus_1",
deleted: false,
invoice_settings: { default_payment_method: "pm_legacy_default" },
default_source: null,
});
mocks.entitlementsList.mockResolvedValue({
data: [],
has_more: false,
});
const result = await syncOrganizationBillingFromStripe("org_1");
expect(result?.stripe?.hasPaymentMethod).toBe(true);
expect(mocks.prismaOrganizationBillingUpdate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
stripe: expect.objectContaining({
hasPaymentMethod: true,
}),
}),
})
);
});
test("createPaidPlanCheckoutSession rejects mixed-interval yearly checkout", async () => {
await expect(
createPaidPlanCheckoutSession({

View File

@@ -1107,6 +1107,21 @@ const resolvePendingPlanChange = async (subscription: Stripe.Subscription | null
return null;
};
const resolveHasPaymentMethod = (
subscription: Stripe.Subscription | null,
customer: Stripe.Customer | Stripe.DeletedCustomer
) => {
if (subscription?.default_payment_method != null) {
return true;
}
if (customer.deleted) {
return false;
}
return customer.invoice_settings.default_payment_method != null || customer.default_source != null;
};
export const syncOrganizationBillingFromStripe = async (
organizationId: string,
event?: { id: string; created: number }
@@ -1132,9 +1147,10 @@ export const syncOrganizationBillingFromStripe = async (
return billing;
}
const [subscription, featureLookupKeys] = await Promise.all([
const [subscription, featureLookupKeys, customer] = await Promise.all([
resolveCurrentSubscription(customerId),
listAllActiveEntitlements(customerId),
stripeClient.customers.retrieve(customerId),
]);
const cloudPlan = resolveCloudPlanFromSubscription(subscription);
@@ -1160,7 +1176,7 @@ export const syncOrganizationBillingFromStripe = async (
interval: billingInterval,
subscriptionStatus,
subscriptionId: subscription?.id ?? null,
hasPaymentMethod: subscription?.default_payment_method != null,
hasPaymentMethod: resolveHasPaymentMethod(subscription, customer),
features: featureLookupKeys,
pendingChange,
lastStripeEventCreatedAt: toIsoStringOrNull(incomingEventDate ?? previousEventDate),