Skip to content

Commit

Permalink
feat: checkout transaction tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
emuvente committed Dec 1, 2023
1 parent e7ae985 commit ff22776
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 19 deletions.
1 change: 1 addition & 0 deletions @kiva/kv-shop/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './basketTotals';
export * from './basketVerification';
export * from './checkoutStatus';
export * from './oneTimeCheckout';
export * from './reciept';
export * from './shopError';
export * from './shopQueries';
export * from './subscriptionCheckout';
Expand Down
78 changes: 59 additions & 19 deletions @kiva/kv-shop/src/oneTimeCheckout.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { ApolloClient } from '@apollo/client/core';
import { gql } from '@apollo/client/core';
import { trackTransaction } from '@kiva/kv-analytics';
import numeral from 'numeral';
import type { DropInWrapper } from './useBraintreeDropIn';
import { pollForFinishedCheckout } from './checkoutStatus';
import { ShopError, parseShopError } from './shopError';
import { callShopMutation, callShopQuery } from './shopQueries';
import { validatePreCheckout } from './validatePreCheckout';
import { wait } from './util/poll';
import getVisitorID from './util/visitorId';
import { getCheckoutTrackingData } from './reciept';

interface CreditAmountNeededData {
shop: {
Expand Down Expand Up @@ -95,11 +98,16 @@ interface DepositCheckoutOptions {
amount: string,
}

interface DepositCheckoutResult {
paymentType: string,
mutation: Promise<CheckoutData>,
}

async function depositCheckout({
apollo,
braintree,
amount,
}: DepositCheckoutOptions) {
}: DepositCheckoutOptions): Promise<DepositCheckoutResult> {
try {
const paymentMethod = await braintree.requestPaymentMethod();
if (!paymentMethod) {
Expand All @@ -109,23 +117,44 @@ async function depositCheckout({
);
}

const { nonce, deviceData } = paymentMethod;
// TODO: need to also track paymentType from above
return callShopMutation<CheckoutData>(apollo, {
mutation: depositCheckoutMutation,
variables: {
nonce,
amount,
savePaymentMethod: false, // save payment methods handled by braintree drop in UI
deviceData,
visitorId: getVisitorID(),
},
}, 0);
const { nonce, deviceData, type } = paymentMethod;
return {
paymentType: type,
mutation: callShopMutation<CheckoutData>(apollo, {
mutation: depositCheckoutMutation,
variables: {
nonce,
amount,
savePaymentMethod: false, // save payment methods handled by braintree drop in UI
deviceData,
visitorId: getVisitorID(),
},
}, 0),
};
} catch (e) {
throw parseShopError(e);
}
}

async function trackSuccess(
apollo: ApolloClient<any>,
checkoutId: string,
paymentType: string,
) {
// get transaction data
const transactionData = await getCheckoutTrackingData(
apollo,
checkoutId,
paymentType,
);

// track transaction
trackTransaction(transactionData);

// wait long enough for tracking to complete
await wait(800);
}

export interface OneTimeCheckoutOptions {
apollo: ApolloClient<any>,
braintree?: DropInWrapper,
Expand Down Expand Up @@ -155,11 +184,19 @@ export async function executeOneTimeCheckout({
}

// initiate async checkout
const data = creditRequired ? await depositCheckout({
apollo,
braintree,
amount: creditNeeded,
}) : await creditCheckout(apollo);
let data: CheckoutData;
let paymentType = '';
if (creditRequired) {
const checkoutResult = await depositCheckout({
apollo,
braintree,
amount: creditNeeded,
});
paymentType = checkoutResult.paymentType;
data = await checkoutResult.mutation;
} else {
data = await creditCheckout(apollo);
}
const transactionId = data?.shop?.transactionId;

// wait on checkout to complete
Expand All @@ -174,9 +211,12 @@ export async function executeOneTimeCheckout({
throw parseShopError(result.errors[0]);
}

// track success
const checkoutId = result.data?.checkoutStatus?.receipt?.checkoutId;
await trackSuccess(apollo, checkoutId, paymentType);

// TODO: redirect needs to handle challenge completion parameters

// redirect to thanks page
const checkoutId = result.data?.checkoutStatus?.receipt?.checkoutId;
window.location.href = `/checkout/post-purchase?kiva_transaction_id=${checkoutId}`;
}
183 changes: 183 additions & 0 deletions @kiva/kv-shop/src/reciept.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/* eslint-disable no-underscore-dangle */
import type { ApolloClient, ApolloQueryResult } from '@apollo/client/core';
import type { TransactionData } from '@kiva/kv-analytics';
import { gql } from '@apollo/client/core';
import getVisitorID from './util/visitorId';

export async function getFTDStatus(apollo: ApolloClient<any>) {
const result = await apollo.query({
query: gql`
query ftdStatus {
my {
id
userAccount {
id
isFirstTimeDepositor
}
}
}
`,
});
return result.data?.my?.userAccount?.isFirstTimeDepositor ?? false;
}

interface RecieptItem {
id: string,
price: string,
__typename: string,
isTip?: boolean,
isUserEdited?: boolean,
}

interface RecieptItemsData {
shop: {
id: string,
receipt: {
id: string,
items: {
totalCount: number,
values: RecieptItem[],
} | null,
} | null,
} | null,
}

export async function getRecieptItems(apollo: ApolloClient<any>, checkoutId: string): Promise<RecieptItem[]> {
return new Promise((resolve, reject) => {
const limit = 100;
let offset = 0;
const observer = apollo.watchQuery<RecieptItemsData>({
query: gql`
query receiptItems($checkoutId: String, $visitorId: String, $limit: Int, $offset: Int) {
shop {
id
receipt(checkoutId: $checkoutId, visitorId: $visitorId) {
id
items(limit: $limit, offset: $offset) {
totalCount
values {
id
price
__typename
... on Donation {
id
isTip
isUserEdited
}
}
}
}
}
}
`,
variables: {
checkoutId,
visitorId: getVisitorID(),
limit,
offset,
},
});

let items: RecieptItem[] = [];
const handleResult = async (result: ApolloQueryResult<RecieptItemsData>) => {
const total = result.data?.shop?.receipt?.items?.totalCount;
items = items.concat(result.data?.shop?.receipt?.items?.values);
if (total > offset + limit) {
offset += limit;
const nextResult = await observer.fetchMore({
variables: {
offset,
},
});
try {
handleResult(nextResult);
} catch (e) {
reject(e);
}
} else {
resolve(items);
}
};

observer.subscribe({
next: handleResult,
error: (error) => {
reject(error);
},
});
});
}

export async function getReceiptTotals(apollo: ApolloClient<any>, checkoutId: string) {
const result = await apollo.query({
query: gql`
query receiptTotals($checkoutId: Int, $visitorId: String) {
shop {
id
receipt(checkoutId: $checkoutId, visitorId: $visitorId) {
id
totals {
loanReservationTotal
donationTotal
kivaCardTotal
itemTotal
kivaCreditAppliedTotal
depositTotals {
depositTotal
}
}
}
}
}
`,
variables: {
checkoutId,
visitorId: getVisitorID(),
},
});
return result.data?.shop?.receipt?.totals;
}

export async function getCheckoutTrackingData(
apollo: ApolloClient<any>,
checkoutId: string,
paymentType: string,
): Promise<TransactionData> {
const [isFTD, items, totals] = await Promise.all([
getFTDStatus(apollo),
getRecieptItems(apollo, checkoutId),
getReceiptTotals(apollo, checkoutId),
]);

const loans = items.filter((item) => item.__typename === 'LoanReservation');
const donations = items.filter((item) => item.__typename === 'Donation');
const kivaCards = items.filter((item) => item.__typename === 'KivaCard');

return {
transactionId: checkoutId,
itemTotal: totals.itemTotal,

// Loan reservations
loans,
loanCount: loans.length,
loanTotal: totals.loanReservationTotal,

// Donations
donations: donations.map(({ id, price, __typename }) => ({ id, price, __typename })),
donationTotal: totals.donationTotal,
isTip: donations.every((donation) => donation.isTip),
isUserEdited: donations.some((donation) => donation.isUserEdited),

// Kiva Cards
kivaCards,
kivaCardCount: kivaCards.length,
kivaCardTotal: totals.kivaCardTotal,

// Credit & deposit
kivaCreditAppliedTotal: totals.kivaCreditAppliedTotal,
depositTotal: totals.depositTotals?.depositTotal ?? '0.00',
paymentType,
isFTD,
};
}
Empty file.

0 comments on commit ff22776

Please sign in to comment.