Skip to content

Commit

Permalink
fix(eligibility): use offeringId instead of productId for eligibility
Browse files Browse the repository at this point in the history
Because:

- Product IDs can be shared between multiple offerings

This commit:

- Switches to comparing offering apiIdentifiers (offering IDs) rather
  than productIds

Closes FXA-10536
  • Loading branch information
julianpoy committed Oct 18, 2024
1 parent 654ceec commit 4437cef
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 696 deletions.
431 changes: 117 additions & 314 deletions libs/payments/eligibility/src/lib/eligibility.manager.spec.ts

Large diffs are not rendered by default.

72 changes: 21 additions & 51 deletions libs/payments/eligibility/src/lib/eligibility.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
EligibilityStatus,
IntervalComparison,
OfferingComparison,
OfferingOverlapProductResult,
OfferingOverlapResult,
} from './eligibility.types';
import { intervalComparison, offeringComparison } from './utils';
Expand All @@ -36,65 +35,35 @@ export class EligibilityManager {
*/
async getOfferingOverlap(
priceIds: string[],
offeringStripeProductIds: string[],
targetPriceId: string
): Promise<OfferingOverlapResult[]> {
if (!priceIds.length && !offeringStripeProductIds.length) return [];
if (!priceIds.length) return [];

const detailsResult =
await this.productConfigurationManager.getPurchaseDetailsForEligibility([
...priceIds,
targetPriceId,
]);
await this.productConfigurationManager.getPurchaseDetailsForEligibility(
Array.from(new Set([...priceIds, targetPriceId]))
);

const result: OfferingOverlapResult[] = [];

const targetOffering = detailsResult.offeringForPlanId(targetPriceId);
if (!targetOffering) return [];

for (const offeringProductId of offeringStripeProductIds) {
const comparison = offeringComparison(offeringProductId, targetOffering);
if (comparison)
result.push({ comparison, offeringProductId, type: 'offering' });
}

for (const priceId of priceIds) {
const fromOffering = detailsResult.offeringForPlanId(priceId);
if (!fromOffering) continue;

const comparison = offeringComparison(
fromOffering.stripeProductId,
fromOffering.apiIdentifier,
targetOffering
);
if (comparison) result.push({ comparison, priceId, type: 'price' });
}
return result;
}

/**
* Determine what existing offering a user has overlap with
* a desired target price and what the comparison is to the target price.
*
* @returns Array of overlapping offeringProductIds and their comparison
* to the target price.
*/
getProductIdOverlap(
offeringStripeProductIds: string[],
targetOffering: EligibilityContentOfferingResult
): OfferingOverlapProductResult[] {
if (!offeringStripeProductIds.length || !targetOffering) return [];

const result: OfferingOverlapProductResult[] = [];

for (const offeringProductId of offeringStripeProductIds) {
const comparison = offeringComparison(offeringProductId, targetOffering);
if (comparison)
result.push({ comparison, offeringProductId, type: 'offering' });
if (comparison) result.push({ comparison, priceId });
}
return result;
}

async compareOverlap(
overlaps: OfferingOverlapProductResult[],
overlaps: OfferingOverlapResult[],
targetOffering: EligibilityContentOfferingResult,
interval: SubplatInterval,
subscribedPrices: StripePrice[]
Expand All @@ -112,34 +81,35 @@ export class EligibilityManager {
if (overlap.comparison === OfferingComparison.DOWNGRADE)
return EligibilityStatus.DOWNGRADE;

const targetPriceIds = targetOffering.defaultPurchase.stripePlanChoices;
const targetPriceIds = targetOffering.defaultPurchase.stripePlanChoices.map(
(el) => el.stripePlanChoice
);
const targetPrice = await this.priceManager.retrieveByInterval(
targetPriceIds.map((el) => el.stripePlanChoice),
targetPriceIds,
interval
);

if (targetPrice) {
const subscribedPriceWithSameProductIdAsTarget = subscribedPrices.find(
(price) => price.product === overlap.offeringProductId
const overlappingPrice = subscribedPrices.find(
(price) => price.id === overlap.priceId
);

if (
!subscribedPriceWithSameProductIdAsTarget ||
!subscribedPriceWithSameProductIdAsTarget.recurring ||
!overlappingPrice ||
!overlappingPrice.recurring ||
!targetPrice.recurring ||
subscribedPriceWithSameProductIdAsTarget.id === targetPrice.id
overlappingPrice.id === targetPrice.id
)
return EligibilityStatus.INVALID;

const intervalComparisonResult = intervalComparison(
{
unit: subscribedPriceWithSameProductIdAsTarget.recurring?.interval,
count:
subscribedPriceWithSameProductIdAsTarget.recurring?.interval_count,
unit: overlappingPrice.recurring.interval,
count: overlappingPrice.recurring.interval_count,
},
{
unit: targetPrice.recurring?.interval,
count: targetPrice.recurring?.interval_count,
unit: targetPrice.recurring.interval,
count: targetPrice.recurring.interval_count,
}
);
// Any interval change that is lower than the existing price's interval is
Expand Down
15 changes: 7 additions & 8 deletions libs/payments/eligibility/src/lib/eligibility.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { EligibilityService } from './eligibility.service';
import {
EligibilityStatus,
OfferingComparison,
OfferingOverlapProductResult,
OfferingOverlapResult,
} from './eligibility.types';

describe('EligibilityService', () => {
Expand Down Expand Up @@ -104,10 +104,9 @@ describe('EligibilityService', () => {
const mockOverlapResult = [
{
comparison: OfferingComparison.UPGRADE,
offeringProductId: 'prod_test',
type: 'offering',
priceId: 'prod_test',
},
] satisfies OfferingOverlapProductResult[];
] satisfies OfferingOverlapResult[];

jest
.spyOn(productConfigurationManager, 'getEligibilityContentByOffering')
Expand All @@ -122,8 +121,8 @@ describe('EligibilityService', () => {
jest.spyOn(subscriptionManager, 'listForCustomer').mockResolvedValue([]);

jest
.spyOn(eligibilityManager, 'getProductIdOverlap')
.mockReturnValue(mockOverlapResult);
.spyOn(eligibilityManager, 'getOfferingOverlap')
.mockResolvedValue(mockOverlapResult);

jest
.spyOn(eligibilityManager, 'compareOverlap')
Expand All @@ -141,9 +140,9 @@ describe('EligibilityService', () => {
expect(subscriptionManager.listForCustomer).toHaveBeenCalledWith(
mockCustomer.id
);
expect(eligibilityManager.getProductIdOverlap).toHaveBeenCalledWith(
expect(eligibilityManager.getOfferingOverlap).toHaveBeenCalledWith(
[],
mockOffering
mockOffering.apiIdentifier
);
expect(eligibilityManager.compareOverlap).toHaveBeenCalledWith(
mockOverlapResult,
Expand Down
8 changes: 4 additions & 4 deletions libs/payments/eligibility/src/lib/eligibility.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ export class EligibilityService {
.flatMap((subscription) => subscription.items.data)
.map((item) => item.price);

const productIds = subscribedPrices.map((price) => price.product);
const priceIds = subscribedPrices.map((price) => price.id);

const overlaps = this.eligibilityManager.getProductIdOverlap(
productIds,
targetOffering
const overlaps = await this.eligibilityManager.getOfferingOverlap(
priceIds,
offeringConfigId
);

const eligibility = await this.eligibilityManager.compareOverlap(
Expand Down
16 changes: 1 addition & 15 deletions libs/payments/eligibility/src/lib/eligibility.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,11 @@ export enum IntervalComparison {
SHORTER = 'shorter',
}

export type OfferingOverlapBaseResult = {
export type OfferingOverlapResult = {
comparison: OfferingComparison;
type: string;
};

export type OfferingOverlapPlanResult = OfferingOverlapBaseResult & {
priceId: string;
type: 'price';
};

export type OfferingOverlapProductResult = OfferingOverlapBaseResult & {
offeringProductId: string;
type: 'offering';
};

export type OfferingOverlapResult =
| OfferingOverlapPlanResult
| OfferingOverlapProductResult;

export type Interval = {
unit: 'day' | 'week' | 'month' | 'year';
count: number;
Expand Down
18 changes: 7 additions & 11 deletions libs/payments/eligibility/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,21 @@ import {
* @returns OfferingComparison if there's overlap, null otherwise.
*/
export const offeringComparison = (
fromOfferingProductId: string,
fromOfferingId: string,
targetOffering: EligibilityContentOfferingResult | EligibilityOfferingResult
) => {
if (targetOffering.stripeProductId === fromOfferingProductId)
if (targetOffering.apiIdentifier === fromOfferingId)
return OfferingComparison.SAME;
const commonSubgroups = targetOffering.subGroups.filter(
(subgroup) =>
!!subgroup.offerings.find(
(oc) => oc.stripeProductId === fromOfferingProductId
)
!!subgroup.offerings.find((oc) => oc.apiIdentifier === fromOfferingId)
);
if (!commonSubgroups.length) return null;
const subgroupProductIds = commonSubgroups[0].offerings.map(
(o) => o.stripeProductId
);
const existingIndex = subgroupProductIds.indexOf(fromOfferingProductId);
const targetIndex = subgroupProductIds.indexOf(
targetOffering.stripeProductId
const subgroupOfferingIds = commonSubgroups[0].offerings.map(
(o) => o.apiIdentifier
);
const existingIndex = subgroupOfferingIds.indexOf(fromOfferingId);
const targetIndex = subgroupOfferingIds.indexOf(targetOffering.apiIdentifier);

const resultIndex = targetIndex - existingIndex;
if (resultIndex === 0) {
Expand Down
4 changes: 2 additions & 2 deletions libs/shared/cms/src/__generated__/gql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4437cef

Please sign in to comment.