def test_cash_pay(self):
        activity = Activity.create(team=self.team,
                                   creator=self.creator,
                                   price='10',
                                   vip_price='8',
                                   leader=self.creator,
                                   payment_type=Activity.PaymentType.CASH_PAY,
                                   title='no online pay',
                                   description='on need online pay',
                                   start_time='3000-01-01 00:00:01',
                                   end_time='3000-12-31 23:59:59',
                                   join_end="4000-01-01 22:59:59",
                                   max_members=10)

        url = self.JOIN_ACTIVITY.format(activity_id=activity.id)
        body = {
            "payment": TeamOrder.OrderPaymentMethod.WXPAY.value,
            "nickname": "Nick name"
        }
        self.auth_user = User.create(name='no need online pay')
        response = self.fetch(url,
                              method='POST',
                              body=json.dumps(body),
                              params={'team_id': self.team.id})
        result = json.loads(response.body.decode())
        expect = {
            "status": "ok",
            "state": ActivityMember.ActivityMemberState.confirmed.value,
            "payment_state": TeamOrder.OrderState.WAIT_BUYER_PAY.value,
            "order_no": ""
        }
        self.assertDictEqual(expect, result, result)

        self.assertEqual(200, response.code, response.body)
        with self.assertRaises(TeamOrder.DoesNotExist):
            TeamOrder.get(
                user=self.auth_user,
                activity_id=activity.id,
            )
        member = ActivityMember.get(activity=activity, user=self.auth_user)

        # 现在支付, 直接确认
        self.assertEqual(ActivityMember.ActivityMemberState.confirmed.value,
                         member.state, member.state)
    def test_cash_free_activity(self):
        """
        参加免费活动不需要支付
        线下支付活动的 ActivityMember.state 应该为 `确认` confirmed
        """

        activity = self.create_free_activity(
            payment_type=Activity.PaymentType.CASH_PAY.value)

        url = self.JOIN_ACTIVITY.format(activity_id=activity.id)
        body = {
            "payment": TeamOrder.OrderPaymentMethod.WXPAY.value,
            "nickname": "Nick name"
        }

        self.auth_user = User.create(name='free activity')
        response = self.fetch(url,
                              method='POST',
                              body=json.dumps(body),
                              params={'team_id': self.team.id})
        self.assertEqual(200, response.code, response.body)
        result = json.loads(response.body.decode())
        expect = {
            "status": "ok",
            "state": ActivityMember.ActivityMemberState.confirmed.value,
            "payment_state": TeamOrder.OrderState.TRADE_BUYER_PAID.value,
            "order_no": ""
        }
        self.assertDictEqual(expect, result, result)

        with self.assertRaises(TeamOrder.DoesNotExist):
            TeamOrder.get(user=self.auth_user, activity_id=activity.id)

        member = ActivityMember.get(activity=activity, user=self.auth_user)
        self.assertEqual(ActivityMember.ActivityMemberState.confirmed.value,
                         member.state, member.state)
    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))