mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
fix(billing): let a catalog price decide the product, and check the session lookup key
CodeRabbit follow-up. Stripe updates a subscription's price and its metadata independently, so when the plan sits on a catalog price (ever_<any>_...) that price now decides, and ever_product metadata may only agree with it. Metadata alone decides only when the plan has no catalog lookup key. A Checkout Session's metadata.ever_lookup_key, when present, must be one of this product's ever_<product>_cloud_ prices. A read-only live check found every ever.co session stamps a consistent ever_lookup_key, and all 63 Gauzy subscriptions sit on ever_gauzy_cloud_ prices, so no current buyer is affected. 7 suites, 161 tests pass locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6488024cfb
commit
a8c02253b9
@@ -137,6 +137,11 @@ export interface ProductScopedCheckoutSession {
|
||||
*
|
||||
* A self-hosted license of the same product is refused on either signal: `ever_hosting` other than
|
||||
* `cloud`, or a plan price of this product that is not an `ever_<product>_cloud_` price.
|
||||
*
|
||||
* When the plan sits on a catalog price (`ever_<any>_...`), the price decides and the metadata may
|
||||
* only agree with it. Stripe updates a subscription's price and its metadata independently, so a
|
||||
* Teams price under `ever_product: 'gauzy'` metadata (or the reverse) is not this product's plan.
|
||||
* The metadata alone decides only when the plan has no catalog lookup key.
|
||||
*/
|
||||
export function subscriptionIsForProduct(
|
||||
subscription: ProductScopedSubscription | null | undefined,
|
||||
@@ -145,36 +150,40 @@ export function subscriptionIsForProduct(
|
||||
if (!subscription || !product) return false;
|
||||
if (!hostingIsCloud(subscription.metadata)) return false;
|
||||
const lookupKey = subscription.items?.data?.[0]?.price?.lookup_key;
|
||||
const cloudPrefix = cloudLookupKeyPrefix(product);
|
||||
if (
|
||||
typeof lookupKey === 'string' &&
|
||||
lookupKey.startsWith(lookupKeyPrefix(product)) &&
|
||||
!lookupKey.startsWith(cloudPrefix)
|
||||
) {
|
||||
return false;
|
||||
const metadataProduct = subscription.metadata?.ever_product;
|
||||
if (typeof lookupKey === 'string' && CATALOG_LOOKUP_KEY.test(lookupKey)) {
|
||||
return lookupKey.startsWith(cloudLookupKeyPrefix(product)) && (!metadataProduct || metadataProduct === product);
|
||||
}
|
||||
if (subscription.metadata?.ever_product === product) return true;
|
||||
return typeof lookupKey === 'string' && lookupKey.startsWith(cloudPrefix);
|
||||
return metadataProduct === product;
|
||||
}
|
||||
|
||||
/** Any catalog lookup key: `ever_<product>_<hosting>_...`. */
|
||||
const CATALOG_LOOKUP_KEY = /^ever_[a-z0-9]+_/;
|
||||
|
||||
/**
|
||||
* Whether a completed Checkout Session is a purchase of `product`'s HOSTED plan that can establish a
|
||||
* tenant link.
|
||||
*
|
||||
* All three are required. `mode === 'subscription'` excludes payment-mode sessions (lifetime
|
||||
* licenses, Ever Works credit packs) and setup-mode card saves, none of which buys a hosted plan;
|
||||
* the hosting check excludes self-hosted license subscriptions (`ever_hosting: 'selfhosted'`).
|
||||
* All are required. `mode === 'subscription'` excludes payment-mode sessions (lifetime licenses,
|
||||
* Ever Works credit packs) and setup-mode card saves, none of which buys a hosted plan; the hosting
|
||||
* check excludes self-hosted license subscriptions (`ever_hosting: 'selfhosted'`); and a
|
||||
* `metadata.ever_lookup_key`, which the shared checkout stamps on every session, must be one of this
|
||||
* product's cloud prices when it is present.
|
||||
*/
|
||||
export function checkoutSessionIsForProduct(
|
||||
session: ProductScopedCheckoutSession | null | undefined,
|
||||
product: string | null | undefined
|
||||
): boolean {
|
||||
if (!session || !product) return false;
|
||||
return (
|
||||
session.metadata?.ever_product === product &&
|
||||
session.mode === 'subscription' &&
|
||||
hostingIsCloud(session.metadata)
|
||||
);
|
||||
if (
|
||||
session.metadata?.ever_product !== product ||
|
||||
session.mode !== 'subscription' ||
|
||||
!hostingIsCloud(session.metadata)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const lookupKey = session.metadata?.ever_lookup_key;
|
||||
return !lookupKey || lookupKey.startsWith(cloudLookupKeyPrefix(product));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -163,6 +163,15 @@ describe('billing-product predicates', () => {
|
||||
'gauzy'
|
||||
)
|
||||
).toBe(false);
|
||||
// The price decides when it is a catalog price; metadata may only agree with it.
|
||||
expect(
|
||||
subscriptionIsForProduct(teamsSub({ metadata: { ever_product: 'gauzy', ever_hosting: 'cloud' } }), 'gauzy')
|
||||
).toBe(false);
|
||||
expect(subscriptionIsForProduct(gauzySub({ metadata: { ever_product: 'teams' } }), 'gauzy')).toBe(false);
|
||||
// Metadata alone decides only when there is no catalog price.
|
||||
expect(
|
||||
subscriptionIsForProduct(gauzySub({ items: { data: [{ price: { lookup_key: null } }] } }), 'gauzy')
|
||||
).toBe(true);
|
||||
// A catalog price of this product that is not a cloud price.
|
||||
expect(
|
||||
subscriptionIsForProduct(
|
||||
@@ -198,6 +207,14 @@ describe('billing-product predicates', () => {
|
||||
'gauzy'
|
||||
)
|
||||
).toBe(false);
|
||||
// A stamped lookup key must be one of this product's cloud prices.
|
||||
const withKey = (ever_lookup_key: string) => ({
|
||||
mode: 'subscription',
|
||||
metadata: { ever_product: 'gauzy', ever_hosting: 'cloud', ever_lookup_key }
|
||||
});
|
||||
expect(checkoutSessionIsForProduct(withKey('ever_gauzy_cloud_starter_annual'), 'gauzy')).toBe(true);
|
||||
expect(checkoutSessionIsForProduct(withKey('ever_gauzy_selfhosted_enterprise_annual'), 'gauzy')).toBe(false);
|
||||
expect(checkoutSessionIsForProduct(withKey('ever_teams_cloud_starter_monthly'), 'gauzy')).toBe(false);
|
||||
});
|
||||
|
||||
it('recognizes Checkout Session ids and nothing else', () => {
|
||||
|
||||
@@ -397,6 +397,28 @@ describe('StripeWebhookController — foreign events are acknowledged with no DB
|
||||
}
|
||||
})
|
||||
],
|
||||
[
|
||||
'Gauzy-stamped session whose lookup key is a self-hosted price',
|
||||
'checkout.session.completed',
|
||||
session({
|
||||
metadata: {
|
||||
ever_product: 'gauzy',
|
||||
ever_hosting: 'cloud',
|
||||
ever_lookup_key: 'ever_gauzy_selfhosted_enterprise_annual'
|
||||
}
|
||||
})
|
||||
],
|
||||
[
|
||||
'ever_product=gauzy metadata on a Teams price',
|
||||
'customer.subscription.created',
|
||||
subscription({
|
||||
status: 'active',
|
||||
metadata: { ever_product: 'gauzy' },
|
||||
items: {
|
||||
data: [{ id: 'si_t', price: { id: 'price_t', lookup_key: 'ever_teams_cloud_starter_monthly' } }]
|
||||
}
|
||||
})
|
||||
],
|
||||
[
|
||||
'subscription with neither metadata nor a lookup key',
|
||||
'customer.subscription.created',
|
||||
|
||||
Reference in New Issue
Block a user