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

feat(payment): PI-2875 Google Pay coupons handling #2787

Draft
wants to merge 1 commit into
base: master
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
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
134 changes: 129 additions & 5 deletions packages/google-pay-integration/src/gateways/google-pay-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ import {
import isGooglePayCardNetworkKey from '../guards/is-google-pay-card-network-key';
import {
CallbackIntentsType,
CallbackTriggerType,
CallbackTriggerType, ErrorReasonType,
ExtraPaymentData,
GooglePayCardDataResponse,
GooglePayCardNetwork,
GooglePayCardParameters,
GooglePayCardParameters, GooglePayError,
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 @@ -245,7 +249,7 @@ export default class GooglePayGateway {
this._getPaymentMethodFn = getPaymentMethod;
this._isBuyNowFlow = Boolean(isBuyNowFlow);
this._currencyCode = currencyCode;

console.log('isBuyNowFlow', isBuyNowFlow);
if (this._isBuyNowFlow) {
this._getCurrencyCodeOrThrow();
}
Expand Down Expand Up @@ -329,6 +333,126 @@ export default class GooglePayGateway {
return totalPrice;
}

async handleCoupons(
offerData: IntermediatePaymentData['offerData'],
isCartPage = false,
) {
const { redemptionCodes = [] } = offerData || {};
const appliedCoupons = this.getAppliedCoupons();

let error = undefined;

if (!redemptionCodes.length) {
await this.removeAllCoupons(appliedCoupons, isCartPage);

return {
newOfferInfo: this.getAppliedCoupons(),
error,
}
}

// Validate for applied coupons to make sure we have only one applied
if (appliedCoupons.offers.length) {
error = {
reason: ErrorReasonType.OFFER_INVALID,
message: "You can only apply one coupon per order.",
intent: CallbackTriggerType.OFFER
};

return {
newOfferInfo: appliedCoupons,
error,
}
}

const code = redemptionCodes[redemptionCodes.length - 1];
const appliedCouponError = await this.applyCoupon(code, isCartPage);

if (appliedCouponError) {
error = appliedCouponError;
}

return {
newOfferInfo: this.getAppliedCoupons(),
error,
}
}

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

// @ts-ignore
window.check = state.getCheckout
const offers = (coupons || []).map((coupon) => {
const { displayName, code } = coupon;
return {
redemptionCode: code,
description: displayName,
}
});

return {
offers,
}
}

protected async applyCoupon(code: string, isCartPage: boolean) {
let error: GooglePayError | undefined = undefined;

try {
await this._paymentIntegrationService.applyCoupon(code);
} catch (e) {
if (e instanceof Error) {
error = {
reason: ErrorReasonType.OFFER_INVALID,
message: e.message,
intent: CallbackTriggerType.OFFER
};
}

return error;
}

if (isCartPage) {
const couponCodeInput = document.getElementById('couponcode') as HTMLInputElement;

if (couponCodeInput) {
couponCodeInput.value = code;

const form = document.querySelector('.coupon-form');

if (form) {
try {
form.dispatchEvent(new CustomEvent('submit', { cancelable: true }));
} catch (error) {}
}
}
}
}

protected async removeAllCoupons(
appliedCoupons: GooglePayPaymentDataRequest['offerInfo'],
withClickOnTheCartPage: boolean
) {
const { offers } = appliedCoupons;

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

await Promise.all(couponsPromise);

if (withClickOnTheCartPage) {
const removeCouponLink = document.querySelector('a[href*="/cart.php?action=removecoupon"]') as HTMLAnchorElement;

if (removeCouponLink) {
removeCouponLink.click()
}
}
}

protected getGooglePayInitializationData(): GooglePayInitializationData {
return guard(
this.getPaymentMethod().initializationData,
Expand Down
Loading