我正在 Stripe 上开发一个由以前的开发人员构建的支付系统。
当前版本有一个错误。当我们注册一个新用户时,我们系统地为该用户添加一个基本订阅。当我们在浏览器中转到
localhost:8000/#/my-subscription
时,我们希望看到这个基本订阅时出现错误。我们看到 debugstripe, fetchSubscriptions
在控制台中打印了 4 次。我们看到在条纹仪表板中创建了 4 个具有相同电子邮件(和基本订阅)的客户,即使在 getOrCreateCustomer
中我们在创建客户之前检查客户是否存在。
看来多次
fetchSubscriptions
被调用得太快了,导致多次执行/httpOnly/stripe/subscriptions
无法异步执行。
你觉得是这个原因吗?有谁知道如何修改它?
这是前端的
services/subscription.ts
:
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
import _ from 'lodash';
import { getConfig } from './auth';
//tslint:disable
export type Plan = any;
export type Subscription = any;
export type Invoice = any;
export type Event = any;
//tslint:enable
export async function fetchPlans(): Promise<Plan[]> {
const res = await axios.get(`localhost:3000/httpOnly/stripe/plans`);
return res.data.plans;
}
export async function fetch(activeOnly = true): Promise<Subscription[]> {
const res = await axios.get(`localhost:3000/httpOnly/stripe/subscriptions`, await getConfig());
return res.data.subscriptions as Subscription[];
}
export async function fetchTrial(app?: string): Promise<Subscription> {
const res = await axios.get(
`localhost:3000/httpOnly/stripe/subscriptions/trial?app=${app}`,
await getConfig(),
);
return res.data.trial as Subscription;
}
export async function fetchInvoices(subscriptionId: string): Promise<Invoice[]> {
const res = await axios.get(
`localhost:3000/httpOnly/stripe/invoices/${subscriptionId}`,
await getConfig(),
);
return res.data.invoices as Invoice[];
}
export async function fetchUpcomingInvoice(subscriptionId: string): Promise<Invoice | undefined> {
try {
const res = await axios.get(
`localhost:3000/httpOnly/stripe/invoices/${subscriptionId}/upcoming`,
await getConfig(),
);
return res.data.invoice as Invoice;
} catch (e: any) {
const err: AxiosError = e;
if (_.get(err, 'response.status') === 404) {
return undefined
}
throw e;
}
}
export async function fetchSubscriptionEvents(subscriptionId: string): Promise<Event[]> {
const res = await axios.get(
`localhost:3000/httpOnly/stripe/subscriptions/${subscriptionId}/events`,
await getConfig(),
);
return res.data.events as Event[];
}
export interface SubscriptionCreatePayload {
plans: any[]; //tslint:disable-line:no-any
source: string;
}
//tslint:disable-next-line:no-any
export async function create(payload: SubscriptionCreatePayload): Promise<any> {
const { plans, source } = payload;
//tslint:disable-next-line:no-any
const res: any = await axios.post(
`localhost:3000/httpOnly/stripe/subscriptions`,
{
plans,
source,
},
await getConfig(),
);
return res.data.subscription;
}
这是前端的
models/subscription.ts
:
import { Model } from 'dva';
import { DvaModelBuilder, actionCreatorFactory, removeActionNamespace } from 'dva-model-creator';
import * as subscriptionService from '../services/subscription';
interface BaseModel {
loaded: boolean;
error?: Error;
}
type PlansData = subscriptionService.Plan[];
interface PlansState extends BaseModel {
data?: PlansData;
}
interface SubscriptionsData {
subscriptions: subscriptionService.Subscription[];
trial?: subscriptionService.Subscription;
}
interface SubscriptionsState extends BaseModel {
data?: SubscriptionsData;
}
interface SubscriptionDetailData {
invoices?: subscriptionService.Invoice[];
upcomingInvoice?: subscriptionService.Invoice;
events?: subscriptionService.Event[];
}
interface SubscriptionDetailState extends BaseModel {
data?: SubscriptionDetailData;
}
export interface State {
plans: PlansState;
subscriptions: SubscriptionsState;
subscriptionDetail: SubscriptionDetailState;
subscribingError?: Error;
subscribing?: boolean;
updatingSubscription?: boolean;
}
//namespace
const namespace = 'subscription';
const actionCreator = actionCreatorFactory(undefined);
//reducers
const savePlans = actionCreator<PlansData>('savePlans');
const savePlansError = actionCreator<Error>('savePlansError');
const clearPlans = actionCreator<{}>('clearPlans');
const saveSubscriptions = actionCreator<SubscriptionsData>('saveSubscriptions');
const saveSubscriptionsError = actionCreator<Error>('saveSubscriptionsError');
const clearSubscriptions = actionCreator<{}>('clearSubscriptions');
const saveSubscriptionDetail = actionCreator<SubscriptionDetailData>('saveSubscriptionDetail');
const saveSubscriptionDetailError = actionCreator<Error>('saveSubscriptionDetailError');
const clearSubscriptionDetail = actionCreator<{}>('clearSubscriptionDetail');
const saveSubscribingError = actionCreator<Error>('saveSubscribingError');
const clearSubscribingError = actionCreator<{}>('clearSubscribingError');
const setSubscribing = actionCreator<{}>('setSubscribing');
const setUpdatingSubscription = actionCreator<{}>('setUpdatingSubscription');
//effects
const subscribe = actionCreator<subscriptionService.SubscriptionCreatePayload>('subscribe');
const updatePlans = actionCreator<{}>('updatePlans');
const updateSubscriptions = actionCreator<{}>('updateSubscriptions');
const updateSubscriptionDetail = actionCreator<subscriptionService.Subscription | undefined>(
'updateSubscriptionDetail',
);
const fetchPlans = actionCreator<{}>('fetchPlans');
const fetchSubscriptions = actionCreator<{}>('fetchSubscriptions');
const fetchSubscriptionDetail = actionCreator<subscriptionService.Subscription | undefined>(
'fetchSubscriptionDetail',
);
const model = new DvaModelBuilder<State>(
{
plans: {
loaded: false,
},
subscriptions: {
loaded: false,
},
subscriptionDetail: {
loaded: false,
},
},
namespace,
)
.immer(savePlans, (state, payload) => {
return { ...state, plans: { data: payload, loaded: true } };
})
.immer(savePlansError, (state, payload) => {
return { ...state, plans: { error: payload, loaded: true } };
})
.immer(clearPlans, (state, _) => {
return { ...state, plans: { loaded: false } };
})
.immer(saveSubscriptions, (state, payload) => {
return { ...state, subscriptions: { data: payload, loaded: true } };
})
.immer(saveSubscriptionsError, (state, payload) => {
return { ...state, subscriptions: { error: payload, loaded: true } };
})
.immer(clearSubscriptions, (state, _) => {
return { ...state, subscriptions: { loaded: false } };
})
.immer(saveSubscriptionDetail, (state, payload) => {
return { ...state, subscriptionDetail: { data: payload, loaded: true } };
})
.immer(saveSubscriptionDetailError, (state, payload) => {
return { ...state, subscriptionDetail: { error: payload, loaded: true } };
})
.immer(clearSubscriptionDetail, (state, _) => {
return { ...state, subscriptionDetail: { loaded: false } };
})
.immer(saveSubscribingError, (state, payload) => {
return { ...state, subscribingError: payload };
})
.immer(clearSubscribingError, (state, _) => {
return { ...state, subscribingError: undefined };
})
.immer(setSubscribing, (state, payload) => {
return { ...state, subscribing: payload }
})
.immer(setUpdatingSubscription, (state, payload) => {
return { ...state, updatingSubscription: payload }
})
.takeEvery(subscribe, function* (payload, { call, put }) {
try {
yield put.resolve(setSubscribing(true))
yield put.resolve(clearSubscribingError({}));
yield call(subscriptionService.create, { ...payload });
yield put.resolve(clearSubscriptionDetail({}));
yield put.resolve(clearSubscriptions({}));
yield put.resolve(updateSubscriptions({}));
} catch (e: any) {
console.log(e);
yield put.resolve(saveSubscribingError(e));
} finally {
yield put.resolve(setSubscribing(false));
}
})
.takeEvery(updatePlans, function* (_, { call, put }) {
try {
yield put.resolve(clearPlans({}));
const plans = yield call(subscriptionService.fetchPlans);
yield put.resolve(savePlans(plans));
} catch (e: any) {
console.log(e);
yield put.resolve(savePlansError(e));
}
})
.takeEvery(updateSubscriptions, function* (_, { call, put, select }) {
try {
yield put.resolve(setUpdatingSubscription(true));
yield put.resolve(clearSubscriptions({}));
let app = yield select(({ app: { name } }) => name);
const subscriptions = yield call(subscriptionService.fetch);
yield put.resolve(saveSubscriptions({ subscriptions: subscriptions }));
} catch (e: any) {
console.log(e);
yield put.resolve(saveSubscriptionsError(e));
} finally {
yield put.resolve(setUpdatingSubscription(false));
}
})
.takeEvery(updateSubscriptionDetail, function* (subscription, { call, put, select }) {
try {
yield put.resolve(clearSubscriptionDetail({}));
if (subscription === undefined) {
const subscriptions: SubscriptionsState = yield select(
({ subscription: { subscriptions } }: { subscription: State }) => subscriptions,
);
if (subscriptions.loaded && subscriptions.data) {
console.log(subscriptions.data);
subscription = subscriptions.data.subscriptions[0] || subscriptions.data.trial;
}
}
if (subscription === undefined) {
throw new Error('Subscriptions should be loaded first before the subscription detail');
}
const invoices = yield call(subscriptionService.fetchInvoices, subscription.id);
const events = yield call(subscriptionService.fetchSubscriptionEvents, subscription.id);
let upcomingInvoice: subscriptionService.Invoice | undefined;
if (subscription.status === 'active') {
upcomingInvoice = yield call(subscriptionService.fetchUpcomingInvoice, subscription.id);
} else {
upcomingInvoice = undefined;
}
yield put.resolve(saveSubscriptionDetail({ invoices, upcomingInvoice, events }));
} catch (e: any) {
console.log(e);
yield put.resolve(saveSubscriptionDetailError(e));
}
})
.takeEvery(fetchPlans, function* (_, { put, select }) {
console.log('fetchPlans');
const plans: PlansState = yield select(
({ subscription: { plans } }: { subscription: State }) => plans,
);
if (!plans.loaded || plans.error) {
yield put(updatePlans({}));
}
})
.takeEvery(fetchSubscriptions, function* (_, { put, select }) {
console.log("debugstripe, fetchSubscriptions")
const subscriptions: SubscriptionsState = yield select(
({ subscription: { subscriptions } }: { subscription: State }) => subscriptions,
);
if (!subscriptions.loaded || subscriptions.error) {
yield put.resolve(updateSubscriptions({}));
}
})
.takeEvery(fetchSubscriptionDetail, function* (subscription, { put, select }) {
yield put.resolve(fetchSubscriptions({}));
if (subscription !== undefined) {
yield put.resolve(updateSubscriptionDetail(subscription));
} else {
// make sure subscriptions has been loaded
const subscriptionDetail: SubscriptionDetailState = yield select(
({ subscription: { subscriptionDetail } }: { subscription: State }) => subscriptionDetail,
);
if (!subscriptionDetail.loaded || subscriptionDetail.error) {
yield put.resolve(updateSubscriptionDetail(undefined));
}
}
})
.subscript(({ dispatch, history }) => {
return history.listen(({ pathname }) => {
dispatch({ type: 'app/updateApp', payload: history });
if (pathname == "/" || pathname == "/sign" || pathname == "/welcome") return;
dispatch(removeActionNamespace(fetchSubscriptions({})));
if (pathname === '/my-subscription') {
dispatch(removeActionNamespace(fetchSubscriptionDetail(undefined)));
}
});
})
.build();
//tslint:disable-next-line:no-default-export
export default model as Model;
这是后端的
routes/subscription.js
:
const express = require("express");
const router = express.Router();
const keys = require("../config/keys.ourapps");
const stripeModule = require("stripe");
const StripeResource = stripeModule.StripeResource;
const stripe = stripeModule(keys.STRIPE_SECRET_KEY);
const jwt = require("express-jwt");
require('dotenv').config();
const auth = jwt({ secret: process.env.JWT_SECRET_KEY, resultProperty: "locals.payload" });
const userModel = require("../models/Users");
const trial_plan_id = "addin_trial";
const appTrials = {
'ourapps': trial_plan_id,
}
const subscriptionEventsResource = StripeResource.extend({
path: "",
listRelated: StripeResource.method({
method: "GET",
path: "events?related_object={customer}",
methodType: "list"
})
});
const subscriptionEvents = new subscriptionEventsResource(stripe);
function isNotAnonym(user) {
return user && user.local && user.local.type !== "anonym";
}
async function getUserById(id) {
return new Promise((resolve, reject) => {
userModel.user.findById(id, function (err, user) {
if (err) return reject(err);
if (!user) return reject(new Error("can't find user"));
resolve({ ...user.toObject(), _id: id });
});
});
}
function loginCheckCallback(req, res, next) {
auth(req, res, async () => {
let user;
if (res.locals.payload) {
try {
user = await getUserById(res.locals.payload._id);
} catch (e) {
res.status(401).json({ message: e.message });
}
}
user = user || req.user;
if (!isNotAnonym(user)) {
res.status(401).json({ message: "should login first" });
} else {
res.locals.user = user;
return next();
}
});
}
function getPlansFromRequest(req) {
let plans = req.body.plans;
const plan_id = req.body.plan_id;
if (plans && plans.length) {
plans = plans.filter(x => x && !!x.id);
} else if (plan_id) {
plans = [{ id: plan_id }];
}
return plans;
}
function plansCheckCallback(req, res, next) {
const plans = getPlansFromRequest(req);
if (!(plans && plans.length)) {
res.status(400).json({ message: "invalid plan" });
} else {
res.locals.plans = plans;
next();
}
}
async function getOrCreateCustomer(user) {
try {
const email = userModel.elementFromUser(user, "email");
// Search for existing customer
const customers = await stripe.customers.list({ email: email, limit: 10 });
console.log("debugstripe, customers", customers)
// If customer exists, return the customer object
if (customers.data.length > 0) {
const customer = customers.data[0];
if (customer.deleted) {
throw "user has been deleted";
} else {
return customer;
}
}
// If customer doesn't exist, create a new customer
const name = userModel.elementFromUser(user, "name");
return await stripe.customers.create({ email: email, name: name });
} catch (e) {
console.error("Error in getOrCreateCustomer:", e);
throw e;
}
}
function getAppTrialPlan(app) {
if (app && typeof appTrials[app] !== 'undefined' && app != null && app != 'null' && appTrials[app]) return appTrials[app];
return trial_plan_id;
}
async function getOrCreateTrial(customer, app) {
let trialPlan = getAppTrialPlan(app);
const response = await stripe.subscriptions.list({
limit: 100,
status: "all",
customer: customer.id,
plan: trialPlan
});
const subs = response.data;
if (!subs.length) {
return await stripe.subscriptions.create({
customer: customer.id,
items: [
{
plan: trialPlan
}
],
cancel_at: Math.floor((Date.now() + 864e5 * keys.TRIAL_DAYS) / 1000) // set trial days to TRIAL_DAYS;
});
} else {
return subs[0];
}
}
router.get("/httpOnly/stripe/plans", async (req, res) => {
const resp = await stripe.plans.list({ limit: 100 });
const plans = resp.data.filter(x => !Object.values(appTrials).find(v => v == x.id));
res.json({ plans });
});
async function getSubscription(customer) {
const [subResp, cancelledSubRep] = await Promise.all([
stripe.subscriptions.list({
limit: 100,
customer: customer.id
}),
stripe.subscriptions.list({
limit: 100,
status: "canceled",
customer: customer.id
})
]);
const is_trial = x => x.plan && Object.values(appTrials).find(v => v == x.plan.id);
subscriptions = subResp.data.filter(x => !is_trial(x));
cancelledSubscriptions = cancelledSubRep.data.filter(x => !is_trial(x));
if (subscriptions && subscriptions.length) {
return subscriptions[0];
}
const newSubscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ plan: "price_1MwQJYEV4K2GahYLGQMmswJl" }],
})
return newSubscription
}
router.get(
"/httpOnly/stripe/subscriptions",
loginCheckCallback,
async (req, res) => {
const user = res.locals.user;
try {
const customer = await getOrCreateCustomer(user);
const subscription = await getSubscription(customer);
const subscriptions = subscription ? [subscription] : [];
res.json({ subscriptions });
} catch (e) {
console.log(e);
res
.status(e.statusCode || 500)
.json({ message: "error when get subscriptions: " + e.message });
}
}
);
router.get(
"/httpOnly/stripe/subscriptions/trial",
loginCheckCallback,
async (req, res) => {
const user = res.locals.user;
const app = req.query.app;
try {
const customer = await getOrCreateCustomer(user);
const trial = await getOrCreateTrial(customer, app);
res.json({ trial });
} catch (e) {
console.log(e);
res
.status(e.statusCode || 500)
.json({ message: "error when fetch trial: " + e.message });
}
}
);
router.post(
"/httpOnly/stripe/subscriptions",
loginCheckCallback,
plansCheckCallback,
async (req, res) => {
const user = res.locals.user;
const plans = res.locals.plans;
const source = req.body.source;
try {
const customer = await getOrCreateCustomer(user);
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: plans.map(x => ({ plan: x.id })),
source
});
res.json({ subscription, created: true });
} catch (e) {
console.log(e);
res
.status(e.statusCode || 500)
.json({ message: "error when create subscriptions: " + e.message });
}
}
);
module.exports = router;