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))
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" })
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)
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)
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)
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.")
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, })
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, })
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)
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)