Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor / align payment (upgrade) integration flows #565

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/common/src/controllers/AccountController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,13 +427,11 @@ export default class AccountController {
// resolve and fetch the pending offer after upgrade/downgrade
try {
if (activeSubscription?.pendingSwitchId) {
assertModuleMethod(this.checkoutService.getOffer, 'getOffer is not available in checkout service');
assertModuleMethod(this.checkoutService.getSubscriptionSwitch, 'getSubscriptionSwitch is not available in checkout service');

const switchOffer = await this.checkoutService.getSubscriptionSwitch({ switchId: activeSubscription.pendingSwitchId });
const offerResponse = await this.checkoutService.getOffer({ offerId: switchOffer.responseData.toOfferId });
const switchOffer = await this.checkoutService.getSubscriptionSwitch({ subscription: activeSubscription });

pendingOffer = offerResponse.responseData;
pendingOffer = switchOffer.responseData;
}
} catch (error: unknown) {
logDev('Failed to fetch the pending offer', error);
Expand Down
15 changes: 5 additions & 10 deletions packages/common/src/controllers/CheckoutController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ export default class CheckoutController {
}

if (!useCheckoutStore.getState().switchSubscriptionOffers.length) {
const subscriptionSwitches = await this.getSubscriptionSwitches();
const switchSubscriptionOffers = subscriptionSwitches ? await this.getOffers({ offerIds: subscriptionSwitches }) : [];
const switchSubscriptionOffers = await this.getSubscriptionSwitches();
useCheckoutStore.setState({ switchSubscriptionOffers });
}
};
Expand Down Expand Up @@ -238,24 +237,20 @@ export default class CheckoutController {
};
};

getSubscriptionSwitches = async (): Promise<string[] | null> => {
getSubscriptionSwitches = async (): Promise<Offer[]> => {
const { getAccountInfo } = useAccountStore.getState();

const { customerId } = getAccountInfo();
const { subscription } = useAccountStore.getState();

if (!subscription || !this.checkoutService.getSubscriptionSwitches) return null;

assertModuleMethod(this.checkoutService.getOffer, 'getOffer is not available in checkout service');
if (!subscription || !this.checkoutService.getSubscriptionSwitches) return [];

const response = await this.checkoutService.getSubscriptionSwitches({
customerId: customerId,
offerId: subscription.offerId,
});

if (!response.responseData.available.length) return null;

return response.responseData.available.map(({ toOfferId }) => toOfferId);
return response.responseData;
};

switchSubscription = async () => {
Expand All @@ -271,9 +266,9 @@ export default class CheckoutController {
const switchDirection: 'upgrade' | 'downgrade' = determineSwitchDirection(subscription);

const switchSubscriptionPayload = {
subscription,
toOfferId: selectedOffer.offerId,
customerId: customerId,
offerId: subscription.offerId,
switchDirection: switchDirection,
};

Expand Down
25 changes: 16 additions & 9 deletions packages/common/src/services/WatchHistoryService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inject, injectable } from 'inversify';
import { array, number, object, string } from 'yup';
import { number, object, string } from 'yup';

import type { PlaylistItem } from '../../types/playlist';
import type { SerializedWatchHistoryItem, WatchHistoryItem } from '../../types/watchHistory';
Expand All @@ -13,12 +13,10 @@ import ApiService from './ApiService';
import StorageService from './StorageService';
import AccountService from './integrations/AccountService';

const schema = array(
object().shape({
mediaid: string(),
progress: number(),
}),
);
const schema = object().shape({
mediaid: string(),
progress: number(),
});

@injectable()
export default class WatchHistoryService {
Expand Down Expand Up @@ -63,8 +61,17 @@ export default class WatchHistoryService {
};

private validateWatchHistory(history: unknown) {
if (history && schema.validateSync(history)) {
return history as SerializedWatchHistoryItem[];
if (Array.isArray(history)) {
const validatedHistory = history.filter((item) => {
try {
return schema.validateSync(item);
} catch (error: unknown) {
logDev('Failed to validated watch history item', error);
return false;
}
});

return validatedHistory as SerializedWatchHistoryItem[];
}

return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type {
GetPaymentMethods,
GetSubscriptionSwitch,
GetSubscriptionSwitches,
GetSubscriptionSwitchesResponse,
GetSubscriptionSwitchResponse,
PaymentWithoutDetails,
PaymentWithPayPal,
SwitchSubscription,
Expand Down Expand Up @@ -128,16 +130,31 @@ export default class CleengCheckoutService extends CheckoutService {
};

getSubscriptionSwitches: GetSubscriptionSwitches = async (payload) => {
return this.cleengService.get(`/customers/${payload.customerId}/subscription_switches/${payload.offerId}/availability`, { authenticate: true });
const response = await this.cleengService.get<ServiceResponse<GetSubscriptionSwitchesResponse>>(
`/customers/${payload.customerId}/subscription_switches/${payload.offerId}/availability`,
{ authenticate: true },
);

const subscriptionSwitches = await this.getOffers({ offerIds: response.responseData.available.map((switchOffer) => switchOffer.toOfferId) });

return {
responseData: subscriptionSwitches,
errors: [],
};
};

getSubscriptionSwitch: GetSubscriptionSwitch = async (payload) => {
return this.cleengService.get(`/subscription_switches/${payload.switchId}`, { authenticate: true });
const response = await this.cleengService.get<ServiceResponse<GetSubscriptionSwitchResponse>>(
`/subscription_switches/${payload.subscription.pendingSwitchId}`,
{ authenticate: true },
);

return this.getOffer({ offerId: response.responseData.toOfferId });
};

switchSubscription: SwitchSubscription = async (payload) => {
return this.cleengService.post(
`/customers/${payload.customerId}/subscription_switches/${payload.offerId}`,
`/customers/${payload.customerId}/subscription_switches/${payload.subscription.offerId}`,
JSON.stringify({ toOfferId: payload.toOfferId, switchDirection: payload.switchDirection }),
{ authenticate: true },
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default class JWPAccountService extends AccountService {
canUpdateEmail: false,
canSupportEmptyFullName: false,
canChangePasswordWithOldPassword: true,
canRenewSubscription: false,
canRenewSubscription: true,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it can't be done for JWP now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this should be reverted and solved with a canSwitchSubscription flag instead.

canExportAccountData: true,
canUpdatePaymentMethod: false,
canShowReceipts: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import InPlayer, { type AccessFee, type MerchantPaymentMethod } from '@inplayer-org/inplayer.js';
import { injectable } from 'inversify';
import { inject, injectable, named } from 'inversify';

import { isSVODOffer } from '../../../utils/offers';
import type {
Expand All @@ -10,18 +10,22 @@ import type {
GetEntitlementsResponse,
GetOffers,
GetPaymentMethods,
GetSubscriptionSwitch,
GetSubscriptionSwitches,
Offer,
Order,
Payment,
PaymentMethod,
PaymentWithAdyen,
PaymentWithoutDetails,
PaymentWithPayPal,
SwitchSubscription,
UpdateOrder,
} from '../../../../types/checkout';
import CheckoutService from '../CheckoutService';
import type { ServiceResponse } from '../../../../types/service';
import { isCommonError } from '../../../utils/api';
import AccountService from '../AccountService';

@injectable()
export default class JWPCheckoutService extends CheckoutService {
Expand Down Expand Up @@ -85,7 +89,6 @@ export default class JWPCheckoutService extends CheckoutService {
active: true,
period: offer.access_type.period === 'month' && offer.access_type.quantity === 12 ? 'year' : offer.access_type.period,
freePeriods: offer.trial_period ? 1 : 0,
planSwitchEnabled: offer.item.plan_switch_enabled ?? false,
} as Offer;
};

Expand All @@ -109,6 +112,10 @@ export default class JWPCheckoutService extends CheckoutService {
} as Order;
};

constructor(@inject(AccountService) @named('JWP') private readonly accountService: AccountService) {
super();
}

createOrder: CreateOrder = async (payload) => {
return {
errors: [],
Expand Down Expand Up @@ -276,13 +283,50 @@ export default class JWPCheckoutService extends CheckoutService {
}
};

getSubscriptionSwitches = undefined;
getSubscriptionSwitches: GetSubscriptionSwitches = async (payload) => {
const { data } = await InPlayer.Asset.getAssetAccessFees(this.parseOfferId(payload.offerId));

const subscriptionSwitches = data?.filter((accessFee) => accessFee.item.plan_switch_enabled).map((accessFee) => this.formatOffer(accessFee)) || [];

return { responseData: subscriptionSwitches, errors: [] };
};

getOrder = undefined;

switchSubscription = undefined;
switchSubscription: SwitchSubscription = async (payload) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also remove changeSubscription method from subscription service?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

const { subscription, toOfferId } = payload;
const accessFeeId = parseInt(toOfferId.split('_')[1]);

try {
const response = await InPlayer.Subscription.changeSubscriptionPlan({
access_fee_id: accessFeeId,
inplayer_token: String(subscription.subscriptionId),
});

await this.accountService.updateCustomer({
metadata: {
[`${subscription.subscriptionId}_pending_downgrade`]: toOfferId,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh...this part was in the hook previously. Makes sense to have it here.

},
});

return {
errors: [],
responseData: response.data.message,
};
} catch {
throw new Error('Failed to change subscription');
}
};

getSubscriptionSwitch: GetSubscriptionSwitch = async ({ subscription }) => {
if (subscription.pendingSwitchId) {
const offers = await this.getOffers({ offerIds: [subscription.offerId] });

getSubscriptionSwitch = undefined;
return { responseData: offers.find((offer) => offer.offerId === subscription.pendingSwitchId) || null, errors: [] };
}

return { responseData: null, errors: [] };
};

createAdyenPaymentSession = undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default class JWPSubscriptionService extends SubscriptionService {
};
};

private formatActiveSubscription = (subscription: SubscriptionDetails, expiresAt: number) => {
private formatActiveSubscription = (subscription: SubscriptionDetails, expiresAt: number, pendingSwitchId: string | null) => {
let status = '';
switch (subscription.action_type) {
case 'free-trial':
Expand Down Expand Up @@ -134,7 +134,7 @@ export default class JWPSubscriptionService extends SubscriptionService {
period: subscription.access_type?.period,
totalPrice: subscription.charged_amount,
unsubscribeUrl: subscription.unsubscribe_url,
pendingSwitchId: null,
pendingSwitchId: pendingSwitchId,
} as Subscription;
};

Expand All @@ -159,6 +159,7 @@ export default class JWPSubscriptionService extends SubscriptionService {

getActiveSubscription: GetActiveSubscription = async () => {
const assetId = this.accountService.assetId;
const { data: customer } = await InPlayer.Account.getAccountInfo();

if (assetId === null) throw new Error("Couldn't fetch active subscription, there is no assetId configured");

Expand All @@ -167,10 +168,13 @@ export default class JWPSubscriptionService extends SubscriptionService {

if (hasAccess) {
const { data } = await InPlayer.Subscription.getSubscriptions();
const activeSubscription = data.collection.find((subscription: SubscriptionDetails) => subscription.item_id === assetId);
const activeSubscription = data.collection.find((subscription: SubscriptionDetails) => subscription.item_id === assetId) as
| SubscriptionDetails
| undefined;
const pendingSwitchId = (customer.metadata?.[`${activeSubscription?.subscription_id}_pending_downgrade`] as string) || null;

if (activeSubscription) {
return this.formatActiveSubscription(activeSubscription, hasAccess?.data?.expires_at);
return this.formatActiveSubscription(activeSubscription, hasAccess?.data?.expires_at, pendingSwitchId);
}

return this.formatGrantedSubscription(hasAccess.data);
Expand Down
11 changes: 5 additions & 6 deletions packages/common/types/checkout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PayloadWithIPOverride } from './account';
import type { PaymentDetail } from './subscription';
import type { PaymentDetail, Subscription } from './subscription';
import type { EmptyEnvironmentServiceRequest, EnvironmentServiceRequest, PromiseRequest } from './service';

export type Offer = {
Expand Down Expand Up @@ -41,7 +41,6 @@ export type Offer = {
contentExternalId: number | null;
contentExternalData: string | null;
contentAgeRestriction: string | null;
planSwitchEnabled?: boolean;
};

export type OfferType = 'svod' | 'tvod';
Expand Down Expand Up @@ -188,7 +187,7 @@ export type GetSubscriptionSwitchesResponse = {
};

export type GetSubscriptionSwitchPayload = {
switchId: string;
subscription: Subscription;
};

export type GetSubscriptionSwitchResponse = {
Expand All @@ -206,7 +205,7 @@ export type GetSubscriptionSwitchResponse = {

export type SwitchSubscriptionPayload = {
customerId: string;
offerId: string;
subscription: Subscription;
toOfferId: string;
switchDirection: string;
};
Expand Down Expand Up @@ -372,8 +371,8 @@ export type GetPaymentMethods = EmptyEnvironmentServiceRequest<PaymentMethodResp
export type PaymentWithoutDetails = EnvironmentServiceRequest<PaymentWithoutDetailsPayload, Payment>;
export type PaymentWithAdyen = EnvironmentServiceRequest<PaymentWithAdyenPayload, Payment>;
export type PaymentWithPayPal = EnvironmentServiceRequest<PaymentWithPayPalPayload, PaymentWithPayPalResponse>;
export type GetSubscriptionSwitches = EnvironmentServiceRequest<GetSubscriptionSwitchesPayload, GetSubscriptionSwitchesResponse>;
export type GetSubscriptionSwitch = EnvironmentServiceRequest<GetSubscriptionSwitchPayload, GetSubscriptionSwitchResponse>;
export type GetSubscriptionSwitches = EnvironmentServiceRequest<GetSubscriptionSwitchesPayload, Offer[]>;
export type GetSubscriptionSwitch = EnvironmentServiceRequest<GetSubscriptionSwitchPayload, Offer | null>;
export type SwitchSubscription = EnvironmentServiceRequest<SwitchSubscriptionPayload, SwitchSubscriptionResponse>;
export type GetEntitlements = EnvironmentServiceRequest<GetEntitlementsPayload, GetEntitlementsResponse>;
export type GetAdyenPaymentSession = EnvironmentServiceRequest<AdyenPaymentMethodPayload, AdyenPaymentSession>;
Expand Down
38 changes: 0 additions & 38 deletions packages/hooks-react/src/useSubscriptionChange.ts

This file was deleted.

Loading
Loading