Skip to content
This repository has been archived by the owner on Dec 11, 2022. It is now read-only.

Commit

Permalink
Merge pull request #9 from Giftbit/2020-08-27
Browse files Browse the repository at this point in the history
2020-08-27
  • Loading branch information
Jeffery Grajkowski committed Oct 7, 2020
2 parents 1b96430 + f54f370 commit 1fcaaef
Show file tree
Hide file tree
Showing 19 changed files with 1,019 additions and 1,105 deletions.
3 changes: 0 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# Stripe API version to test against.
STRIPE_API_VERSION=2020-03-02

# Stripe test API key for an account to run the tests as.
STRIPE_TEST_SECRET_KEY=

Expand Down
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module.exports = {
"no-inner-declarations": "off", // Needed to allow functions exported from namespaces.
"no-constant-condition": ["error", {
checkLoops: false
}]
}],
"semi": "error"
}
};
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Simulates a stateful Stripe server for local unit testing. Makes Stripe calls 50-100x faster than testing against the official server.

**Stripe version mocked:** `2020-08-27`

Supported features:
- charges: create with the most common [test tokens](https://stripe.com/docs/testing) or customer card, retrieve, list, update, capture
- refunds: create, retrieve, list
Expand Down
1,654 changes: 713 additions & 941 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stripe-stateful-mock",
"version": "0.0.14",
"version": "0.0.15",
"description": "A half-baked, stateful Stripe mock server",
"main": "dist/index.js",
"scripts": {
Expand Down Expand Up @@ -35,30 +35,30 @@
"dependencies": {
"basic-auth": "^2.0.1",
"body-parser": "^1.19.0",
"deep-equal": "^2.0.3",
"deep-equal": "^2.0.4",
"express": "^4.17.1",
"loglevel": "^1.6.8"
"loglevel": "^1.7.0"
},
"devDependencies": {
"@types/basic-auth": "^1.1.3",
"@types/body-parser": "^1.19.0",
"@types/chai": "^4.2.11",
"@types/chai-as-promised": "^7.1.2",
"@types/chai": "^4.2.13",
"@types/chai-as-promised": "^7.1.3",
"@types/deep-equal": "^1.0.1",
"@types/dotenv-safe": "^8.1.0",
"@types/express": "^4.17.6",
"@types/dotenv-safe": "^8.1.1",
"@types/express": "^4.17.8",
"@types/loglevel": "^1.6.3",
"@types/mocha": "^7.0.2",
"@typescript-eslint/eslint-plugin": "^3.2.0",
"@typescript-eslint/parser": "^3.2.0",
"@types/mocha": "^8.0.3",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
"chai": "^4.2.0",
"chai-exclude": "^2.0.2",
"dotenv-safe": "^8.2.0",
"eslint": "^7.2.0",
"mocha": "^8.0.1",
"eslint": "^7.10.0",
"mocha": "^8.1.3",
"rimraf": "^3.0.2",
"stripe": "^8.63.0",
"ts-node": "^8.10.2",
"typescript": "^3.9.5"
"stripe": "^8.92.0",
"ts-node": "^9.0.0",
"typescript": "^4.0.3"
}
}
15 changes: 12 additions & 3 deletions src/api/customers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Stripe from "stripe";
import {RestError} from "./RestError";
import {applyListOptions, generateId, stringifyMetadata} from "./utils";
import {applyListOptions, expandObject, generateId, stringifyMetadata} from "./utils";
import {cards} from "./cards";
import {AccountData} from "./AccountData";
import {verify} from "./verify";
Expand Down Expand Up @@ -78,7 +78,11 @@ export namespace customers {
accountCustomers.put(accountId, customer);
}

return customer;
return expandObject(
customer,
["sources", "subscriptions"],
params.expand
);
}

export function retrieve(accountId: string, customerId: string, paramName: string): Stripe.Customer {
Expand All @@ -104,6 +108,7 @@ export namespace customers {
if (params.email) {
data = data.filter(d => d.email === params.email);
}

return applyListOptions(data, params, (id, paramName) => retrieve(accountId, id, paramName));
}

Expand Down Expand Up @@ -164,7 +169,11 @@ export namespace customers {
customer.tax_exempt = params.tax_exempt;
}

return customer;
return expandObject(
customer,
["sources", "subscriptions"],
params.expand
);
}

export function addSubscription(accountId: string, customerId: string, subscription: Stripe.Subscription): void {
Expand Down
2 changes: 1 addition & 1 deletion src/api/disputes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export namespace disputes {
source: `dp_${generateId(24)}`,
status: "pending",
type: "adjustment"
})
});
}

const dispute: Stripe.Dispute = {
Expand Down
52 changes: 47 additions & 5 deletions src/api/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,60 @@ export namespace plans {
interval_count: params.interval_count || 1,
livemode: false,
metadata: stringifyMetadata(params.metadata),
nickname: params.nickname || null,
nickname: params.nickname ?? null,
product: product.id,
tiers: params.tiers as any /* close enough */ || null,
tiers_mode: params.tiers_mode || null,
transform_usage: params.transform_usage || null,
trial_period_days: params.trial_period_days || null,
tiers: billingScheme === "tiered" ? params.tiers?.map(tierCreateToTier) : undefined,
tiers_mode: billingScheme === "tiered" ? params.tiers_mode : null,
transform_usage: params.transform_usage ?? null,
trial_period_days: params.trial_period_days ?? null,
usage_type: usageType
};
accountPlans.put(accountId, plan);
return plan;
}

/**
* Coalesce the number amount (which may be passed in as a string) and the
* string decimal amount into a number value.
*
* If this garbage needs to happen in more places refactor it into utils.
*/
function coalesceToAmount(amount?: string | number, decimal?: string): number | null {
if (!isNaN(+amount)) {
return +amount;
}
if (!isNaN(+decimal)) {
return +decimal;
}
return null;
}

/**
* Coalesce the number amount (which may be passed in as a string) and the
* string decimal amount into a string decimal value.
*
* If this garbage needs to happen in more places refactor it into utils.
*/
function coalesceToDecimal(amount?: string | number, decimal?: string): string | null {
if (!isNaN(+amount)) {
return +amount + "";
}
if (decimal) {
return decimal;
}
return null;
}

function tierCreateToTier(tier: Stripe.PlanCreateParams.Tier): Stripe.Plan.Tier {
return {
flat_amount: coalesceToAmount(tier.flat_amount, tier.flat_amount_decimal),
flat_amount_decimal: coalesceToDecimal(tier.flat_amount, tier.flat_amount_decimal),
unit_amount: coalesceToAmount(tier.unit_amount, tier.unit_amount_decimal),
unit_amount_decimal: coalesceToDecimal(tier.unit_amount, tier.unit_amount_decimal),
up_to: +tier.up_to || null
};
}

export function retrieve(accountId: string, planId: string, paramName: string): Stripe.Plan {
log.debug("plans.retrieve", accountId, planId);

Expand Down
2 changes: 1 addition & 1 deletion src/api/prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export namespace prices {

let data = accountPrices.getAll(accountId);
if (params.active != undefined) {
data = data.filter(price => price.active === (params.active as any === "true"))
data = data.filter(price => price.active === (params.active as any === "true"));
}
if (params.currency != undefined) {
data = data.filter(price => price.currency === params.currency);
Expand Down
63 changes: 19 additions & 44 deletions src/api/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {AccountData} from "./AccountData";
import {RestError} from "./RestError";
import {applyListOptions, generateId, stringifyMetadata} from "./utils";
import {customers} from "./customers";
import {plans} from "./plans";
import {prices} from "./prices";
import {verify} from "./verify";
import {taxRates} from "./taxRates";
Expand All @@ -13,7 +14,6 @@ export namespace subscriptions {

const accountSubscriptions = new AccountData<Stripe.Subscription>();
const accountSubscriptionItems = new AccountData<Stripe.SubscriptionItem>();
const accountPlans = new AccountData<Stripe.Plan>();

export function create(accountId: string, params: Stripe.SubscriptionCreateParams): Stripe.Subscription {
log.debug("subscriptions.create", accountId, params);
Expand All @@ -30,8 +30,6 @@ export namespace subscriptions {
default_source = paramsDefaultSource;
}

const plan = (params as any).plan ?? params.items[0].plan;

const subscriptionId = (params as any).id || `sub_${generateId(14)}`;
if (accountSubscriptions.contains(accountId, subscriptionId)) {
throw new RestError(400, {
Expand All @@ -52,7 +50,10 @@ export namespace subscriptions {
application_fee_percent: +params.application_fee_percent || null,
collection_method: params.collection_method || "charge_automatically",
billing_cycle_anchor: +params.billing_cycle_anchor || now,
billing_thresholds: null,
billing_thresholds: params.billing_thresholds ? {
amount_gte: params.billing_thresholds.amount_gte ?? null,
reset_billing_cycle_anchor: params.billing_thresholds.reset_billing_cycle_anchor ?? null
} : null,
cancel_at: null,
cancel_at_period_end: false,
canceled_at: null,
Expand Down Expand Up @@ -80,12 +81,9 @@ export namespace subscriptions {
pending_invoice_item_interval: null,
pending_setup_intent: null,
pending_update: null,
plan: getOrCreatePlanObj(accountId, plan),
quantity: params.items?.length === 1 ? +params.items[0].quantity || 1 : undefined,
schedule: null,
start_date: Math.floor(Date.now() / 1000),
status: "active",
tax_percent: +params.tax_percent || null,
transfer_data: params.transfer_data ? {
amount_percent: params.transfer_data.amount_percent ?? null,
destination: accounts.retrieve(accountId, params.transfer_data.destination, "")
Expand All @@ -112,37 +110,19 @@ export namespace subscriptions {
return subscription;
}

function getOrCreatePlanObj(accountId: string, planName: string): Stripe.Plan {
if (accountPlans.contains(accountId, planName)) {
return accountPlans.get(accountId, planName);
function getOrCreatePlan(accountId: string, planId: string): Stripe.Plan {
try {
return plans.retrieve(accountId, planId, "plan");
} catch (error) {
if ((error as RestError).error?.code === "resource_missing") {
return plans.create(accountId, {
id: planId,
currency: "usd",
interval: "month"
});
}
throw error;
}

const plan: Stripe.Plan = {
id: planName,
object: "plan",
active: true,
aggregate_usage: null,
amount: 10 * 100,
amount_decimal: "10",
billing_scheme: "per_unit",
created: Math.floor(Date.now() / 1000),
currency: "usd",
deleted: undefined,
interval: "month",
interval_count: 1,
livemode: false,
metadata: {},
nickname: null,
product: `prod_${planName.substr(5)}`,
tiers: null,
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: "licensed"
};
accountPlans.put(accountId, plan);

return plan;
}

function createItem(accountId: string, item: Stripe.SubscriptionCreateParams.Item, subscriptionId: string): Stripe.SubscriptionItem {
Expand All @@ -152,11 +132,11 @@ export namespace subscriptions {
const subscriptionItem: Stripe.SubscriptionItem = {
object: "subscription_item",
id: subItemId,
billing_thresholds: null,
billing_thresholds: item.billing_thresholds ?? null,
created: Math.floor(Date.now() / 1000),
deleted: undefined,
metadata: stringifyMetadata(item.metadata),
plan: getOrCreatePlanObj(accountId, item.plan),
plan: getOrCreatePlan(accountId, item.plan), // isn't in the documentation, deprecated?
price: item.price ? prices.retrieve(accountId, item.price, "price") : null,
quantity: +item.quantity || 1,
subscription: subscriptionId,
Expand All @@ -174,11 +154,6 @@ export namespace subscriptions {

if (params.quantity) {
subscriptionItem.quantity = +params.quantity;

const sub = retrieve(accountId, subscriptionItem.subscription, "id");
if (sub.items.data.length === 1) {
sub.quantity = +params.quantity;
}
}

return subscriptionItem;
Expand Down
35 changes: 35 additions & 0 deletions src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,38 @@ export function applyListOptions<T extends { id: string }>(data: T[], params: St
url: "/v1/refunds"
};
}

export type Nullable<T> = T extends null | undefined ? T : never;

/**
* Hide some properties from the object that are not expanded.
* This creates a copy of the object.
* @param obj The object to expand.
* @param hideList The list of properties to hide.
* @param expandList The list of properties to expand (overriding hideList).
*/
export function expandObject<T extends { id: string }>(obj: T, hideList: (keyof Nullable<T>)[], expandList?: (keyof Nullable<T>)[]): T {
const expandListValid = expandList != null && Array.isArray(expandList);
const filteredObj: Partial<T> = {};
for (const key in obj) {
if (!hideList.includes(key) || (expandListValid && expandList.includes(key))) {
filteredObj[key] = obj[key];
}
}

return filteredObj as T;
}

/**
* Hide some properties from the objects that are not expanded.
* This creates a copy of the object.
* @param list The list of objects to expand.
* @param hideList The list of properties to hide.
* @param expandList The list of properties to expand (overriding hideList).
*/
export function expandList<T extends { id: string }>(list: Stripe.ApiList<T>, hideList: (keyof T)[], expandList?: (keyof T)[]): Stripe.ApiList<Partial<T>> {
return {
...list,
data: list.data.map(d => expandObject(d, hideList, expandList))
};
}
Loading

0 comments on commit 1fcaaef

Please sign in to comment.