def post(self, match_id):
        """"
        TODO: 如果回调时赛事已满员则自动退款处理

        Parteam Api request example: {
          "message": "成功",
          "code": 200,
          "attribute": {
              "userId": 169,
              "orderNo": "11354642167546",
              "paymentMethod":1, // 支付类型 1微信,2支付宝
              "orderState":2 // // 订单状态 1 待付款 2 已付款 3 超时未付款 4 已发货
          }
        }
        """
        data = self.request.body.decode()
        data = json.loads(data)

        if not data.get("attribute"):
            raise ApiException(400, "未包含 attribute")

        order = data['attribute']
        member = MatchMember.get_or_404(pt_order_no=order['orderNo'])

        if order['orderState'] in (2, 4):

            # 将状态为未支付报名状态修改为正常
            if member.state == MatchMember.MatchMemberState.wait_pay:

                payment_method = (TeamOrder.OrderPaymentMethod.WXPAY,
                                  TeamOrder.OrderPaymentMethod.ALIPAY
                                  )[order['paymentMethod'] - 1]

                with self.db.transaction():
                    MatchMember.update(
                        state=MatchMember.MatchMemberState.normal).where(
                            MatchMember.id == member.id).execute()

                    TeamOrder.update(
                        paid=datetime.now(),
                        state=TeamOrder.OrderState.TRADE_BUYER_PAID,
                        gateway_trade_no=order['orderNo'],
                        payment_method=payment_method).where(
                            TeamOrder.id == member.order_id).execute()

                # 统计赛事人数
                Match.update_members_count(member.match_id)
                if member.group_id > 0:
                    MatchGroup.update_members_count(member.group_id)

            match_notify.join_match_done\
                .apply_async(args=[match_id, member.id], retry=True)

        self.write_success()
 def initial_data(self):
     self.team_owner = User.create(name='test_activity')
     self.team = Team.create(name='club_test_activity',
                             owner_id=self.team_owner.id)
     self.user = self.creator = User.create(name='activity_creator')
     self.activity = Activity.create(team=self.team,
                                     creator=self.creator,
                                     price='10',
                                     vip_price='8',
                                     leader=self.creator,
                                     title='just a test',
                                     description='description',
                                     need_nickname=True,
                                     join_end="3000-12-30 22:59:59",
                                     max_members=10,
                                     start_time='3000-01-01 00:00:01',
                                     end_time='3000-12-31 23:59:59')
     self.activity_2 = Activity.create(team=self.team,
                                       creator=self.creator,
                                       price='10',
                                       vip_price='8',
                                       leader=self.creator,
                                       title='just a test',
                                       description='description',
                                       need_nickname=True,
                                       join_end="3000-12-30 22:59:59",
                                       max_members=10,
                                       start_time='3000-01-01 00:00:01',
                                       end_time='3000-12-31 23:59:59')
     self.activity_member = ActivityMember.create(
         activity=self.activity,
         user=self.user,
         total_fee='0.01',
         nickname="leave activities",
         payment_method=TeamOrder.OrderPaymentMethod.WXPAY.value,
         payment_state=TeamOrder.OrderState.TRADE_BUYER_PAID,
     )
     TeamMemberService.new_order(team=self.team,
                                 activity_id=self.activity.id,
                                 user=self.user,
                                 order_type=TeamOrder.OrderType.ACTIVITY,
                                 payment_method='wxpay',
                                 total_fee=self.activity.price,
                                 payment_fee=self.activity.price,
                                 title='TestLeave')
     TeamOrder.update(state=TeamOrder.OrderState.TRADE_BUYER_PAID)\
         .where(TeamOrder.activity_id == self.activity.id,
                TeamOrder.user == self.user)\
         .execute()
    def _finish_order(self, order: TeamOrder, arguments: dict,
                      payment: PaymentMethod):
        """
        完成支付流程
        Args:
            order: TeamOrder
            arguments: dict, 微信服务器回调的 body
            payment: PaymentMethod, 支付方式

        Returns:

        """

        logging.debug('开始终结支付订单[{0}]'.format(order.order_no))
        if payment == PaymentMethod.ALIPAY:
            # 支付宝
            gateway_account = arguments["buyer_email"]
            gateway_trade_no = arguments["trade_no"]
        else:
            # 默认违心支付
            gateway_trade_no = arguments['transaction_id']
            gateway_account = arguments['openid']

        TeamOrder.update(state=TeamOrder.OrderState.TRADE_BUYER_PAID,
                         paid=datetime.datetime.now(),
                         gateway_trade_no=gateway_trade_no,
                         gateway_account=gateway_account) \
            .where(TeamOrder.order_no == order.order_no) \
            .execute()
        ActivityMember \
            .update(payment_method=payment.value,
                    payment_state=TeamOrder.OrderState.TRADE_BUYER_PAID,
                    state=ActivityMember.ActivityMemberState.confirmed,
                    paid=datetime.datetime.now(),
                    confirmed=datetime.datetime.now()) \
            .where(ActivityMember.activity == order.activity_id) \
            .execute()

        # 更新活动成员数
        Activity.update_members_count(order.activity_id)
    def leave(cls,
              user_id,
              match: Match,
              notify_url: str = None,
              insists=False,
              role: int = 1):
        """
        退出赛事

        :param user_id:
        :param match:
        :param notify_url: 派队回调地址
        :param insists: 强制退出
        :param role: 退赛发起人, 1 用户, 2 赛事方
        :return:
        """
        logging.debug("notify_url: {0}, insists: {1}".format(
            notify_url, insists))

        if not insists and notify_url is None:
            raise AssertionError("非强制退出 `insists=False` 操作需要提供退款回调"
                                 "地址 `notify_url`")

        if insists is False and not match.can_leave():
            raise MatchException("赛事无法退出")

        # 退出赛事
        member = MatchMember.get(user_id=user_id,
                                 match_id=match.id)  # type: MatchMember
        with Match._meta.database.transaction():
            if insists:
                match.leave(member)

            # Warning: 数据库事物中尝试远程 HTTP 调用, 需要修复
            else:
                # match.leave_request(member)
                # 调用退款接口
                pt = Parteam(app.settings["parteam_api_url"])
                if member.order_id:
                    # 有支付信息, 调用退款接口
                    order = TeamOrder.get(
                        id=member.order_id)  # type: TeamOrder
                    refund_fee = int(order.payment_fee * 100)
                    try:
                        pt.order_refund(user_id=user_id,
                                        order_no=member.pt_order_no,
                                        refund_fee=refund_fee,
                                        notify_url=notify_url,
                                        role=role)
                    except NotPaidError as e:
                        logging.warning("调用派队退款接口发现订单未支付: {0}".format(str(e)))
                        TeamOrder.update(state=TeamOrder.OrderState
                                         .TRADE_CLOSED_BY_USER.value) \
                            .where(TeamOrder.id == member.order_id) \
                            .execute()
                        match.leave(member)
                    except RefundError as e:
                        raise MatchException(e)
                    else:
                        # 更新订单状态为 `退款`, 订单退款状态为 `全部退款`
                        TeamOrder.update(
                            state=TeamOrder.OrderState.TRADE_CLOSED.value,
                            refund_state=TeamOrder.OrderRefundState.FULL_REFUNDED.value,
                            refunded_time=datetime.now())\
                            .where(TeamOrder.id == member.order_id)\
                            .execute()
                        match.leave(member)
                else:
                    # 无支付信息直接退赛
                    match.leave(member)
def refund(order_no, refund_fee, is_retry=False):
    """
    Args:
        order_no: int, 订单号
        refund_fee: 退款金额
    """

    wxpay = WxPay(appid=current_app.settings["wxpay_appid"],
                  mch_id=current_app.settings["wxpay_mchid"],
                  secret_key=current_app.settings["wxpay_secret_key"],
                  ca_certs=current_app.settings["wxpay_ca_certs"],
                  client_cert=current_app.settings["wxpay_api_client_cert"],
                  client_key=current_app.settings["wxpay_api_client_key"])

    order = TeamOrder.get(order_no=order_no)
    if order.payment_method == order.OrderPaymentMethod.WXPAY.value and \
            order.refund_state in (
                TeamOrder.OrderRefundState.PARTIAL_REFUNDING.value,
                TeamOrder.OrderRefundState.PARTIAL_REFUNDED.value,
                TeamOrder.OrderRefundState.FULL_REFUNDING.value,
                TeamOrder.OrderRefundState.FULL_REFUNDED.value
            ):

        if is_retry:
            out_trade_no = 'J%s' % order.order_no
        else:
            out_trade_no = 'N%s' % order.order_no

        try:
            response = wxpay.refund(out_trade_no=out_trade_no,
                                    out_refund_no="R%s" % order.order_no,
                                    total_fee=order.total_fee,
                                    refund_fee=refund_fee,
                                    # transaction_id=order.gateway_trade_no,
                                    op_user_id=order.gateway_account)
        except Exception as e:
            logging.error("refund fail, order_no:{0} exception:{1}".format(out_trade_no, e))
            response = None

        if response and response["result_code"] == "SUCCESS":
            logging.debug(response)
            TeamOrder.update(
                refund_state=TeamOrder.OrderRefundState.FULL_REFUNDED.value,
                refunded_fee=refund_fee,
                refunded_time=datetime.datetime.now(),
                state=TeamOrder.OrderState.TRADE_CLOSED,
            )\
                .where(TeamOrder.order_no == order.order_no)\
                .execute()

        # 换个订单号重试
        elif not is_retry:
            refund(order_no, refund_fee, is_retry=True)
        else:
            # 标记退款失败
            TeamOrder.update(
                refund_state=TeamOrder.OrderRefundState.FULL_REFUND_FAILED.value,
            ).where(TeamOrder.order_no == order.order_no)\
                .execute()

            logging.error("refund fail,order_no:{0} response:{1}".format(order_no, response))
def finish_activity(activity_id):
    """结算活动场次

        1. 将用户在线支付费用转到俱乐部账户
        2. 标记场次和订单状态为完成
        3. 记录俱乐部账户变化
    """

    activity = Activity.get_or_404(id=activity_id)

    if activity.end_time > datetime.now():
        raise Exception("活动场次未结束")

    if activity.state == Activity.ActivityState.cancelled:
        raise Exception("活动场次已取消")

    if activity.state == Activity.ActivityState.finished:
        raise Exception("活动场次已结算")

    # 计算在线支付完成交易的总额
    online_paid_amount = TeamOrder.select(fn.SUM(TeamOrder.payment_fee)).where(
        TeamOrder.activity_id == activity_id,
        TeamOrder.state >= TeamOrder.OrderState.TRADE_BUYER_PAID.value,
        TeamOrder.refund_state == TeamOrder.OrderRefundState.NO_REFUND,
        TeamOrder.payment_method <<
        TeamOrder.ONLINE_PAYMENT_METHODS).scalar() or 0

    # 计算余额支付完成交易的总额
    credit_paid_amount = TeamOrder.select(fn.SUM(TeamOrder.credit_fee)).where(
        TeamOrder.activity_id == activity_id, TeamOrder.state >=
        TeamOrder.OrderState.TRADE_BUYER_PAID.value).scalar() or 0

    # 使用次数
    free_times_amount = ActivityMember.select(fn.SUM(
        ActivityMember.free_times)).where(
            ActivityMember.state ==
            ActivityMember.ActivityMemberState.confirmed).scalar() or 0

    # online_paid_amount= DecimalField(default=Decimal(0), verbose_name="在线支付收入")
    # credit_paid_amount= DecimalField(default=Decimal(0), verbose_name="余额支付收入")
    # cash_paid_amount= DecimalField(default=Decimal(0), verbose_name="现金支付收入")
    # free_times_amount = IntegerField(default=0, verbose_name="次卡支付数量")

    with app.db.transaction() as txn:
        team = Team.select().where(
            Team.id == activity.team.id).for_update().get()

        # 将收入打到俱乐部账上
        Team.update(
            credit=Team.credit + online_paid_amount,
            total_receipts=Team.total_receipts + online_paid_amount,
            updated=datetime.now()).where(Team.id == team.id).execute()

        # 将订单修改状态为已完成
        TeamOrder.update(
            state=TeamOrder.OrderState.TRADE_FINISHED.value,
            finished=datetime.now()).where(
                TeamOrder.activity_id == activity_id,
                TeamOrder.state == TeamOrder.OrderState.TRADE_BUYER_PAID.value,
                TeamOrder.refund_state ==
                TeamOrder.OrderRefundState.NO_REFUND.value).execute()

        # 修改场次状态为已结算
        Activity.update(state=Activity.ActivityState.finished,
                        finished=datetime.now(),
                        online_paid_amount=online_paid_amount,
                        credit_paid_amount=credit_paid_amount,
                        free_times_amount=free_times_amount).where(
                            Activity.id == activity_id).execute()

        # 记录俱乐部账户变更
        TeamAccountLog.create(team_id=team.id,
                              credit_change=online_paid_amount,
                              change_type=0,
                              credit_before=team.credit,
                              credit_after=team.credit + online_paid_amount,
                              note="活动结算:%s(%s)" %
                              (activity.title, activity.start_time),
                              activity_id=activity_id,
                              operator_id=0)

    # 生成下期活动
    gen_next_activity(activity_id)