Skip to content

Commit

Permalink
Separate stripe step operations from actual calls to stripe to allow …
Browse files Browse the repository at this point in the history
…stubbing

Sinon stubs don't work if the method is called from within the same file where it is defined: sinonjs/sinon#1161

This separates the code that handles one or more StripeTransactionSteps from the code that actually makes calls to Stripe.
  • Loading branch information
Tana Jukes committed Jul 26, 2018
1 parent b13f746 commit 6ad1518
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 110 deletions.
2 changes: 1 addition & 1 deletion src/lambdas/rest/transactions/executeTransactionPlan.ts
Expand Up @@ -5,7 +5,6 @@ import {Transaction} from "../../../model/Transaction";
import {TransactionPlanError} from "./TransactionPlanError";
import {getKnexWrite} from "../../../utils/dbUtils/connection";
import * as log from "loglevel";
import {chargeStripeSteps, rollbackStripeSteps} from "../../../utils/stripeUtils/stripeTransactions";
import {setupLightrailAndMerchantStripeConfig} from "../../../utils/stripeUtils/stripeAccess";
import {LightrailAndMerchantStripeConfig} from "../../../utils/stripeUtils/StripeConfig";
import {
Expand All @@ -14,6 +13,7 @@ import {
insertStripeTransactionSteps,
insertTransaction
} from "./insertTransactions";
import {chargeStripeSteps, rollbackStripeSteps} from "../../../utils/stripeUtils/stripeStepOperations";

export interface ExecuteTransactionPlannerOptions {
allowRemainder: boolean;
Expand Down
112 changes: 112 additions & 0 deletions src/utils/stripeUtils/stripeStepOperations.ts
@@ -0,0 +1,112 @@
import {
StripeTransactionPlanStep,
TransactionPlan,
TransactionPlanStep
} from "../../lambdas/rest/transactions/TransactionPlan";
import * as giftbitRoutes from "giftbit-cassava-routes";
import {createRefund, createStripeCharge} from "./stripeTransactions";
import {LightrailAndMerchantStripeConfig} from "./StripeConfig";
import {StripeTransactionParty} from "../../model/TransactionRequest";
import {StripeRestError} from "./StripeRestError";
import {TransactionPlanError} from "../../lambdas/rest/transactions/TransactionPlanError";
import {StripeCreateChargeParams} from "./StripeCreateChargeParams";
import {PaymentSourceForStripeMetadata, StripeSourceForStripeMetadata} from "./PaymentSourceForStripeMetadata";
import log = require("loglevel");

export async function chargeStripeSteps(auth: giftbitRoutes.jwtauth.AuthorizationBadge, stripeConfig: LightrailAndMerchantStripeConfig, plan: TransactionPlan) {
const stripeSteps = plan.steps.filter(step => step.rail === "stripe") as StripeTransactionPlanStep[];

try {
for (let step of stripeSteps) {
const stepForStripe = stripeTransactionPlanStepToStripeRequest(auth, step, plan);

const charge = await createStripeCharge(stepForStripe, stripeConfig.lightrailStripeConfig.secretKey, stripeConfig.merchantStripeConfig.stripe_user_id, step.idempotentStepId);

// Update transaction plan with charge details
step.chargeResult = charge;
// trace back to the requested payment source that lists the right 'source' and/or 'customer' param
if (plan.paymentSources) {
let stepSource = plan.paymentSources.find(
source => source.rail === "stripe" &&
(step.source ? source.source === step.source : true) &&
(step.customer ? source.customer === step.customer : true)
) as StripeTransactionParty;
stepSource.chargeId = charge.id;
}
}
// await doFraudCheck(lightrailStripeConfig, merchantStripeConfig, params, charge, evt, auth);
} catch (err) {
// todo: differentiate between stripe errors / db step errors, and fraud check errors once we do fraud checking: rollback if appropriate & make sure message is clear
if ((err as StripeRestError).additionalParams.stripeError) {
throw err;
} else {
throw new TransactionPlanError(`Transaction execution canceled because there was a problem charging Stripe: ${err}`, {
isReplanable: false
});
}
}
}

function stripeTransactionPlanStepToStripeRequest(auth: giftbitRoutes.jwtauth.AuthorizationBadge, step: StripeTransactionPlanStep, plan: TransactionPlan): StripeCreateChargeParams {
let stepForStripe: StripeCreateChargeParams = {
amount: -step.amount /* Lightrail treats debits as negative amounts on Steps but Stripe requires a positive amount when charging a credit card. */,
currency: plan.currency,
metadata: {
...plan.metadata,
lightrailTransactionId: plan.id,
lightrailTransactionSources: JSON.stringify(plan.steps
.filter(src => !isCurrentStripeStep(src, step))
.map(src => condensePaymentSourceForStripeMetadata(src))),
lightrailUserId: auth.giftbitUserId
}
};
if (step.source) {
stepForStripe.source = step.source;
}
if (step.customer) {
stepForStripe.customer = step.customer;
}

log.debug("Created stepForStripe: \n" + JSON.stringify(stepForStripe, null, 4));
return stepForStripe;
}

export async function rollbackStripeSteps(lightrailStripeSecretKey: string, merchantStripeAccountId: string, steps: StripeTransactionPlanStep[], reason: string): Promise<void> {
try {
for (const step of steps) {
const refund = await createRefund(step, lightrailStripeSecretKey, merchantStripeAccountId, reason);
log.info(`Refunded Stripe charge ${step.chargeResult.id}. Refund: ${JSON.stringify(refund)}.`);
}
} catch (err) {
giftbitRoutes.sentry.sendErrorNotification(err);
throw err;
}
}

function condensePaymentSourceForStripeMetadata(step: TransactionPlanStep): PaymentSourceForStripeMetadata {
switch (step.rail) {
case "lightrail":
return {
rail: "lightrail",
valueId: step.value.id
};
case "internal":
return {
rail: "internal",
internalId: step.internalId
};
case "stripe":
let stripeStep = {rail: "stripe"};
if (step.source) {
(stripeStep as any).source = step.source;
}
if (step.customer) {
(stripeStep as any).customer = step.customer;
}
return stripeStep as StripeSourceForStripeMetadata;
}
}

function isCurrentStripeStep(step: TransactionPlanStep, currentStep: StripeTransactionPlanStep): boolean {
return step.rail === "stripe" && step.idempotentStepId === currentStep.idempotentStepId;
}
110 changes: 1 addition & 109 deletions src/utils/stripeUtils/stripeTransactions.ts
@@ -1,20 +1,12 @@
import {
StripeTransactionPlanStep,
TransactionPlan,
TransactionPlanStep
} from "../../lambdas/rest/transactions/TransactionPlan";
import {StripeTransactionPlanStep} from "../../lambdas/rest/transactions/TransactionPlan";
import {StripeUpdateChargeParams} from "./StripeUpdateChargeParams";
import {StripeRestError} from "./StripeRestError";
import {LightrailAndMerchantStripeConfig} from "./StripeConfig";
import {StripeTransactionParty} from "../../model/TransactionRequest";
import {TransactionPlanError} from "../../lambdas/rest/transactions/TransactionPlanError";
import {StripeCreateChargeParams} from "./StripeCreateChargeParams";
import * as giftbitRoutes from "giftbit-cassava-routes";
import log = require("loglevel");
import Stripe = require("stripe");
import IRefund = Stripe.refunds.IRefund;
import ICharge = Stripe.charges.ICharge;
import {PaymentSourceForStripeMetadata, StripeSourceForStripeMetadata} from "./PaymentSourceForStripeMetadata";

export async function createStripeCharge(params: StripeCreateChargeParams, lightrailStripeSecretKey: string, merchantStripeAccountId: string, stepIdempotencyKey: string): Promise<ICharge> {
const lightrailStripe = require("stripe")(lightrailStripeSecretKey);
Expand Down Expand Up @@ -55,18 +47,6 @@ export async function createStripeCharge(params: StripeCreateChargeParams, light
return charge;
}

export async function rollbackStripeSteps(lightrailStripeSecretKey: string, merchantStripeAccountId: string, steps: StripeTransactionPlanStep[], reason: string): Promise<void> {
try {
for (const step of steps) {
const refund = await createRefund(step, lightrailStripeSecretKey, merchantStripeAccountId, reason);
log.info(`Refunded Stripe charge ${step.chargeResult.id}. Refund: ${JSON.stringify(refund)}.`);
}
} catch (err) {
giftbitRoutes.sentry.sendErrorNotification(err);
throw err;
}
}

export async function createRefund(step: StripeTransactionPlanStep, lightrailStripeSecretKey: string, merchantStripeAccountId: string, reason?: string): Promise<IRefund> {
const lightrailStripe = require("stripe")(lightrailStripeSecretKey);
lightrailStripe.setApiVersion(process.env["STRIPE_API_VERSION"]);
Expand Down Expand Up @@ -108,91 +88,3 @@ export async function updateCharge(chargeId: string, params: StripeUpdateChargeP
log.info(`Updated Stripe charge ${JSON.stringify(chargeUpdate)}.`);
return chargeUpdate;
}


export async function chargeStripeSteps(auth: giftbitRoutes.jwtauth.AuthorizationBadge, stripeConfig: LightrailAndMerchantStripeConfig, plan: TransactionPlan) {
const stripeSteps = plan.steps.filter(step => step.rail === "stripe") as StripeTransactionPlanStep[];

try {
for (let step of stripeSteps) {
const stepForStripe = stripeTransactionPlanStepToStripeRequest(auth, step, plan);

const charge = await createStripeCharge(stepForStripe, stripeConfig.lightrailStripeConfig.secretKey, stripeConfig.merchantStripeConfig.stripe_user_id, step.idempotentStepId);

// Update transaction plan with charge details
step.chargeResult = charge;
// trace back to the requested payment source that lists the right 'source' and/or 'customer' param
if (plan.paymentSources) {
let stepSource = plan.paymentSources.find(
source => source.rail === "stripe" &&
(step.source ? source.source === step.source : true) &&
(step.customer ? source.customer === step.customer : true)
) as StripeTransactionParty;
stepSource.chargeId = charge.id;
}
}
// await doFraudCheck(lightrailStripeConfig, merchantStripeConfig, params, charge, evt, auth);
} catch (err) {
// todo: differentiate between stripe errors / db step errors, and fraud check errors once we do fraud checking: rollback if appropriate & make sure message is clear
if ((err as StripeRestError).additionalParams.stripeError) {
throw err;
} else {
throw new TransactionPlanError(`Transaction execution canceled because there was a problem charging Stripe: ${err}`, {
isReplanable: false
});
}
}
}

function stripeTransactionPlanStepToStripeRequest(auth: giftbitRoutes.jwtauth.AuthorizationBadge, step: StripeTransactionPlanStep, plan: TransactionPlan): StripeCreateChargeParams {
let stepForStripe: StripeCreateChargeParams = {
amount: -step.amount /* Lightrail treats debits as negative amounts on Steps but Stripe requires a positive amount when charging a credit card. */,
currency: plan.currency,
metadata: {
...plan.metadata,
lightrailTransactionId: plan.id,
lightrailTransactionSources: JSON.stringify(plan.steps
.filter(src => !isCurrentStripeStep(src, step))
.map(src => condensePaymentSourceForStripeMetadata(src))),
lightrailUserId: auth.giftbitUserId
}
};
if (step.source) {
stepForStripe.source = step.source;
}
if (step.customer) {
stepForStripe.customer = step.customer;
}

log.debug("Created stepForStripe: \n" + JSON.stringify(stepForStripe, null, 4));
return stepForStripe;
}


function condensePaymentSourceForStripeMetadata(step: TransactionPlanStep): PaymentSourceForStripeMetadata {
switch (step.rail) {
case "lightrail":
return {
rail: "lightrail",
valueId: step.value.id
};
case "internal":
return {
rail: "internal",
internalId: step.internalId
};
case "stripe":
let stripeStep = {rail: "stripe"};
if (step.source) {
(stripeStep as any).source = step.source;
}
if (step.customer) {
(stripeStep as any).customer = step.customer;
}
return stripeStep as StripeSourceForStripeMetadata;
}
}

function isCurrentStripeStep(step: TransactionPlanStep, currentStep: StripeTransactionPlanStep): boolean {
return step.rail === "stripe" && step.idempotentStepId === currentStep.idempotentStepId;
}

0 comments on commit 6ad1518

Please sign in to comment.