зеркало из https://github.com/mozilla/fxa.git
Merge pull request #16091 from mozilla/FXA-6166
refactor(payments): Utilize new plan-eligibility endpoint
This commit is contained in:
Коммит
d2ce1788db
|
@ -379,7 +379,9 @@ export class CapabilityService {
|
|||
targetPlan: AbbrevPlan
|
||||
): Promise<SubscriptionChangeEligibility> {
|
||||
if (!this.eligibilityManager)
|
||||
return [SubscriptionEligibilityResult.INVALID];
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.INVALID,
|
||||
};
|
||||
const iapProductIds = iapSubscribedPlans.map((p) => p.product_id);
|
||||
const planIds = [
|
||||
...stripeSubscribedPlans.map((p) => p.plan_id),
|
||||
|
@ -392,20 +394,32 @@ export class CapabilityService {
|
|||
);
|
||||
|
||||
// No overlap, we can create a new subscription
|
||||
if (!overlaps.length) return [SubscriptionEligibilityResult.CREATE];
|
||||
if (!overlaps.length)
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.CREATE,
|
||||
};
|
||||
|
||||
// Users with IAP Offering overlaps should not be allowed to proceed
|
||||
if (
|
||||
overlaps.some(
|
||||
const iapRoadblockPlan = iapSubscribedPlans.find((plan) => {
|
||||
return overlaps?.some(
|
||||
(overlap) =>
|
||||
overlap.type === 'offering' &&
|
||||
iapProductIds.includes(overlap.offeringProductId)
|
||||
)
|
||||
)
|
||||
return [SubscriptionEligibilityResult.BLOCKED_IAP];
|
||||
);
|
||||
});
|
||||
|
||||
if (iapRoadblockPlan)
|
||||
return {
|
||||
subscriptionEligibilityResult:
|
||||
SubscriptionEligibilityResult.BLOCKED_IAP,
|
||||
eligibleSourcePlan: iapRoadblockPlan,
|
||||
};
|
||||
|
||||
// Multiple existing overlapping plans, we can't merge them
|
||||
if (overlaps.length > 1) return [SubscriptionEligibilityResult.INVALID];
|
||||
if (overlaps.length > 1)
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.INVALID,
|
||||
};
|
||||
|
||||
const overlap = overlaps[0];
|
||||
assert(
|
||||
|
@ -417,10 +431,15 @@ export class CapabilityService {
|
|||
);
|
||||
|
||||
if (overlap.comparison === OfferingComparison.DOWNGRADE)
|
||||
return [SubscriptionEligibilityResult.DOWNGRADE, overlapAbbrev];
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.DOWNGRADE,
|
||||
eligibleSourcePlan: overlapAbbrev,
|
||||
};
|
||||
|
||||
if (!overlapAbbrev || overlapAbbrev.plan_id === targetPlan.plan_id)
|
||||
return [SubscriptionEligibilityResult.INVALID];
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.INVALID,
|
||||
};
|
||||
|
||||
// Any interval change that is lower than the existing plans interval is
|
||||
// a downgrade. Otherwise its considered an upgrade.
|
||||
|
@ -430,9 +449,15 @@ export class CapabilityService {
|
|||
{ unit: targetPlan.interval, count: targetPlan.interval_count }
|
||||
) === IntervalComparison.SHORTER
|
||||
)
|
||||
return [SubscriptionEligibilityResult.DOWNGRADE, overlapAbbrev];
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.DOWNGRADE,
|
||||
eligibleSourcePlan: overlapAbbrev,
|
||||
};
|
||||
|
||||
return [SubscriptionEligibilityResult.UPGRADE, overlapAbbrev];
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.UPGRADE,
|
||||
eligibleSourcePlan: overlapAbbrev,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -450,10 +475,13 @@ export class CapabilityService {
|
|||
useFirestoreProductConfigs
|
||||
);
|
||||
|
||||
if (!targetProductSet) return [SubscriptionEligibilityResult.INVALID];
|
||||
if (!targetProductSet)
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.INVALID,
|
||||
};
|
||||
|
||||
// Lookup whether user holds an IAP subscription with a shared productSet to the target
|
||||
const iapRoadblock = iapSubscribedPlans.some((abbrevPlan) => {
|
||||
const iapRoadblockPlan = iapSubscribedPlans.find((abbrevPlan) => {
|
||||
const { productSet } = productUpgradeFromProductConfig(
|
||||
abbrevPlan,
|
||||
useFirestoreProductConfigs
|
||||
|
@ -464,7 +492,12 @@ export class CapabilityService {
|
|||
|
||||
// Users with an IAP subscription to the productSet that we're trying to subscribe
|
||||
// to should not be allowed to proceed
|
||||
if (iapRoadblock) return [SubscriptionEligibilityResult.BLOCKED_IAP];
|
||||
if (iapRoadblockPlan)
|
||||
return {
|
||||
subscriptionEligibilityResult:
|
||||
SubscriptionEligibilityResult.BLOCKED_IAP,
|
||||
eligibleSourcePlan: iapRoadblockPlan,
|
||||
};
|
||||
|
||||
const isSubscribedToProductSet = stripeSubscribedPlans.some(
|
||||
(abbrevPlan) => {
|
||||
|
@ -478,7 +511,9 @@ export class CapabilityService {
|
|||
);
|
||||
|
||||
if (!isSubscribedToProductSet)
|
||||
return [SubscriptionEligibilityResult.CREATE];
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.CREATE,
|
||||
};
|
||||
|
||||
// Use the upgradeEligibility helper to check if any of our existing plans are
|
||||
// elegible for an upgrade and if so the user can upgrade that existing plan to the desired plan
|
||||
|
@ -490,12 +525,21 @@ export class CapabilityService {
|
|||
);
|
||||
|
||||
if (eligibility === SubscriptionUpdateEligibility.UPGRADE)
|
||||
return [SubscriptionEligibilityResult.UPGRADE, abbrevPlan];
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.UPGRADE,
|
||||
eligibleSourcePlan: abbrevPlan,
|
||||
};
|
||||
|
||||
if (eligibility === SubscriptionUpdateEligibility.DOWNGRADE)
|
||||
return [SubscriptionEligibilityResult.DOWNGRADE, abbrevPlan];
|
||||
return {
|
||||
subscriptionEligibilityResult:
|
||||
SubscriptionEligibilityResult.DOWNGRADE,
|
||||
eligibleSourcePlan: abbrevPlan,
|
||||
};
|
||||
}
|
||||
return [SubscriptionEligibilityResult.INVALID];
|
||||
return {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.INVALID,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -92,12 +92,14 @@ export class MozillaSubscriptionHandler {
|
|||
|
||||
const targetPlanId = request.params.planId;
|
||||
|
||||
const eligibility = (
|
||||
await this.capabilityService.getPlanEligibility(uid, targetPlanId)
|
||||
)[0];
|
||||
const result = await this.capabilityService.getPlanEligibility(
|
||||
uid,
|
||||
targetPlanId
|
||||
);
|
||||
|
||||
return {
|
||||
eligibility,
|
||||
eligibility: result.subscriptionEligibilityResult,
|
||||
currentPlan: result.eligibleSourcePlan,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,13 +108,14 @@ export class PayPalHandler extends StripeWebhookHandler {
|
|||
}
|
||||
|
||||
// Validate that the user doesn't have conflicting subscriptions, for instance via IAP
|
||||
const eligibility = (
|
||||
await this.capabilityService.getPlanEligibility(
|
||||
customer.metadata.userid,
|
||||
priceId
|
||||
)
|
||||
)[0];
|
||||
if (eligibility !== SubscriptionEligibilityResult.CREATE) {
|
||||
const result = await this.capabilityService.getPlanEligibility(
|
||||
customer.metadata.userid,
|
||||
priceId
|
||||
);
|
||||
if (
|
||||
result.subscriptionEligibilityResult !==
|
||||
SubscriptionEligibilityResult.CREATE
|
||||
) {
|
||||
throw error.userAlreadySubscribedToProduct();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { SeverityLevel } from '@sentry/types';
|
|||
import { getAccountCustomerByUid } from 'fxa-shared/db/models/auth';
|
||||
import {
|
||||
AbbrevPlan,
|
||||
SubscriptionChangeEligibility,
|
||||
SubscriptionEligibilityResult,
|
||||
SubscriptionUpdateEligibility,
|
||||
} from 'fxa-shared/subscriptions/types';
|
||||
|
@ -219,14 +220,14 @@ export class StripeHandler {
|
|||
);
|
||||
}
|
||||
|
||||
const eligibility = await this.capabilityService.getPlanEligibility(
|
||||
uid,
|
||||
planId
|
||||
);
|
||||
const result: SubscriptionChangeEligibility =
|
||||
await this.capabilityService.getPlanEligibility(uid, planId);
|
||||
|
||||
const eligibleForUpgrade =
|
||||
eligibility[0] === SubscriptionEligibilityResult.UPGRADE;
|
||||
const isUpgradeForCurrentPlan = eligibility[1]?.plan_id === currentPlan.id;
|
||||
result.subscriptionEligibilityResult ===
|
||||
SubscriptionEligibilityResult.UPGRADE;
|
||||
const isUpgradeForCurrentPlan =
|
||||
result.eligibleSourcePlan?.plan_id === currentPlan.id;
|
||||
if (!eligibleForUpgrade || !isUpgradeForCurrentPlan) {
|
||||
throw error.invalidPlanUpdate();
|
||||
}
|
||||
|
@ -405,13 +406,15 @@ export class StripeHandler {
|
|||
let isUpgrade = false,
|
||||
sourcePlan;
|
||||
if (customer) {
|
||||
const upgradeResult = await this.capabilityService.getPlanEligibility(
|
||||
const result = await this.capabilityService.getPlanEligibility(
|
||||
customer.metadata.userid,
|
||||
priceId
|
||||
);
|
||||
|
||||
isUpgrade = upgradeResult[0] === SubscriptionUpdateEligibility.UPGRADE;
|
||||
sourcePlan = upgradeResult[1];
|
||||
isUpgrade =
|
||||
result.subscriptionEligibilityResult ===
|
||||
SubscriptionUpdateEligibility.UPGRADE;
|
||||
sourcePlan = result.eligibleSourcePlan;
|
||||
}
|
||||
|
||||
const previewInvoice = await this.stripeHelper.previewInvoice({
|
||||
|
@ -553,13 +556,14 @@ export class StripeHandler {
|
|||
}
|
||||
|
||||
// Validate that the user doesn't have conflicting subscriptions, for instance via IAP
|
||||
const eligibility = (
|
||||
const { subscriptionEligibilityResult } =
|
||||
await this.capabilityService.getPlanEligibility(
|
||||
customer.metadata.userid,
|
||||
priceId
|
||||
)
|
||||
)[0];
|
||||
if (eligibility !== SubscriptionEligibilityResult.CREATE) {
|
||||
);
|
||||
if (
|
||||
subscriptionEligibilityResult !== SubscriptionEligibilityResult.CREATE
|
||||
) {
|
||||
throw error.userAlreadySubscribedToProduct();
|
||||
}
|
||||
|
||||
|
|
|
@ -546,7 +546,7 @@ describe('CapabilityService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('elibility', () => {
|
||||
describe('eligibility', () => {
|
||||
const mockPlanTier1ShortInterval = {
|
||||
plan_id: 'plan_123456',
|
||||
product_id: 'prod_123456',
|
||||
|
@ -612,26 +612,30 @@ describe('CapabilityService', () => {
|
|||
type: 'offering',
|
||||
},
|
||||
]);
|
||||
const actual = (
|
||||
const actual =
|
||||
await capabilityService.eligibilityFromEligibilityManager(
|
||||
[],
|
||||
[mockPlanTier1ShortInterval],
|
||||
mockPlanTier1LongInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.BLOCKED_IAP);
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult:
|
||||
SubscriptionEligibilityResult.BLOCKED_IAP,
|
||||
eligibleSourcePlan: mockPlanTier1ShortInterval,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns create for targetPlan with offering user is not subscribed to', async () => {
|
||||
mockEligibilityManager.getOfferingOverlap = sinon.fake.resolves([]);
|
||||
const actual = (
|
||||
const actual =
|
||||
await capabilityService.eligibilityFromEligibilityManager(
|
||||
[],
|
||||
[],
|
||||
mockPlanTier1ShortInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.CREATE);
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.CREATE,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns upgrade for targetPlan with offering user is subscribed to a lower tier of', async () => {
|
||||
|
@ -642,14 +646,16 @@ describe('CapabilityService', () => {
|
|||
type: 'plan',
|
||||
},
|
||||
]);
|
||||
const actual = (
|
||||
const actual =
|
||||
await capabilityService.eligibilityFromEligibilityManager(
|
||||
[mockPlanTier1ShortInterval],
|
||||
[],
|
||||
mockPlanTier2LongInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.UPGRADE);
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.UPGRADE,
|
||||
eligibleSourcePlan: mockPlanTier1ShortInterval,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns downgrade for targetPlan with offering user is subscribed to a higher tier of', async () => {
|
||||
|
@ -660,14 +666,17 @@ describe('CapabilityService', () => {
|
|||
type: 'plan',
|
||||
},
|
||||
]);
|
||||
const actual = (
|
||||
const actual =
|
||||
await capabilityService.eligibilityFromEligibilityManager(
|
||||
[mockPlanTier2LongInterval],
|
||||
[],
|
||||
mockPlanTier1ShortInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.DOWNGRADE);
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult:
|
||||
SubscriptionEligibilityResult.DOWNGRADE,
|
||||
eligibleSourcePlan: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns upgrade for targetPlan with offering user is subscribed to a higher interval of', async () => {
|
||||
|
@ -678,14 +687,16 @@ describe('CapabilityService', () => {
|
|||
type: 'plan',
|
||||
},
|
||||
]);
|
||||
const actual = (
|
||||
const actual =
|
||||
await capabilityService.eligibilityFromEligibilityManager(
|
||||
[mockPlanTier1ShortInterval],
|
||||
[],
|
||||
mockPlanTier1LongInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.UPGRADE);
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.UPGRADE,
|
||||
eligibleSourcePlan: mockPlanTier1ShortInterval,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns downgrade for targetPlan with shorter interval but higher tier than user is subscribed to', async () => {
|
||||
|
@ -698,14 +709,17 @@ describe('CapabilityService', () => {
|
|||
]);
|
||||
Container.set(EligibilityManager, mockEligibilityManager);
|
||||
capabilityService = new CapabilityService();
|
||||
const actual = (
|
||||
const actual =
|
||||
await capabilityService.eligibilityFromEligibilityManager(
|
||||
[mockPlanTier1LongInterval],
|
||||
[],
|
||||
mockPlanTier2ShortInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.DOWNGRADE);
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult:
|
||||
SubscriptionEligibilityResult.DOWNGRADE,
|
||||
eligibleSourcePlan: mockPlanTier1LongInterval,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns invalid for targetPlan with same offering user is subscribed to', async () => {
|
||||
|
@ -716,14 +730,15 @@ describe('CapabilityService', () => {
|
|||
type: 'plan',
|
||||
},
|
||||
]);
|
||||
const actual = (
|
||||
const actual =
|
||||
await capabilityService.eligibilityFromEligibilityManager(
|
||||
[mockPlanTier1ShortInterval],
|
||||
[],
|
||||
mockPlanTier1ShortInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.INVALID);
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.INVALID,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -731,67 +746,72 @@ describe('CapabilityService', () => {
|
|||
it('returns blocked_iap for targetPlan with productSet the user is subscribed to with IAP', async () => {
|
||||
capabilityService.fetchSubscribedPricesFromAppStore =
|
||||
sinon.fake.resolves(['plan_123456']);
|
||||
const actual = (
|
||||
await capabilityService.eligibilityFromStripeMetadata(
|
||||
[],
|
||||
[mockPlanTier2LongInterval],
|
||||
mockPlanTier1ShortInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.BLOCKED_IAP);
|
||||
const actual = await capabilityService.eligibilityFromStripeMetadata(
|
||||
[],
|
||||
[mockPlanTier2LongInterval],
|
||||
mockPlanTier1ShortInterval
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult:
|
||||
SubscriptionEligibilityResult.BLOCKED_IAP,
|
||||
eligibleSourcePlan: mockPlanTier2LongInterval,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns create for targetPlan with productSet user is not subscribed to', async () => {
|
||||
const actual = (
|
||||
await capabilityService.eligibilityFromStripeMetadata(
|
||||
[],
|
||||
[],
|
||||
mockPlanTier1ShortInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.CREATE);
|
||||
const actual = await capabilityService.eligibilityFromStripeMetadata(
|
||||
[],
|
||||
[],
|
||||
mockPlanTier1ShortInterval
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.CREATE,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns upgrade for targetPlan with productSet user is subscribed to a lower tier of', async () => {
|
||||
capabilityService.fetchSubscribedPricesFromStripe = sinon.fake.resolves(
|
||||
[mockPlanTier1ShortInterval.plan_id]
|
||||
);
|
||||
const actual = (
|
||||
await capabilityService.eligibilityFromStripeMetadata(
|
||||
[mockPlanTier1ShortInterval],
|
||||
[],
|
||||
mockPlanTier2LongInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.UPGRADE);
|
||||
const actual = await capabilityService.eligibilityFromStripeMetadata(
|
||||
[mockPlanTier1ShortInterval],
|
||||
[],
|
||||
mockPlanTier2LongInterval
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.UPGRADE,
|
||||
eligibleSourcePlan: mockPlanTier1ShortInterval,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns downgrade for targetPlan with productSet user is subscribed to a higher tier of', async () => {
|
||||
capabilityService.fetchSubscribedPricesFromStripe = sinon.fake.resolves(
|
||||
[mockPlanTier2LongInterval.plan_id]
|
||||
);
|
||||
const actual = (
|
||||
await capabilityService.eligibilityFromStripeMetadata(
|
||||
[mockPlanTier2LongInterval],
|
||||
[],
|
||||
mockPlanTier1ShortInterval
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.DOWNGRADE);
|
||||
const actual = await capabilityService.eligibilityFromStripeMetadata(
|
||||
[mockPlanTier2LongInterval],
|
||||
[],
|
||||
mockPlanTier1ShortInterval
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult:
|
||||
SubscriptionEligibilityResult.DOWNGRADE,
|
||||
eligibleSourcePlan: mockPlanTier2LongInterval,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns invalid for targetPlan with no product order', async () => {
|
||||
capabilityService.fetchSubscribedPricesFromStripe = sinon.fake.resolves(
|
||||
[mockPlanTier2LongInterval.plan_id]
|
||||
);
|
||||
const actual = (
|
||||
await capabilityService.eligibilityFromStripeMetadata(
|
||||
[mockPlanTier2LongInterval],
|
||||
[],
|
||||
mockPlanNoProductOrder
|
||||
)
|
||||
)[0];
|
||||
assert.equal(actual, SubscriptionEligibilityResult.INVALID);
|
||||
const actual = await capabilityService.eligibilityFromStripeMetadata(
|
||||
[mockPlanTier2LongInterval],
|
||||
[],
|
||||
mockPlanNoProductOrder
|
||||
);
|
||||
assert.deepEqual(actual, {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.INVALID,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -319,7 +319,9 @@ describe('mozilla-subscriptions', () => {
|
|||
describe('plan-eligibility', () => {
|
||||
beforeEach(() => {
|
||||
capabilityService = {
|
||||
getPlanEligibility: sandbox.stub().resolves(['eligibility', undefined]),
|
||||
getPlanEligibility: sandbox.stub().resolves({
|
||||
subscriptionEligibilityResult: 'eligibility',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -334,6 +336,7 @@ describe('plan-eligibility', () => {
|
|||
);
|
||||
assert.deepEqual(resp, {
|
||||
eligibility: 'eligibility',
|
||||
currentPlan: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -204,10 +204,9 @@ describe('subscriptions payPalRoutes', () => {
|
|||
beforeEach(() => {
|
||||
stripeHelper.findCustomerSubscriptionByPlanId =
|
||||
sinon.fake.returns(undefined);
|
||||
capabilityService.getPlanEligibility = sinon.fake.resolves([
|
||||
SubscriptionEligibilityResult.CREATE,
|
||||
undefined,
|
||||
]);
|
||||
capabilityService.getPlanEligibility = sinon.fake.resolves({
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.CREATE,
|
||||
});
|
||||
stripeHelper.cancelSubscription = sinon.fake.resolves({});
|
||||
payPalHelper.cancelBillingAgreement = sinon.fake.resolves({});
|
||||
profile.deleteCache = sinon.fake.resolves({});
|
||||
|
|
|
@ -418,10 +418,9 @@ describe('DirectStripeRoutes', () => {
|
|||
},
|
||||
};
|
||||
mockCapabilityService.getPlanEligibility = sinon.stub();
|
||||
mockCapabilityService.getPlanEligibility.resolves([
|
||||
SubscriptionEligibilityResult.CREATE,
|
||||
undefined,
|
||||
]);
|
||||
mockCapabilityService.getPlanEligibility.resolves({
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.CREATE,
|
||||
});
|
||||
mockCapabilityService.getClients = sinon.stub();
|
||||
mockCapabilityService.getClients.resolves(mockContentfulClients);
|
||||
Container.set(CapabilityService, mockCapabilityService);
|
||||
|
@ -1974,10 +1973,10 @@ describe('DirectStripeRoutes', () => {
|
|||
VALID_REQUEST.params = { subscriptionId: subscriptionId };
|
||||
|
||||
mockCapabilityService.getPlanEligibility = sinon.stub();
|
||||
mockCapabilityService.getPlanEligibility.resolves([
|
||||
SubscriptionEligibilityResult.UPGRADE,
|
||||
plan.id,
|
||||
]);
|
||||
mockCapabilityService.getPlanEligibility.resolves({
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.UPGRADE,
|
||||
eligibleSourcePlan: subscription2,
|
||||
});
|
||||
|
||||
directStripeRoutesInstance.stripeHelper.changeSubscriptionPlan.resolves();
|
||||
|
||||
|
@ -2013,10 +2012,9 @@ describe('DirectStripeRoutes', () => {
|
|||
directStripeRoutesInstance.stripeHelper.findAbbrevPlanById.resolves(plan);
|
||||
|
||||
mockCapabilityService.getPlanEligibility = sinon.stub();
|
||||
mockCapabilityService.getPlanEligibility.resolves([
|
||||
SubscriptionEligibilityResult.UPGRADE,
|
||||
plan.id,
|
||||
]);
|
||||
mockCapabilityService.getPlanEligibility.resolves({
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult.UPGRADE,
|
||||
});
|
||||
|
||||
try {
|
||||
await directStripeRoutesInstance.updateSubscription(VALID_REQUEST);
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
LatestInvoiceItems,
|
||||
} from 'fxa-shared/dto/auth/payments/invoice';
|
||||
import {
|
||||
AbbrevPlan,
|
||||
IapSubscription,
|
||||
MozillaSubscriptionTypes,
|
||||
WebSubscription,
|
||||
|
@ -129,7 +130,7 @@ export const SELECTED_PLAN: Plan = {
|
|||
},
|
||||
};
|
||||
|
||||
export const UPGRADE_FROM_PLAN: Plan = {
|
||||
export const UPGRADE_FROM_PLAN: AbbrevPlan = {
|
||||
plan_id: 'plan_abc',
|
||||
product_id: PRODUCT_ID,
|
||||
product_name: 'Example Product',
|
||||
|
@ -139,6 +140,7 @@ export const UPGRADE_FROM_PLAN: Plan = {
|
|||
interval_count: 1,
|
||||
active: true,
|
||||
plan_metadata: null,
|
||||
plan_name: 'Example Product Monthly',
|
||||
product_metadata: {
|
||||
webIconURL: 'http://placekitten.com/49/49?image=9',
|
||||
webIconBackground: 'lime',
|
||||
|
@ -156,7 +158,7 @@ export const UPGRADE_FROM_PLAN: Plan = {
|
|||
},
|
||||
};
|
||||
|
||||
export const UPGRADE_FROM_PLAN_ARCHIVED: Plan = {
|
||||
export const UPGRADE_FROM_PLAN_ARCHIVED: AbbrevPlan = {
|
||||
...UPGRADE_FROM_PLAN,
|
||||
product_metadata: {
|
||||
...UPGRADE_FROM_PLAN.product_metadata,
|
||||
|
|
|
@ -30,14 +30,14 @@ import './index.scss';
|
|||
import { ProductProps } from '../index';
|
||||
import { PaymentConsentCheckbox } from '../../../components/PaymentConsentCheckbox';
|
||||
import { PaymentProviderDetails } from '../../../components/PaymentProviderDetails';
|
||||
import { WebSubscription } from 'fxa-shared/subscriptions/types';
|
||||
import { AbbrevPlan, WebSubscription } from 'fxa-shared/subscriptions/types';
|
||||
import { FirstInvoicePreview } from 'fxa-shared/dto/auth/payments/invoice';
|
||||
|
||||
export type SubscriptionUpgradeProps = {
|
||||
profile: Profile;
|
||||
customer: Customer;
|
||||
selectedPlan: Plan;
|
||||
upgradeFromPlan: Plan;
|
||||
upgradeFromPlan: AbbrevPlan;
|
||||
upgradeFromSubscription: WebSubscription;
|
||||
invoicePreview: FirstInvoicePreview;
|
||||
discount?: number;
|
||||
|
|
|
@ -476,10 +476,39 @@ describe('routes/Product', () => {
|
|||
});
|
||||
|
||||
it('offers upgrade if user is already subscribed to another plan in the same product set', async () => {
|
||||
const apiMocks = initSubscribedApiMocks({
|
||||
planId: 'plan_upgrade',
|
||||
planEligibility: 'upgrade',
|
||||
});
|
||||
const apiMocks = [
|
||||
nock(profileServer)
|
||||
.get('/v1/profile')
|
||||
.reply(
|
||||
200,
|
||||
{ ...MOCK_PROFILE },
|
||||
{ 'Access-Control-Allow-Origin': '*' }
|
||||
),
|
||||
nock(authServer)
|
||||
.get('/v1/oauth/subscriptions/plans')
|
||||
.reply(200, MOCK_PLANS, { 'Access-Control-Allow-Origin': '*' }),
|
||||
nock(authServer)
|
||||
.get(
|
||||
'/v1/oauth/mozilla-subscriptions/customer/billing-and-subscriptions'
|
||||
)
|
||||
.reply(200, MOCK_CUSTOMER, { 'Access-Control-Allow-Origin': '*' }),
|
||||
nock(authServer)
|
||||
.persist()
|
||||
.get(
|
||||
`/v1/oauth/mozilla-subscriptions/customer/plan-eligibility/plan_upgrade`
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
{ eligibility: 'upgrade', currentPlan: MOCK_PLANS[1] },
|
||||
{ 'Access-Control-Allow-Origin': '*' }
|
||||
),
|
||||
nock(authServer)
|
||||
.persist()
|
||||
.post('/v1/oauth/subscriptions/invoice/preview')
|
||||
.reply(200, mockPreviewInvoiceResponse, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}),
|
||||
];
|
||||
const { findByTestId } = renderWithLocalizationProvider(
|
||||
<Subject
|
||||
{...{
|
||||
|
@ -522,7 +551,39 @@ describe('routes/Product', () => {
|
|||
});
|
||||
|
||||
it('does not allow a downgrade', async () => {
|
||||
const apiMocks = initSubscribedApiMocks({ planId: 'plan_no_downgrade' });
|
||||
const apiMocks = [
|
||||
nock(profileServer)
|
||||
.get('/v1/profile')
|
||||
.reply(
|
||||
200,
|
||||
{ ...MOCK_PROFILE },
|
||||
{ 'Access-Control-Allow-Origin': '*' }
|
||||
),
|
||||
nock(authServer)
|
||||
.get('/v1/oauth/subscriptions/plans')
|
||||
.reply(200, MOCK_PLANS, { 'Access-Control-Allow-Origin': '*' }),
|
||||
nock(authServer)
|
||||
.get(
|
||||
'/v1/oauth/mozilla-subscriptions/customer/billing-and-subscriptions'
|
||||
)
|
||||
.reply(200, MOCK_CUSTOMER, { 'Access-Control-Allow-Origin': '*' }),
|
||||
nock(authServer)
|
||||
.persist()
|
||||
.get(
|
||||
`/v1/oauth/mozilla-subscriptions/customer/plan-eligibility/plan_no_downgrade`
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
{ eligibility: 'downgrade', currentPlan: MOCK_PLANS[0] },
|
||||
{ 'Access-Control-Allow-Origin': '*' }
|
||||
),
|
||||
nock(authServer)
|
||||
.persist()
|
||||
.post('/v1/oauth/subscriptions/invoice/preview')
|
||||
.reply(200, mockPreviewInvoiceResponse, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}),
|
||||
];
|
||||
const { findByTestId } = renderWithLocalizationProvider(
|
||||
<Subject
|
||||
{...{
|
||||
|
@ -562,25 +623,57 @@ describe('routes/Product', () => {
|
|||
});
|
||||
|
||||
it('displays roadblock for an IAP subscribed product', async () => {
|
||||
const apiMocks = initSubscribedApiMocks({
|
||||
mockCustomer: {
|
||||
...MOCK_CUSTOMER_AFTER_SUBSCRIPTION,
|
||||
subscriptions: [
|
||||
const apiMocks = [
|
||||
nock(profileServer)
|
||||
.get('/v1/profile')
|
||||
.reply(
|
||||
200,
|
||||
{ ...MOCK_PROFILE },
|
||||
{ 'Access-Control-Allow-Origin': '*' }
|
||||
),
|
||||
nock(authServer)
|
||||
.get('/v1/oauth/subscriptions/plans')
|
||||
.reply(200, MOCK_PLANS, { 'Access-Control-Allow-Origin': '*' }),
|
||||
nock(authServer)
|
||||
.get(
|
||||
'/v1/oauth/mozilla-subscriptions/customer/billing-and-subscriptions'
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
{
|
||||
_subscription_type: MozillaSubscriptionTypes.IAP_GOOGLE,
|
||||
product_id: PRODUCT_ID,
|
||||
auto_renewing: true,
|
||||
expiry_time_millis: Date.now(),
|
||||
package_name: 'org.mozilla.cooking.with.foxkeh',
|
||||
sku: 'org.mozilla.foxkeh.yearly',
|
||||
product_name: 'Cooking with Foxkeh',
|
||||
price_id: 'nextlevel',
|
||||
...MOCK_CUSTOMER_AFTER_SUBSCRIPTION,
|
||||
subscriptions: [
|
||||
{
|
||||
_subscription_type: MozillaSubscriptionTypes.IAP_GOOGLE,
|
||||
product_id: PRODUCT_ID,
|
||||
auto_renewing: true,
|
||||
expiry_time_millis: Date.now(),
|
||||
package_name: 'org.mozilla.cooking.with.foxkeh',
|
||||
sku: 'org.mozilla.foxkeh.yearly',
|
||||
product_name: 'Cooking with Foxkeh',
|
||||
price_id: 'nextlevel',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
planId: 'nextlevel',
|
||||
planEligibility: 'blocked_iap',
|
||||
});
|
||||
{ 'Access-Control-Allow-Origin': '*' }
|
||||
),
|
||||
nock(authServer)
|
||||
.persist()
|
||||
.get(
|
||||
`/v1/oauth/mozilla-subscriptions/customer/plan-eligibility/nextlevel`
|
||||
)
|
||||
.reply(
|
||||
200,
|
||||
{ eligibility: 'blocked_iap', currentPlan: MOCK_PLANS[1] },
|
||||
{ 'Access-Control-Allow-Origin': '*' }
|
||||
),
|
||||
nock(authServer)
|
||||
.persist()
|
||||
.post('/v1/oauth/subscriptions/invoice/preview')
|
||||
.reply(200, mockPreviewInvoiceResponse, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}),
|
||||
];
|
||||
const { findByTestId } = renderWithLocalizationProvider(
|
||||
<Subject
|
||||
{...{
|
||||
|
|
|
@ -15,9 +15,7 @@ import { State } from '../../store/state';
|
|||
import { sequences, SequenceFunctions } from '../../store/sequences';
|
||||
import { actions, ActionFunctions } from '../../store/actions';
|
||||
import { selectors, SelectorReturns } from '../../store/selectors';
|
||||
import { Plan, ProductMetadata } from '../../store/types';
|
||||
import { metadataFromPlan } from 'fxa-shared/subscriptions/metadata';
|
||||
import { getSubscriptionUpdateEligibility } from 'fxa-shared/subscriptions/stripe';
|
||||
import { Plan } from '../../store/types';
|
||||
|
||||
import FetchErrorDialogMessage from '../../components/FetchErrorDialogMessage';
|
||||
import PlanErrorDialog from '../../components/PlanErrorDialog';
|
||||
|
@ -27,38 +25,23 @@ import SubscriptionUpgrade from '../Product/SubscriptionUpgrade';
|
|||
import SubscriptionCreate from '../Product/SubscriptionCreate';
|
||||
import SubscriptionChangeRoadblock from './SubscriptionChangeRoadblock';
|
||||
import {
|
||||
AbbrevPlan,
|
||||
SubscriptionUpdateEligibility,
|
||||
WebSubscription,
|
||||
IapSubscription,
|
||||
} from 'fxa-shared/subscriptions/types';
|
||||
import {
|
||||
isWebSubscription,
|
||||
isIapSubscription,
|
||||
} from 'fxa-shared/subscriptions/type-guards';
|
||||
import { isWebSubscription } from 'fxa-shared/subscriptions/type-guards';
|
||||
import { findCustomerIapSubscriptionByProductId } from '../../lib/customer';
|
||||
import IapRoadblock from './IapRoadblock';
|
||||
import { CouponDetails } from 'fxa-shared/dto/auth/payments/coupon';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import DialogMessage from '../../components/DialogMessage';
|
||||
|
||||
type PlansByIdType = {
|
||||
[plan_id: string]: { plan: Plan; metadata: ProductMetadata };
|
||||
};
|
||||
|
||||
const indexPlansById = (plans: State['plans']): PlansByIdType =>
|
||||
(plans.result || []).reduce(
|
||||
(acc, plan) => ({
|
||||
...acc,
|
||||
[plan.plan_id]: { plan, metadata: metadataFromPlan(plan) },
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
// Check if the customer is subscribed to a product.
|
||||
const customerIsSubscribedToProduct = (
|
||||
customerSubscriptions: ProductProps['customerSubscriptions'],
|
||||
productId: string
|
||||
) =>
|
||||
): boolean | null =>
|
||||
customerSubscriptions &&
|
||||
customerSubscriptions.some(
|
||||
(customerSubscription) => customerSubscription.product_id === productId
|
||||
|
@ -69,7 +52,7 @@ const customerIsSubscribedToProduct = (
|
|||
const customerIsSubscribedToPlan = (
|
||||
customerSubscriptions: ProductProps['customerSubscriptions'],
|
||||
selectedPlan: Plan
|
||||
) =>
|
||||
): boolean | null =>
|
||||
customerSubscriptions &&
|
||||
customerSubscriptions.some(
|
||||
(customerSubscription) =>
|
||||
|
@ -77,44 +60,6 @@ const customerIsSubscribedToPlan = (
|
|||
selectedPlan.plan_id === customerSubscription.plan_id
|
||||
);
|
||||
|
||||
// If the customer has any subscribed plan that matches the productSet for the
|
||||
// selected plan, determine whether if it's an upgrade or downgrade.
|
||||
// Otherwise, it's 'invalid'.
|
||||
const subscriptionUpdateEligibilityResult = (
|
||||
customerSubscriptions: WebSubscription[],
|
||||
selectedPlan: Plan,
|
||||
plansById: PlansByIdType,
|
||||
useFirestoreProductConfigs: boolean
|
||||
):
|
||||
| {
|
||||
subscriptionUpdateEligibility: SubscriptionUpdateEligibility;
|
||||
plan: Plan;
|
||||
subscription: WebSubscription;
|
||||
}
|
||||
| typeof SubscriptionUpdateEligibility.INVALID => {
|
||||
if (customerSubscriptions) {
|
||||
for (const customerSubscription of customerSubscriptions) {
|
||||
const subscriptionPlanInfo = plansById[customerSubscription.plan_id];
|
||||
const subscriptionUpdateEligibility = getSubscriptionUpdateEligibility(
|
||||
subscriptionPlanInfo.plan,
|
||||
selectedPlan,
|
||||
useFirestoreProductConfigs
|
||||
);
|
||||
if (
|
||||
subscriptionUpdateEligibility !== SubscriptionUpdateEligibility.INVALID
|
||||
) {
|
||||
return {
|
||||
subscriptionUpdateEligibility,
|
||||
plan: subscriptionPlanInfo.plan,
|
||||
subscription: customerSubscription,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SubscriptionUpdateEligibility.INVALID;
|
||||
};
|
||||
|
||||
export type ProductProps = {
|
||||
profile: SelectorReturns['profile'];
|
||||
plans: SelectorReturns['plans'];
|
||||
|
@ -174,9 +119,8 @@ export const Product = ({
|
|||
'(max-width: 429px) and (max-height: 945px) and (orientation: portrait),(max-width: 945px) and (orientation: landscape) and (max-height: 429px)',
|
||||
matchMediaDefault
|
||||
);
|
||||
const plansById = useMemo(() => indexPlansById(plans), [plans]);
|
||||
|
||||
const selectedPlan = useMemo(
|
||||
const selectedPlan: Plan = useMemo(
|
||||
() => getSelectedPlan(productId, planId, plansByProductId),
|
||||
[productId, planId, plansByProductId]
|
||||
);
|
||||
|
@ -264,10 +208,8 @@ export const Product = ({
|
|||
|
||||
// Only check for upgrade or existing subscription if we have a customer.
|
||||
if (customer.result && subscriptionChangeEligibility.result !== null) {
|
||||
const iapSubscription = findCustomerIapSubscriptionByProductId(
|
||||
customerSubscriptions,
|
||||
productId
|
||||
);
|
||||
const iapSubscription: IapSubscription | null =
|
||||
findCustomerIapSubscriptionByProductId(customerSubscriptions, productId);
|
||||
|
||||
// Note regarding IAP roadblock:
|
||||
//
|
||||
|
@ -283,47 +225,11 @@ export const Product = ({
|
|||
// else, product is not subscribed to, but on same product set/might be eligible for upgrade
|
||||
// show iap upgrade contact support error messaging
|
||||
if (subscriptionChangeEligibility.result.eligibility === 'blocked_iap') {
|
||||
// Get plan customer is blocked on
|
||||
const currentPlan = () => {
|
||||
if (selectedPlan.product_metadata !== null) {
|
||||
const iapSubscriptions = (customerSubscriptions || []).filter((s) =>
|
||||
isIapSubscription(s)
|
||||
) as IapSubscription[];
|
||||
|
||||
for (const customerSubscription of iapSubscriptions) {
|
||||
const subscriptionPlanInfo =
|
||||
plansById[customerSubscription.price_id];
|
||||
|
||||
const currentPlanProductSet: Array<string> =
|
||||
subscriptionPlanInfo.metadata.productSet || [];
|
||||
const selectedPlanProductSet: Array<string> = selectedPlan
|
||||
.product_metadata.productSet
|
||||
? selectedPlan.product_metadata.productSet.split(',')
|
||||
: [];
|
||||
|
||||
if (
|
||||
currentPlanProductSet.length !== 0 &&
|
||||
selectedPlanProductSet.length !== 0
|
||||
) {
|
||||
if (
|
||||
selectedPlanProductSet.some(
|
||||
(product: string) =>
|
||||
currentPlanProductSet.indexOf(product) >= 0
|
||||
)
|
||||
) {
|
||||
return subscriptionPlanInfo.plan;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectedPlan;
|
||||
};
|
||||
|
||||
const currentPlan = subscriptionChangeEligibility.result.currentPlan!;
|
||||
return (
|
||||
<IapRoadblock
|
||||
{...{
|
||||
currentPlan: currentPlan(),
|
||||
currentPlan,
|
||||
selectedPlan,
|
||||
customer: customer.result,
|
||||
profile: profile.result,
|
||||
|
@ -341,10 +247,8 @@ export const Product = ({
|
|||
isWebSubscription(s)
|
||||
) as WebSubscription[];
|
||||
|
||||
const alreadySubscribedToSelectedPlan = customerIsSubscribedToPlan(
|
||||
webSubscriptions,
|
||||
selectedPlan
|
||||
);
|
||||
const alreadySubscribedToSelectedPlan: boolean | null =
|
||||
customerIsSubscribedToPlan(webSubscriptions, selectedPlan);
|
||||
|
||||
if (invoicePreview.error || !invoicePreview.result) {
|
||||
const ariaLabelledBy = 'product-invoice-preview-error-title';
|
||||
|
@ -383,15 +287,11 @@ export const Product = ({
|
|||
);
|
||||
}
|
||||
|
||||
const planUpdateEligibilityResult = subscriptionUpdateEligibilityResult(
|
||||
webSubscriptions,
|
||||
selectedPlan,
|
||||
plansById,
|
||||
config.featureFlags.useFirestoreProductConfigs
|
||||
);
|
||||
|
||||
// Not an upgrade or a downgrade.
|
||||
if (planUpdateEligibilityResult === SubscriptionUpdateEligibility.INVALID) {
|
||||
if (
|
||||
subscriptionChangeEligibility.result.eligibility ===
|
||||
SubscriptionUpdateEligibility.INVALID
|
||||
) {
|
||||
if (customerIsSubscribedToProduct(webSubscriptions, productId)) {
|
||||
return (
|
||||
<SubscriptionChangeRoadblock
|
||||
|
@ -415,7 +315,7 @@ export const Product = ({
|
|||
}
|
||||
|
||||
if (
|
||||
planUpdateEligibilityResult.subscriptionUpdateEligibility ===
|
||||
subscriptionChangeEligibility.result.eligibility ===
|
||||
SubscriptionUpdateEligibility.DOWNGRADE
|
||||
) {
|
||||
return (
|
||||
|
@ -426,9 +326,14 @@ export const Product = ({
|
|||
}
|
||||
|
||||
if (
|
||||
planUpdateEligibilityResult.subscriptionUpdateEligibility ===
|
||||
subscriptionChangeEligibility.result.eligibility ===
|
||||
SubscriptionUpdateEligibility.UPGRADE
|
||||
) {
|
||||
const currentPlan: AbbrevPlan =
|
||||
subscriptionChangeEligibility.result.currentPlan!;
|
||||
const currentSubscriptionFromPlan = webSubscriptions.find(
|
||||
(sub) => sub.plan_id === currentPlan.plan_id
|
||||
);
|
||||
return (
|
||||
<SubscriptionUpgrade
|
||||
{...{
|
||||
|
@ -436,8 +341,8 @@ export const Product = ({
|
|||
profile: profile.result,
|
||||
customer: customer.result,
|
||||
selectedPlan,
|
||||
upgradeFromPlan: planUpdateEligibilityResult.plan,
|
||||
upgradeFromSubscription: planUpdateEligibilityResult.subscription,
|
||||
upgradeFromPlan: currentPlan,
|
||||
upgradeFromSubscription: currentSubscriptionFromPlan!,
|
||||
updateSubscriptionPlanAndRefresh,
|
||||
resetUpdateSubscriptionPlan,
|
||||
updateSubscriptionPlanStatus,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AbbrevPlan,
|
||||
MozillaSubscription,
|
||||
PaypalPaymentError,
|
||||
SubscriptionEligibilityResult,
|
||||
|
@ -94,6 +95,7 @@ export type Customer = {
|
|||
|
||||
export type PlanEligibility = {
|
||||
eligibility: SubscriptionEligibilityResult;
|
||||
currentPlan?: AbbrevPlan;
|
||||
};
|
||||
|
||||
export interface CreateSubscriptionResult {
|
||||
|
|
|
@ -220,7 +220,7 @@ export type InvoicePreview = [
|
|||
proratedInvoice?: Stripe.UpcomingInvoice
|
||||
];
|
||||
|
||||
export type SubscriptionChangeEligibility = [
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult,
|
||||
eligibleSourcePlan?: AbbrevPlan
|
||||
];
|
||||
export type SubscriptionChangeEligibility = {
|
||||
subscriptionEligibilityResult: SubscriptionEligibilityResult;
|
||||
eligibleSourcePlan?: AbbrevPlan;
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче