Skip to content

Commit

Permalink
feat(payment): PI-2875 Google Pay coupons handling
Browse files Browse the repository at this point in the history
  • Loading branch information
bc-dronov committed Feb 10, 2025
1 parent 401c12f commit 77d3050
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import { SubscriptionsActionCreator, SubscriptionsRequestSender } from '../subsc
import createPaymentIntegrationSelectors from './create-payment-integration-selectors';
import DefaultPaymentIntegrationService from './default-payment-integration-service';
import PaymentIntegrationStoreProjectionFactory from './payment-integration-store-projection-factory';
import CouponActionCreator from '../coupon/coupon-action-creator';
import CouponRequestSender from '../coupon/coupon-request-sender';

export default function createPaymentIntegrationService(
store: CheckoutStore,
Expand Down Expand Up @@ -112,6 +114,10 @@ export default function createPaymentIntegrationService(
new StoreCreditRequestSender(requestSender),
);

const applyCouponActionCreator = new CouponActionCreator(
new CouponRequestSender(requestSender),
);

const spamProtection = createSpamProtection(createScriptLoader());
const spamProtectionRequestSender = new SpamProtectionRequestSender(requestSender);
const spamProtectionActionCreator = new SpamProtectionActionCreator(
Expand Down Expand Up @@ -150,6 +156,7 @@ export default function createPaymentIntegrationService(
customerActionCreator,
cartRequestSender,
storeCreditActionCreator,
applyCouponActionCreator,
spamProtectionActionCreator,
paymentProviderCustomerActionCreator,
shippingCountryActionCreator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { StoreCreditActionCreator } from '../store-credit';

import DefaultPaymentIntegrationService from './default-payment-integration-service';
import PaymentIntegrationStoreProjectionFactory from './payment-integration-store-projection-factory';
import CouponActionCreator from '../coupon/coupon-action-creator';

describe('DefaultPaymentIntegrationService', () => {
let subject: PaymentIntegrationService;
Expand Down Expand Up @@ -84,6 +85,7 @@ describe('DefaultPaymentIntegrationService', () => {
let customerActionCreator: Pick<CustomerActionCreator, 'signInCustomer' | 'signOutCustomer'>;
let cartRequestSender: CartRequestSender;
let storeCreditActionCreator: Pick<StoreCreditActionCreator, 'applyStoreCredit'>;
let applyCouponActionCreator: Pick<CouponActionCreator, 'applyCoupon'>;
let paymentProviderCustomerActionCreator: PaymentProviderCustomerActionCreator;
let shippingCountryActionCreator: Pick<ShippingCountryActionCreator, 'loadCountries'>;
let remoteCheckoutActionCreator: Pick<
Expand Down Expand Up @@ -226,6 +228,15 @@ describe('DefaultPaymentIntegrationService', () => {
),
};

applyCouponActionCreator = {
// TODO: remove ts-ignore and update test with related type (PAYPAL-4383)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
applyCoupon: jest.fn(
async () => () => createAction('APPLY_COUPON'),
),
};

spamProtectionActionCreator = {
// TODO: remove ts-ignore and update test with related type (PAYPAL-4383)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -295,6 +306,7 @@ describe('DefaultPaymentIntegrationService', () => {
customerActionCreator as CustomerActionCreator,
cartRequestSender,
storeCreditActionCreator as StoreCreditActionCreator,
applyCouponActionCreator as CouponActionCreator,
spamProtectionActionCreator as SpamProtectionActionCreator,
paymentProviderCustomerActionCreator,
shippingCountryActionCreator as ShippingCountryActionCreator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { PaymentHumanVerificationHandler, SpamProtectionActionCreator } from '..
import { StoreCreditActionCreator } from '../store-credit';

import PaymentIntegrationStoreProjectionFactory from './payment-integration-store-projection-factory';
import CouponActionCreator from '../coupon/coupon-action-creator';

export default class DefaultPaymentIntegrationService implements PaymentIntegrationService {
private _storeProjection: DataStoreProjection<PaymentIntegrationSelectors>;
Expand All @@ -54,6 +55,7 @@ export default class DefaultPaymentIntegrationService implements PaymentIntegrat
private _customerActionCreator: CustomerActionCreator,
private _cartRequestSender: CartRequestSender,
private _storeCreditActionCreator: StoreCreditActionCreator,
private _couponActionCreator: CouponActionCreator,
private _spamProtectionActionCreator: SpamProtectionActionCreator,
private _paymentProviderCustomerActionCreator: PaymentProviderCustomerActionCreator,
private _shippingCountryActionCreator: ShippingCountryActionCreator,
Expand Down Expand Up @@ -215,6 +217,28 @@ export default class DefaultPaymentIntegrationService implements PaymentIntegrat
return this._storeProjection.getState();
}

async applyCoupon(
coupon: string,
options?: RequestOptions,
): Promise<PaymentIntegrationSelectors> {
await this._store.dispatch(
this._couponActionCreator.applyCoupon(coupon, options),
);

return this._storeProjection.getState();
}

async removeCoupon(
coupon: string,
options?: RequestOptions,
): Promise<PaymentIntegrationSelectors> {
await this._store.dispatch(
this._couponActionCreator.removeCoupon(coupon, options),
);

return this._storeProjection.getState();
}

async verifyCheckoutSpamProtection(): Promise<PaymentIntegrationSelectors> {
const { checkout } = this._store.getState();
const { shouldExecuteSpamCheck } = checkout.getCheckoutOrThrow();
Expand Down
44 changes: 42 additions & 2 deletions packages/google-pay-integration/src/gateways/google-pay-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ import {
GooglePayFullBillingAddress,
GooglePayGatewayParameters,
GooglePayInitializationData,
GooglePayMerchantInfo,
GooglePayMerchantInfo, GooglePayPaymentDataRequest,
GooglePayRequiredPaymentData,
GooglePaySetExternalCheckoutData,
GooglePayTransactionInfo,
GooglePayTransactionInfo, IntermediatePaymentData,
ShippingOptionParameters,
TotalPriceStatusType,
} from '../types';
Expand Down Expand Up @@ -142,19 +142,23 @@ export default class GooglePayGateway {
CallbackTriggerType.INITIALIZE,
CallbackTriggerType.SHIPPING_ADDRESS,
CallbackTriggerType.SHIPPING_OPTION,
CallbackTriggerType.OFFER,
];

const initializationTrigger = [CallbackTriggerType.INITIALIZE];
const addressChangeTriggers = [
CallbackTriggerType.INITIALIZE,
CallbackTriggerType.SHIPPING_ADDRESS,
];
const shippingOptionsChangeTriggers = [CallbackTriggerType.SHIPPING_OPTION];
const offerChangeTriggers = [CallbackTriggerType.OFFER];

return {
availableTriggers,
initializationTrigger,
addressChangeTriggers,
shippingOptionsChangeTriggers,
offerChangeTriggers,
};
}

Expand Down Expand Up @@ -311,6 +315,42 @@ export default class GooglePayGateway {
}
}

getAppliedCoupons(): GooglePayPaymentDataRequest['offerInfo'] {
const state = this._paymentIntegrationService.getState();
const { coupons } = state.getCheckout() || {};

const offers = (coupons || []).map((coupon) => {
const { displayName, code } = coupon;
return {
redemptionCode: code,
description: displayName,
}
});

return {
offers,
}
}

async handleCoupons(offerData: IntermediatePaymentData['offerData']): Promise<GooglePayPaymentDataRequest['offerInfo']> {
const { redemptionCodes = [] } = offerData || {};

if (!redemptionCodes.length) {
const { offers } = this.getAppliedCoupons();

const couponsPromise = offers.map((couponCode) => {
return this._paymentIntegrationService.removeCoupon(couponCode.redemptionCode);
});

await Promise.all(couponsPromise);
} else {
const code = redemptionCodes[redemptionCodes.length - 1];
await this._paymentIntegrationService.applyCoupon(code);
}

return this.getAppliedCoupons();
}

async handleShippingOptionChange(optionId: string) {
if (optionId === 'shipping_option_unselected') {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,13 @@ export default class GooglePayButtonStrategy implements CheckoutButtonStrategy {
callbackTrigger,
shippingAddress,
shippingOptionData,
offerData,
}: IntermediatePaymentData): Promise<NewTransactionInfo | void> => {
const {
availableTriggers,
addressChangeTriggers,
shippingOptionsChangeTriggers,
offerChangeTriggers,
} = this._googlePayPaymentProcessor.getCallbackTriggers();

if (!availableTriggers.includes(callbackTrigger)) {
Expand All @@ -216,6 +218,10 @@ export default class GooglePayButtonStrategy implements CheckoutButtonStrategy {
);
}

if (offerChangeTriggers.includes(callbackTrigger)) {
await this._googlePayPaymentProcessor.handleCoupons(offerData);
}

if (this._buyNowInitializeOptions) {
return this._getBuyNowTransactionInfo(availableShippingOptions);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,13 @@ export default class GooglePayCustomerStrategy implements CustomerStrategy {
callbackTrigger,
shippingAddress,
shippingOptionData,
offerData,
}: IntermediatePaymentData): Promise<NewTransactionInfo | void> => {
const {
availableTriggers,
addressChangeTriggers,
shippingOptionsChangeTriggers,
offerChangeTriggers,
} = this._googlePayPaymentProcessor.getCallbackTriggers();

if (!availableTriggers.includes(callbackTrigger)) {
Expand All @@ -135,6 +137,10 @@ export default class GooglePayCustomerStrategy implements CustomerStrategy {
);
}

const newOfferInfo = offerChangeTriggers.includes(callbackTrigger)
? await this._googlePayPaymentProcessor.handleCoupons(offerData)
: undefined;

await this._paymentIntegrationService.loadCheckout();

const totalPrice = this._googlePayPaymentProcessor.getTotalPrice();
Expand All @@ -152,6 +158,9 @@ export default class GooglePayCustomerStrategy implements CustomerStrategy {
...(availableShippingOptions && {
newShippingOptionParameters: availableShippingOptions,
}),
...(newOfferInfo && {
newOfferInfo,
}),
};
},
},
Expand Down Expand Up @@ -217,6 +226,7 @@ export default class GooglePayCustomerStrategy implements CustomerStrategy {
this._googlePayPaymentProcessor.mapToBillingAddressRequestBody(response);
const shippingAddress =
this._googlePayPaymentProcessor.mapToShippingAddressRequestBody(response);

const siteLink =
window.location.pathname === '/embedded-checkout'
? this._paymentIntegrationService.getState().getStoreConfigOrThrow().links.siteLink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
GooglePayIsReadyToPayRequest,
GooglePaymentsClient,
GooglePayPaymentDataRequest,
GooglePayPaymentOptions,
GooglePayPaymentOptions, IntermediatePaymentData,
ShippingOptionParameters,
} from './types';

Expand Down Expand Up @@ -152,6 +152,10 @@ export default class GooglePayPaymentProcessor {
await this._gateway.handleShippingOptionChange(optionId);
}

async handleCoupons(offerData: IntermediatePaymentData['offerData']): Promise<GooglePayPaymentDataRequest['offerInfo']> {
return await this._gateway.handleCoupons(offerData);
}

getTotalPrice(): string {
return this._gateway.getTotalPrice();
}
Expand Down Expand Up @@ -244,6 +248,7 @@ export default class GooglePayPaymentProcessor {
merchantInfo: this._gateway.getMerchantInfo(),
...(await this._gateway.getRequiredData()),
callbackIntents: this._gateway.getCallbackIntents(),
offerInfo: this._gateway.getAppliedCoupons(),
};
this._isReadyToPayRequest = {
...this._baseRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,20 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy {
paymentDataCallbacks: {
onPaymentDataChanged: async ({
callbackTrigger,
offerData,
}: IntermediatePaymentData): Promise<NewTransactionInfo | void> => {
if (callbackTrigger !== CallbackTriggerType.INITIALIZE) {
return;
}

const {
offerChangeTriggers,
} = this._googlePayPaymentProcessor.getCallbackTriggers();

if (offerChangeTriggers.includes(callbackTrigger)) {
await this._googlePayPaymentProcessor.handleCoupons(offerData);
}

await this._paymentIntegrationService.loadCheckout();

const { getCheckoutOrThrow, getCartOrThrow } =
Expand Down
22 changes: 21 additions & 1 deletion packages/google-pay-integration/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ export interface GooglePayPaymentDataRequest extends GooglePayGatewayBaseRequest
allowedCountryCodes?: string[];
phoneNumberRequired?: boolean;
};
offerInfo: {
offers: {
redemptionCode: string,
description: string,
}[];
};
shippingOptionRequired?: boolean;
callbackIntents?: CallbackIntentsType[];
}
Expand All @@ -176,6 +182,17 @@ export interface NewShippingOptionParameters {
newShippingOptionParameters?: ShippingOptionParameters;
}

export interface NewOfferData {
newOfferInfo: {
offers: NewOfferInfo[];
}
}

export interface NewOfferInfo {
redemptionCode: string,
description: string,
}

export interface GoogleShippingOption {
id: string;
label?: string;
Expand All @@ -192,13 +209,16 @@ export interface IntermediatePaymentData {
callbackTrigger: CallbackTriggerType;
shippingAddress: GooglePayFullBillingAddress;
shippingOptionData: GoogleShippingOption;
offerData: {
redemptionCodes: string[];
};
}

export interface GooglePayPaymentOptions {
paymentDataCallbacks?: {
onPaymentDataChanged(
intermediatePaymentData: IntermediatePaymentData,
): Promise<(NewTransactionInfo & NewShippingOptionParameters) | void>;
): Promise<(NewTransactionInfo | NewShippingOptionParameters | NewOfferData) | void>;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ export default interface PaymentIntegrationService {
options?: RequestOptions,
): Promise<PaymentIntegrationSelectors>;

applyCoupon(
coupon: string,
options?: RequestOptions,
): Promise<PaymentIntegrationSelectors>;

removeCoupon(
couponId: string,
options?: RequestOptions,
): Promise<PaymentIntegrationSelectors>;

createBuyNowCart(body: BuyNowCartRequestBody, options?: RequestOptions): Promise<Cart>;

updatePaymentProviderCustomer(
Expand Down
Loading

0 comments on commit 77d3050

Please sign in to comment.