libs/payments/eligibility/src/lib/eligibility.service.ts (124 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Injectable } from '@nestjs/common';
import { SubscriptionManager, SubplatInterval } from '@fxa/payments/customer';
import { ProductConfigurationManager } from '@fxa/shared/cms';
import {
GoogleIapPurchaseManager,
AppleIapPurchaseManager,
} from '@fxa/payments/iap';
import { EligibilityManager } from './eligibility.manager';
import {
EligibilityStatus,
LocationStatus,
SubscriptionEligibilityResult,
} from './eligibility.types';
import { LocationConfig } from './location.config';
import { EligibilityError } from './eligibility.error';
@Injectable()
export class EligibilityService {
constructor(
private locationConfig: LocationConfig,
private productConfigurationManager: ProductConfigurationManager,
private eligibilityManager: EligibilityManager,
private subscriptionManager: SubscriptionManager,
private googleIapPurchaseManager: GoogleIapPurchaseManager,
private appleIapPurchaseManager: AppleIapPurchaseManager
) {}
/**
* Checks if user is eligible to subscribe to price
*/
async checkEligibility(
interval: SubplatInterval,
offeringConfigId: string,
uid?: string | null | undefined,
stripeCustomerId?: string | null | undefined
): Promise<SubscriptionEligibilityResult> {
if (!uid) {
if (stripeCustomerId) {
throw new EligibilityError(
"'uid' is required when 'stripeCustomerId' is provided"
);
}
return {
subscriptionEligibilityResult: EligibilityStatus.CREATE,
};
}
const targetOfferingResult =
await this.productConfigurationManager.getEligibilityContentByOffering(
offeringConfigId
);
const targetOffering = targetOfferingResult.getOffering();
const [appleIapPurchases, googleIapPurchases] = await Promise.all([
this.appleIapPurchaseManager.getForUser(uid),
this.googleIapPurchaseManager.getForUser(uid),
]);
if (appleIapPurchases.length || googleIapPurchases.length) {
const iapOfferingResult =
await this.productConfigurationManager.getIapOfferings([
...appleIapPurchases.map((el) => el.productId),
...googleIapPurchases.map((el) => el.sku),
]);
if (iapOfferingResult.hasOverlap(offeringConfigId)) {
return {
subscriptionEligibilityResult: EligibilityStatus.BLOCKED_IAP,
};
}
}
if (!stripeCustomerId) {
return {
subscriptionEligibilityResult: EligibilityStatus.CREATE,
};
}
const subscriptions =
await this.subscriptionManager.listForCustomer(stripeCustomerId);
const subscribedPrices = subscriptions
.flatMap((subscription) => subscription.items.data)
.map((item) => item.price);
const priceIds = subscribedPrices.map((price) => price.id);
const overlaps = await this.eligibilityManager.getOfferingOverlap({
priceIds,
targetOffering,
});
const eligibility = await this.eligibilityManager.compareOverlaps(
overlaps,
targetOffering,
interval,
subscribedPrices
);
return eligibility;
}
async getProductAvailabilityForLocation(
offeringId: string,
countryCode?: string
) {
if (!countryCode) {
return {
status: LocationStatus.Unresolved,
message: 'Country code was not resolved',
};
}
const { countries } =
await this.productConfigurationManager.fetchCMSData(offeringId);
const supportedCountries = countries.map((country) => country.slice(0, 2));
const isSanctionedLocation = countryCode
? this.locationConfig.subscriptionsUnsupportedLocations.includes(
countryCode
)
: undefined;
const isSupportedLocation = countryCode
? supportedCountries.includes(countryCode)
: undefined;
if (isSanctionedLocation) {
return {
status: LocationStatus.SanctionedLocation,
message: `Customer is in a sanctioned location: ${countryCode}`,
};
}
if (isSupportedLocation === false) {
return {
status: LocationStatus.ProductNotAvailable,
message: `Product is not available in customer's location: ${countryCode}`,
};
}
return {
status: LocationStatus.Valid,
message: `Customer is in a location where the product is supported: ${countryCode}`,
};
}
}