예제 #1
0
    def test_event_checkout_session_completed_positive(self):
        # links:
        #   https://stripe.com/docs/webhooks/signatures
        #   https://stripe.com/docs/api/events/object

        # given
        product = PRODUCTS["club1"]
        opened_payment: Payment = Payment.create(reference=f"random-reference-{uuid.uuid4()}",
                                                 user=self.existed_user,
                                                 product=product)

        strip_secret = "stripe_secret"
        with self.settings(STRIPE_WEBHOOK_SECRET=strip_secret):
            json_event = self.read_json_event('checkout.session.completed')
            json_event['data']['object']['id'] = opened_payment.reference

            timestamp = int(time.time())
            signed_payload = f"{timestamp}.{json.dumps(json_event)}"
            computed_signature = WebhookSignature._compute_signature(signed_payload, strip_secret)

            # when
            header = {'HTTP_STRIPE_SIGNATURE': f't={timestamp},v1={computed_signature}'}
            response = self.client.post(reverse("stripe_webhook"), data=json_event,
                                        content_type='application/json', **header)

            # then
            self.assertEqual(response.status_code, 200)
            # subscription prolongated
            user = User.objects.get(id=self.existed_user.id)
            self.assertAlmostEquals(user.membership_expires_at,
                                    self.existed_user.membership_expires_at + product['data']['timedelta'],
                                    delta=timedelta(seconds=10))
예제 #2
0
    def test_club_subscription_activator_positive_membership_expires_in_future(
            self):
        # given
        future_membership_expiration = datetime.utcnow() + timedelta(days=5)
        existed_user: User = User.objects.create(
            email="*****@*****.**",
            membership_started_at=datetime.utcnow() - timedelta(days=5),
            membership_expires_at=future_membership_expiration,
        )
        new_payment: Payment = Payment.create(
            reference=f"random-reference-{uuid.uuid4()}",
            user=existed_user,
            product=PRODUCTS["club1"])

        # when
        result = products.club_subscription_activator(
            product=PRODUCTS["club1_recurrent_yearly"],
            payment=new_payment,
            user=existed_user)

        # then
        self.assertTrue(result)

        user = User.objects.get(id=existed_user.id)
        self.assertAlmostEquals(user.membership_expires_at,
                                future_membership_expiration +
                                timedelta(days=365),
                                delta=timedelta(seconds=10))
        self.assertEqual(user.membership_platform_type,
                         User.MEMBERSHIP_PLATFORM_DIRECT)
        self.assertEqual(user.membership_platform_data, {
            "reference": new_payment.reference,
            "recurrent": "yearly"
        })
예제 #3
0
def stripe_webhook(request):
    payload = request.body
    sig_header = request.META.get("HTTP_STRIPE_SIGNATURE")

    if not payload or not sig_header:
        return HttpResponse("[invalid payload]", status=400)

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
        )
    except ValueError:
        return HttpResponse("[invalid payload]", status=400)
    except stripe.error.SignatureVerificationError:
        return HttpResponse("[invalid signature]", status=400)

    log.info("Stripe webhook event: " + event["type"])

    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]
        try:
            payment = Payment.finish(
                reference=session["id"],
                status=Payment.STATUS_SUCCESS,
                data=session,
            )
        except PaymentException:
            return HttpResponse("[payment not found]", status=400)

        product = PRODUCTS[payment.product_code]
        product["activator"](product, payment, payment.user)
        return HttpResponse("[ok]", status=200)

    if event["type"] == "invoice.paid":
        invoice = event["data"]["object"]
        if invoice["billing_reason"] == "subscription_create":
            # already processed in "checkout.session.completed" event
            return HttpResponse("[ok]", status=200)

        user = User.objects.filter(stripe_id=invoice["customer"]).first()
        # todo: do we need throw error in case user not found?

        payment = Payment.create(
            reference=invoice["id"],
            user=user,
            product=find_by_stripe_id(invoice["lines"]["data"][0]["plan"]["id"]),
            data=invoice,
            status=Payment.STATUS_SUCCESS,
        )
        product = PRODUCTS[payment.product_code]
        product["activator"](product, payment, user)
        return HttpResponse("[ok]", status=200)

    if event["type"] in {"customer.created", "customer.updated"}:
        customer = event["data"]["object"]
        User.objects.filter(email=customer["email"]).update(stripe_id=customer["id"])
        return HttpResponse("[ok]", status=200)

    return HttpResponse("[unknown event]", status=400)
예제 #4
0
    def setUpTestData(cls):
        # Set up data for the whole TestCase
        cls.existed_user: User = User.objects.create(
            email="*****@*****.**",
            membership_started_at=datetime.now() - timedelta(days=5),
            membership_expires_at=datetime.now() + timedelta(days=5),
        )

        cls.existed_payment: Payment = Payment.create(
            reference=f"random-reference-{uuid.uuid4()}",
            user=cls.existed_user,
            product=PRODUCTS["club1"],
            data={"fake-prop": 1},
            status=Payment.STATUS_STARTED)
예제 #5
0
    def test_not_club_member(self):
        existed_user: User = User.objects.create(
            email="*****@*****.**",
            membership_started_at=datetime.utcnow() - timedelta(days=5),
            membership_expires_at=datetime.utcnow() + timedelta(days=5),
            moderation_status=User.MODERATION_STATUS_INTRO,
            slug="ujlbu4",
        )
        existed_payment: Payment = Payment.create(reference=f"random-reference-{uuid.uuid4()}",
                                                  user=existed_user,
                                                  product=PRODUCTS["club1"])

        response = self.client.get(reverse('done'), data={'reference': existed_payment.reference})
        self.assertContains(response=response, text="Теперь у вас есть аккаунт в Клубе", status_code=200)
예제 #6
0
    def test_create_payment_positive(self):
        # when
        payment: Payment = Payment.create(
            reference=f"random-reference-{uuid.uuid4()}",
            user=self.existed_user,
            product=PRODUCTS["club1"],
            data={"fake-prop": 1},
            status=Payment.STATUS_STARTED)

        # then
        self.assertIsNotNone(payment)
        self.assertIsNotNone(payment.id)
        self.assertTrue(
            Payment.objects.filter(pk=payment.id).exists(),
            "The only payment I expect is here, persisted, and correct.")
예제 #7
0
def pay(request):
    product_code = request.GET.get("product_code")
    is_recurrent = request.GET.get("is_recurrent")
    if is_recurrent:
        interval = request.GET.get("recurrent_interval") or "yearly"
        product_code = f"{product_code}_recurrent_{interval}"

    product = PRODUCTS.get(product_code)

    if not product:
        return render(
            request, "error.html", {
                "title":
                "Не выбран пакет 😣",
                "message":
                "Выберите что вы хотите купить или насколько пополнить свою карту"
            })

    # user authorized or we need to create a new one?
    if request.me:
        user = request.me
    else:
        email = request.GET.get("email") or ""
        if not email or "@" not in email:
            return render(
                request, "error.html", {
                    "title":
                    "Плохой e-mail адрес 😣",
                    "message":
                    "Нам ведь нужно будет как-то привязать ваш аккаунт к платежу"
                })

        email = email.lower()
        now = datetime.utcnow()
        user, _ = User.objects.get_or_create(
            email=email,
            defaults=dict(
                membership_platform_type=User.MEMBERSHIP_PLATFORM_DIRECT,
                full_name=email[:email.find("@")],
                membership_started_at=now,
                membership_expires_at=now + timedelta(days=1),
                created_at=now,
                updated_at=now,
                moderation_status=User.MODERATION_STATUS_INTRO,
            ),
        )

    if user.stripe_id:
        customer_data = dict(customer=user.stripe_id)
    else:
        customer_data = dict(customer_email=user.email)

    session = stripe.checkout.Session.create(
        payment_method_types=["card"],
        line_items=[{
            "price": product["stripe_id"],
            "quantity": 1,
        }],
        **customer_data,
        mode="subscription" if is_recurrent else "payment",
        success_url=settings.STRIPE_SUCCESS_URL,
        cancel_url=settings.STRIPE_CANCEL_URL,
    )

    payment = Payment.create(session.id, user, product)

    return render(request, "payments/pay.html", {
        "session": session,
        "product": product,
        "payment": payment,
        "user": user,
    })
예제 #8
0
def pay(request):
    product_code = request.GET.get("product_code")
    is_invite = request.GET.get("is_invite")
    is_recurrent = request.GET.get("is_recurrent")
    if is_recurrent:
        interval = request.GET.get("recurrent_interval") or "yearly"
        product_code = f"{product_code}_recurrent_{interval}"

    # find product by code
    product = PRODUCTS.get(product_code)
    if not product:
        return render(
            request, "error.html", {
                "title":
                "Не выбран пакет 😣",
                "message":
                "Выберите что вы хотите купить или насколько пополнить свою карту"
            })

    payment_data = {}
    now = datetime.utcnow()

    # parse email
    email = request.GET.get("email") or ""
    if email:
        email = email.lower()

    # who's paying?
    if not request.me:  # scenario 1: new user
        if not email or "@" not in email:
            return render(
                request, "error.html", {
                    "title":
                    "Плохой e-mail адрес 😣",
                    "message":
                    "Нам ведь нужно будет как-то привязать аккаунт к платежу"
                })

        user, _ = User.objects.get_or_create(
            email=email,
            defaults=dict(
                membership_platform_type=User.MEMBERSHIP_PLATFORM_DIRECT,
                full_name=email[:email.find("@")],
                membership_started_at=now,
                membership_expires_at=now,
                created_at=now,
                updated_at=now,
                moderation_status=User.MODERATION_STATUS_INTRO,
            ),
        )
    elif is_invite:  # scenario 2: invite a friend
        if not email or "@" not in email:
            return render(
                request, "error.html", {
                    "title": "Плохой e-mail адрес друга 😣",
                    "message": "Нам ведь нужно будет куда-то выслать инвайт"
                })

        friend, is_created = User.objects.get_or_create(
            email=email,
            defaults=dict(
                membership_platform_type=User.MEMBERSHIP_PLATFORM_DIRECT,
                full_name=email[:email.find("@")],
                membership_started_at=now,
                membership_expires_at=now,
                created_at=now,
                updated_at=now,
                moderation_status=User.MODERATION_STATUS_INTRO,
            ),
        )

        if not is_created:
            return render(
                request, "error.html", {
                    "title":
                    "Пользователь уже существует ✋",
                    "message":
                    "Юзер с таким имейлом уже есть в Клубе, "
                    "нельзя высылать ему инвайт еще раз, может он правда не хочет."
                })

        user = request.me
        payment_data = {"invite": email}
    else:  # scenario 3: account renewal
        user = request.me

    # reuse stripe customer ID if user already has it
    if user.stripe_id:
        customer_data = dict(customer=user.stripe_id)
    else:
        customer_data = dict(customer_email=user.email)

    # create stripe session and payment (to keep track of history)
    session = stripe.checkout.Session.create(
        payment_method_types=["card"],
        line_items=[{
            "price": product["stripe_id"],
            "quantity": 1,
            "tax_rates": [TAX_RATE_VAT] if TAX_RATE_VAT else [],
        }],
        **customer_data,
        mode="subscription" if is_recurrent else "payment",
        metadata=payment_data,
        success_url=settings.STRIPE_SUCCESS_URL,
        cancel_url=settings.STRIPE_CANCEL_URL,
    )

    payment = Payment.create(
        reference=session.id,
        user=user,
        product=product,
        data=payment_data,
    )

    return render(request, "payments/pay.html", {
        "session": session,
        "product": product,
        "payment": payment,
        "user": user,
    })
예제 #9
0
def coinbase_webhook(request):
    payload = request.body
    webhook_signature = request.META.get("HTTP_X_CC_WEBHOOK_SIGNATURE")
    if not payload or not webhook_signature:
        return HttpResponse("[invalid payload]", status=400)

    # verify webhook signature
    payload_signature = hmac.new(
        key=bytes(settings.COINBASE_WEBHOOK_SECRET, "utf-8"),
        msg=payload,  # it's already in bytes
        digestmod=hashlib.sha256
    ).hexdigest()
    if payload_signature.upper() != webhook_signature.upper():
        return HttpResponse("[bad signature]", status=400)

    # load event data
    try:
        data = json.loads(payload)
    except json.JSONDecodeError:
        return HttpResponse("[payload is not json]", status=400)

    event = data.get("event")
    event_type = event.get("type")
    event_data = event.get("data")
    event_code = event_data.get("code")
    if not event or not event_type or not event_data or not event_code:
        return HttpResponse("[bad payload structure]", status=400)

    log.info(f"Coinbase webhook event: {event_type} ({event_code})")

    # find or create the user
    metadata_email = event_data.get("metadata", {}).get("email")
    if not metadata_email:
        return HttpResponse("[no email in payload]", status=400)

    now = datetime.utcnow()
    user, _ = User.objects.get_or_create(
        email=metadata_email,
        defaults=dict(
            membership_platform_type=User.MEMBERSHIP_PLATFORM_CRYPTO,
            full_name=metadata_email[:metadata_email.find("@")],
            membership_started_at=now,
            membership_expires_at=now,
            created_at=now,
            updated_at=now,
            moderation_status=User.MODERATION_STATUS_INTRO,
        ),
    )

    # find product
    checkout_id = event_data.get("checkout", {}).get("id")
    product = find_by_coinbase_id(checkout_id)
    if not checkout_id or not product:
        return HttpResponse("[product not found]", status=404)

    # make actions for event_types
    if event_type == "charge:created":
        Payment.create(
            reference=event_code,
            user=user,
            product=product,
            data=event,
            status=Payment.STATUS_STARTED,
        )
        return HttpResponse("[ok]", status=200)

    elif event_type == "charge:confirmed":
        try:
            payment = Payment.finish(
                reference=event_code,
                status=Payment.STATUS_SUCCESS,
                data=event,
            )
        except PaymentNotFound:
            payment = Payment.create(
                reference=event_code,
                user=user,
                product=product,
                data=event,
                status=Payment.STATUS_SUCCESS,
            )
        except PaymentAlreadyFinalized:
            return HttpResponse("[duplicate payment]", status=400)

        product["activator"](product, payment, user)
        return HttpResponse("[ok]", status=200)

    elif event_type == "charge:failed":
        Payment.finish(
            reference=event_code,
            status=Payment.STATUS_FAILED,
            data=event,
        )
        return HttpResponse("[ok]", status=200)

    elif event_type == "charge:pending":
        return HttpResponse("[ok]", status=200)

    return HttpResponse("[unknown event]", status=400)
예제 #10
0
    def post(self, request, *args, **kwargs):
        data = request.data

        order_pk = data.get('order', None)
        user_pk = data.get('user', None)
        email = data.get('email', None)
        phone = data.get('phone', None).replace('+', '')
        name = data.get('name')

        user_created = False
        password = None

        order = None

        try:
            order = Order.objects.get(pk=order_pk)
        except ObjectDoesNotExist:
            return Response('Заказ не найден',
                            status=status.HTTP_404_NOT_FOUND)

        user = None
        if order.user is not None:
            if user_pk != order.user.id:
                return Response(
                    'Переданный ID пользователя и ID пользователя заказа не совпадают',
                    status=status.HTTP_400_BAD_REQUEST)
            else:
                try:
                    user = User.objects.get(id=user_pk)
                except ObjectDoesNotExist:
                    return Response('Пользователь с таким ID не найден')

        # 1. Есть заказ
        # 2. Нет юзера
        if user is None:
            try:
                user = User.objects.get(email=email)
            except ObjectDoesNotExist:
                pass

            # Всё еще не найден
            if user is None:
                password = User.objects.make_random_password()

                user = User(email=email, username=email, password=password)
                try:
                    user.full_clean()
                    user.set_password(password)
                    user.save()
                except ValidationError:
                    return Response('Недопустимые данные',
                                    status=status.HTTP_400_BAD_REQUEST)

                # 1. Есть заказ
                # 2. Есть новый юзер
                user_created = True
                order.user = user
                order.save()
            else:
                # 1. Есть заказ
                # 2. Есть найденный юзер
                order.user = user
                order.save()

        # 1. Есть заказ
        # 2. Есть переданный юзер
        else:
            pass

        return_url = 'https://presidentwatches.ru/u/profile/#payments'.format(
            uuid=order.uuid)
        description = 'Заказ №{public_id}'.format(public_id=order.public_id)

        receipt = {
            'phone': phone,
            'email': email,
            'items': [],
        }

        for item in order.cart['items'].values():
            descr = "{brand} {model}".format(brand=item['brand'],
                                             model=item['model'])
            result_price = round(item['total_price'] / item['quantity'])
            receipt['items'].append({
                'description': descr,
                'quantity': item['quantity'],
                'amount': {
                    'value': result_price,
                    'currency': 'RUB'
                },
                'vat_code': '1',
                'payment_mode': 'full_prepayment',
                'payment_subject': 'commodity'
            })

        try:
            delivery_price = order.delivery['price']
        except:
            delivery_price = None

        if delivery_price is not None:
            if delivery_price > 0:
                receipt['items'].append({
                    'description': 'Доставка',
                    'quantity': 1,
                    'amount': {
                        'value': delivery_price,
                        'currency': 'RUB'
                    },
                    'vat_code': '1',
                    'payment_mode': 'full_prepayment',
                    'payment_subject': 'commodity'
                })

        params = {
            'amount': {
                'value': order.total_price,
                'currency': 'RUB'
            },
            'confirmation': {
                'type': 'redirect',
                'return_url': return_url
            },
            'receipt': receipt,
            'capture': True,
            'description': description
        }

        payment = Payment.create(params, order=order, user=user)

        if user_created:
            notify_new_user.delay(payment.id, password=password)
        else:
            notify.delay(payment.id)

        serializer = self.serializer_class(payment)

        if user_created:
            # Если создали нового юзера - отправляем ссылку с авто-аутентификацией
            pass
        else:
            # Если не создавали - просто ссылку с редиректом на платежи.
            pass

        return Response({
            'payment': serializer.data,
            'user': user.id
        },
                        status=status.HTTP_201_CREATED)