确保从前端异步执行

问题描述 投票:0回答:0

我正在 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;
reactjs react-redux stripe-payments redux-saga
© www.soinside.com 2019 - 2024. All rights reserved.