{% blocktrans trimmed %}
3 | After you submitted your order, we will redirect you to the payment service provider to complete your payment.
4 | You will then be redirected back here to get your tickets.
5 | {% endblocktrans %}
{% blocktrans trimmed %}
5 | After you submitted your order, we will redirect you to the payment service provider to complete your payment.
6 | You will then be redirected back here to get your tickets.
7 | {% endblocktrans %}
13 | {% blocktrans trimmed %}
14 | After you submitted your order, we will redirect you to the payment service provider to complete your
15 | payment. You will then be redirected back here to get your tickets.
16 | {% endblocktrans %}
17 |
{% blocktrans trimmed %}
25 | After you submitted your order, we will redirect you to the payment service provider to complete your payment.
26 | You will then be redirected back here to get your tickets.
27 | {% endblocktrans %}
28 |
29 |
{% trans "Payment method" %}
30 |
{{ provider.public_name }}
31 | {% if provider.method == "bancontact" %}
32 |
18 | {% blocktrans trimmed %}
19 | In your online bank account or from an ATM, choose "Payment and other services".
20 | {% endblocktrans %}
21 |
22 | {% else %}
23 |
{% blocktrans trimmed %}
24 | We're waiting for an answer from the payment provider regarding your payment. Please contact us if this
25 | takes more than a few days.
26 | {% endblocktrans %}
{% blocktrans trimmed %}
30 | You need to confirm your payment. Please click the link below to do so or start a new payment.
31 | {% endblocktrans %}
32 |
{% blocktrans trimmed %}
42 | Please scan the barcode below to complete your WeChat payment.
43 | Once you have completed your payment, you can refresh this page.
44 | {% endblocktrans %}
45 |
46 |
47 |
48 | {% else %}
49 |
{% blocktrans trimmed %}
50 | The payment transaction could not be completed for the following reason:
51 | {% endblocktrans %}
52 |
53 | {% if payment_info and payment_info.error %}
54 | {{ payment_info.message }}
55 | {% else %}
56 | {% trans "Unknown reason" %}
57 | {% endif %}
58 |
63 | {% blocktrans trimmed %}
64 | Your payment will be processed by Stripe, Inc. Your credit card data will be transmitted directly to
65 | Stripe and never touches our servers.
66 | {% endblocktrans %}
67 |
68 |
69 |
71 |
73 |
74 |
50 | {% bootstrap_form form layout='horizontal' %}
51 |
52 |
53 |
54 | {% blocktrans trimmed with sepa_creditor_name=settings.sepa_creditor_name %}
55 | By providing your payment information and confirming this payment, you authorize (A)
56 | {{ sepa_creditor_name }} and Stripe, our payment service provider and/or PPRO, its local service provider,
57 | to send instructions to your bank to debit your account and (B) your bank to debit your account in
58 | accordance with those instructions. As part of your rights, you are entitled to a refund from your bank
59 | under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks
60 | starting from the date on which your account was debited. Your rights are explained in a statement that you
61 | can obtain from your bank. You agree to receive notifications for future debits up to 2 days before they
62 | occur.
63 | {% endblocktrans %}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/docs/Readme.md:
--------------------------------------------------------------------------------
1 | # Stripe Payment Settings Instruction
2 |
3 | This guide will walk you through obtaining the necessary keys to integrate Stripe with your application.
4 | To get started, you'll need an active Stripe merchant account. If you don't have one yet, you can sign up at [Stripe](http://stripe.com/).
5 |
6 | ---
7 |
8 | ## Step 1: Configure Stripe OAuth and Retrieve Client ID
9 |
10 | You can refer to the [Stripe documentation](https://docs.stripe.com/connect/oauth-standard-accounts#integrating-oauth) or follow the steps below:
11 |
12 | 1. Go to [Stripe Dashboard](https://dashboard.stripe.com/).
13 | 2. Select **Settings** in the upper-right corner of the Stripe dashboard.
14 | 3. In **Product settings** section, select the **Connect**.
15 |
16 | 
17 |
18 | 4. In **Onboard connected accounts** section, click **Onboarding options**.
19 |
20 | 
21 |
22 | 5. In the **OAuth** tab:
23 |
24 | 
25 |
26 | - Enable OAuth (if disabled).
27 | - Add the URI for the Stripe OAuth flow (e.g, ```https:///_stripe/oauth_return/```).
28 |
29 | 
30 |
31 | - Copy Client ID (Test client ID if you are in test mode).
32 |
33 | 
34 |
35 | ---
36 |
37 | ## Step 2: Retrieve Secret Key and Publishable Key
38 |
39 | You can also refer to the [Stripe documentation](https://docs.stripe.com/keys) or follow the steps below:
40 |
41 | 1. Go to [Stripe Dashboard](https://dashboard.stripe.com/).
42 | 2. Log in with your credentials.
43 | 3. In the Dashboard, navigate to:
44 | - Click **Developers** in the left menu → Click **API keys**.
45 | 4. You will see two keys:
46 | - **Publishable Key**: Starts with `pk_`.
47 | - **Secret Key**: Starts with `sk_`.
48 |
49 | 
50 |
51 | 5. Click **Reveal test key** or **Reveal live key** to see the **Secret Key** (depending on your mode):
52 | - **Test Mode**: For development and testing purposes.
53 | - **Live Mode**: For production use.
54 |
55 | 6. Copy both keys and store them securely.
56 |
57 | ---
58 |
59 | ## Step 3: Retrieve Webhook Signing Secret
60 |
61 | You can also refer to the [Stripe documentation](https://docs.stripe.com/webhooks) or follow the steps below:
62 |
63 | 1. In the Dashboard, go to:
64 | - Click Developers in the left menu → Click [Webhooks](https://dashboard.stripe.com/webhooks).
65 | 2. Click **Create an event destination**.
66 |
67 | 
68 |
69 | 3. Choose the event types:
70 | - **Endpoint URL**: Enter the URL in your application that will handle the webhook (e.g., ```https://yourdomain.com/_stripe/webhook```).
71 | - **Description:** Add optional description of the destination.
72 | - **Listen to**: Select **Events on Connected accounts**.
73 | - **Select events to listen to**: Choose the following events:
74 | - `charge.succeeded`
75 | - `charge.failed`
76 | - `charge.refunded`
77 | - `charge.updated`
78 | - `charge.dispute.created`
79 | - `charge.dispute.updated`
80 | - `charge.dispute.closed`
81 | - `source.chargeable`
82 | - `source.failed`
83 | - `source.canceled`
84 | - `payment_intent.succeeded`
85 | - `payment_intent.payment_failed`
86 | - `payment_intent.canceled`
87 | - `payment_intent.processing`
88 | - Click **Add endpoint**.
89 |
90 | 
91 |
92 | 4. Retrieve the signing secret:
93 |
94 | 
95 |
96 | - Click **Reveal** under **Signing secret**.
97 |
98 | - Copy the **Signing secret** (starts with `whsec_`).
99 |
100 | ---
101 |
102 | ## Step 4: Configure Stripe Keys to Eventyay Global Settings
103 |
104 | 1. Log in to eventyay as an admin user.
105 | 2. Access to eventyay admin dashboard.
106 | 3. In the left menu, click to **Global settings**.
107 |
108 | 
109 |
110 | 4. Scroll to bottom of the page and fill in the Stripe configuration fields:
111 |
112 | 
113 |
114 | - **Stripe Connect: Client ID**: `[Client ID from Step 1]`
115 | - **Stripe Connect: Secret key**: `[Secret key from Step 2 (live mode)]`
116 | - **Stripe Connect: Publishable key**: `[Publishable key from Step 2 (live mode)]`
117 | - **Stripe Connect: Secret key (test)**: `[Secret key from Step 2 (test mode)]`
118 | - **Stripe Connect: Publishable key (test)**: `[Publishable key from Step 2 (test mode)]`
119 | - **Stripe Webhook: Signing secret**: `[Webhook signing secret from Step 3]`
120 | - Click **Save**.
121 |
122 | ---
123 |
124 | ### Notes
125 |
126 | 1. Ensure the Stripe payment plugin is installed for Stripe settings to appear in the Eventyay admin dashboard.
127 | 2. Certain Stripe payment methods must be enabled on [organizer’s Stripe account](https://dashboard.stripe.com/settings/payments) before configuring them in Eventyay.
128 | 3. If your Stripe dashboard appears different, you might have Workbench enabled under [Developer Settings](https://dashboard.stripe.com/settings/developers). Refer to the [Stripe documentation](https://docs.stripe.com/webhooks) for updated instructions to retrieve the webhook signing secret.
129 |
--------------------------------------------------------------------------------
/eventyay_stripe/signals.py:
--------------------------------------------------------------------------------
1 | import json
2 | from collections import OrderedDict
3 |
4 | from django import forms
5 | from django.dispatch import receiver
6 | from django.template.loader import get_template
7 | from django.urls import resolve, reverse
8 | from django.utils.translation import gettext_lazy as _
9 |
10 | from pretix.base.forms import SecretKeySettingsField
11 | from pretix.base.settings import settings_hierarkey
12 | from pretix.base.signals import (
13 | logentry_display, register_global_settings, register_payment_providers,
14 | )
15 | from pretix.control.signals import nav_organizer
16 | from pretix.presale.signals import html_head
17 |
18 | from .forms import StripeKeyValidator
19 |
20 |
21 | @receiver(register_payment_providers, dispatch_uid="payment_stripe")
22 | def register_payment_provider(sender, **kwargs):
23 | from .payment import (
24 | StripeAffirm, StripeAlipay, StripeBancontact, StripeCreditCard,
25 | StripeEPS, StripeIdeal, StripeKlarna, StripeMobilePay,
26 | StripeMultibanco, StripePayPal, StripePrzelewy24, StripeRevolutPay,
27 | StripeSEPADirectDebit, StripeSettingsHolder, StripeSofort, StripeSwish,
28 | StripeTwint, StripeWeChatPay,
29 | )
30 |
31 | return [
32 | StripeSettingsHolder, StripeCreditCard, StripeIdeal, StripeAlipay, StripeBancontact,
33 | StripeSofort, StripeEPS, StripeMultibanco, StripePrzelewy24, StripeWeChatPay,
34 | StripePayPal, StripeRevolutPay, StripeSEPADirectDebit, StripeSwish, StripeTwint,
35 | StripeMobilePay, StripeAffirm, StripeKlarna
36 | ]
37 |
38 |
39 | @receiver(html_head, dispatch_uid="payment_stripe_html_head")
40 | def html_head_presale(sender, request=None, **kwargs):
41 | from .payment import StripeSettingsHolder
42 |
43 | provider = StripeSettingsHolder(sender)
44 | url = resolve(request.path_info)
45 | if provider.settings.get('_enabled', as_type=bool) and ("checkout" in url.url_name or "order.pay" in url.url_name):
46 | template = get_template('plugins/stripe/presale_head.html')
47 | ctx = {
48 | 'event': sender,
49 | 'settings': provider.settings,
50 | 'testmode': (
51 | (provider.settings.get('endpoint', 'live') == 'test' or sender.testmode)
52 | and provider.settings.publishable_test_key
53 | )
54 | }
55 | return template.render(ctx)
56 | else:
57 | return ""
58 |
59 |
60 | @receiver(signal=logentry_display, dispatch_uid="stripe_logentry_display")
61 | def pretixcontrol_logentry_display(sender, logentry, **kwargs):
62 | if logentry.action_type != 'pretix.plugins.stripe.event':
63 | return
64 |
65 | data = json.loads(logentry.data)
66 | event_type = data.get('type')
67 | text = None
68 | plains = {
69 | 'charge.succeeded': _('Charge succeeded.'),
70 | 'charge.refunded': _('Charge refunded.'),
71 | 'charge.updated': _('Charge updated.'),
72 | 'charge.pending': _('Charge pending'),
73 | 'source.chargeable': _('Payment authorized.'),
74 | 'source.canceled': _('Payment authorization canceled.'),
75 | 'source.failed': _('Payment authorization failed.')
76 | }
77 |
78 | if event_type in plains:
79 | text = plains[event_type]
80 | elif event_type == 'charge.failed':
81 | text = _('Charge failed. Reason: {}').format(data['data']['object']['failure_message'])
82 | elif event_type == 'charge.dispute.created':
83 | text = _('Dispute created. Reason: {}').format(data['data']['object']['reason'])
84 | elif event_type == 'charge.dispute.updated':
85 | text = _('Dispute updated. Reason: {}').format(data['data']['object']['reason'])
86 | elif event_type == 'charge.dispute.closed':
87 | text = _('Dispute closed. Status: {}').format(data['data']['object']['status'])
88 |
89 | if text:
90 | return _('Stripe reported an event: {}').format(text)
91 |
92 |
93 | settings_hierarkey.add_default('payment_stripe_method_card', True, bool)
94 | settings_hierarkey.add_default('payment_stripe_reseller_moto', False, bool)
95 |
96 |
97 | @receiver(register_global_settings, dispatch_uid='stripe_global_settings')
98 | def register_global_settings(sender, **kwargs):
99 | return OrderedDict([
100 | ('payment_stripe_connect_client_id', forms.CharField(
101 | label=_('Stripe Connect: Client ID'),
102 | required=False,
103 | validators=(
104 | StripeKeyValidator('ca_'),
105 | ),
106 | )),
107 | ('payment_stripe_connect_secret_key', SecretKeySettingsField(
108 | label=_('Stripe Connect: Secret key'),
109 | required=False,
110 | validators=(
111 | StripeKeyValidator(['sk_live_', 'rk_live_']),
112 | ),
113 | )),
114 | ('payment_stripe_connect_publishable_key', forms.CharField(
115 | label=_('Stripe Connect: Publishable key'),
116 | required=False,
117 | validators=(
118 | StripeKeyValidator('pk_live_'),
119 | ),
120 | )),
121 | ('payment_stripe_connect_test_secret_key', SecretKeySettingsField(
122 | label=_('Stripe Connect: Secret key (test)'),
123 | required=False,
124 | validators=(
125 | StripeKeyValidator(['sk_test_', 'rk_test_']),
126 | ),
127 | )),
128 | ('payment_stripe_connect_test_publishable_key', forms.CharField(
129 | label=_('Stripe Connect: Publishable key (test)'),
130 | required=False,
131 | validators=(
132 | StripeKeyValidator('pk_test_'),
133 | ),
134 | )),
135 | ('payment_stripe_connect_app_fee_percent', forms.DecimalField(
136 | label=_('Stripe Connect: App fee (percent)'),
137 | required=False,
138 | )),
139 | ('payment_stripe_connect_app_fee_max', forms.DecimalField(
140 | label=_('Stripe Connect: App fee (max)'),
141 | required=False,
142 | )),
143 | ('payment_stripe_connect_app_fee_min', forms.DecimalField(
144 | label=_('Stripe Connect: App fee (min)'),
145 | required=False,
146 | )),
147 | ])
148 |
149 |
150 | @receiver(nav_organizer, dispatch_uid="stripe_nav_organizer")
151 | def nav_o(sender, request, organizer, **kwargs):
152 | if request.user.has_active_staff_session(request.session.session_key):
153 | url = resolve(request.path_info)
154 | return [{
155 | 'label': _('Stripe Connect'),
156 | 'url': reverse('plugins:eventyay_stripe:settings.connect', kwargs={
157 | 'organizer': request.organizer.slug
158 | }),
159 | 'parent': reverse('control:organizer.edit', kwargs={
160 | 'organizer': request.organizer.slug
161 | }),
162 | 'active': 'settings.connect' in url.url_name,
163 | }]
164 | return []
165 |
--------------------------------------------------------------------------------
/eventyay_stripe/tests/test_provider.py:
--------------------------------------------------------------------------------
1 | import json
2 | from datetime import timedelta
3 | from decimal import Decimal
4 |
5 | import pytest
6 | import stripe
7 | from django.test import RequestFactory
8 | from django.utils.timezone import now
9 | from django_scopes import scope
10 | from stripe.error import APIConnectionError, CardError
11 |
12 | from eventyay_stripe import __version__
13 | from eventyay_stripe.payment import StripeCreditCard
14 | from pretix.base.models import Event, Order, OrderRefund, Organizer
15 | from pretix.base.payment import PaymentException
16 |
17 |
18 | @pytest.fixture
19 | def env():
20 | o = Organizer.objects.create(name='Dummy', slug='dummy')
21 | with scope(organizer=o):
22 | event = Event.objects.create(
23 | organizer=o, name='Dummy', slug='dummy',
24 | date_from=now(), live=True
25 | )
26 | o1 = Order.objects.create(
27 | code='FOOBAR', event=event, email='dummy@dummy.test',
28 | status=Order.STATUS_PENDING,
29 | datetime=now(), expires=now() + timedelta(days=10),
30 | total=Decimal('13.37')
31 | )
32 | yield event, o1
33 |
34 |
35 | @pytest.fixture(autouse=True)
36 | def no_messages(monkeypatch):
37 | # Patch out template rendering for performance improvements
38 | monkeypatch.setattr("django.contrib.messages.api.add_message", lambda *args, **kwargs: None)
39 |
40 |
41 | @pytest.fixture
42 | def factory():
43 | return RequestFactory()
44 |
45 |
46 | class MockedRefunds():
47 | pass
48 |
49 |
50 | class MockedCharge():
51 | status = ''
52 | paid = False
53 | id = 'ch_123345345'
54 | refunds = MockedRefunds()
55 |
56 | def refresh(self):
57 | pass
58 |
59 |
60 | class Object():
61 | pass
62 |
63 |
64 | class MockedPaymentintent():
65 | status = ''
66 | id = 'pi_1EUon12Tb35ankTnZyvC3SdE'
67 | charges = Object()
68 | charges.data = [MockedCharge()]
69 | last_payment_error = None
70 |
71 |
72 | @pytest.mark.django_db
73 | def test_perform_success(env, factory, monkeypatch):
74 | event, order = env
75 |
76 | def paymentintent_create(**kwargs):
77 | assert kwargs['amount'] == 1337
78 | assert kwargs['currency'] == 'eur'
79 | assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu'
80 | c = MockedPaymentintent()
81 | c.status = 'succeeded'
82 | c.charges.data[0].paid = True
83 | return c
84 |
85 | monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create)
86 | prov = StripeCreditCard(event)
87 | prov.init_api()
88 |
89 | # Verify Stripe API version and app info configuration
90 | assert stripe.api_version == "2024-11-20.acacia"
91 | assert stripe.app_info == {
92 | 'name': 'eventyay-stripe',
93 | 'version': __version__,
94 | 'url': 'https://github.com/fossasia/eventyay-stripe'
95 | }
96 |
97 | req = factory.post('/', {
98 | 'stripe_payment_method_id': 'pm_189fTT2eZvKYlo2CvJKzEzeu',
99 | 'stripe_last4': '4242',
100 | 'stripe_brand': 'Visa'
101 | })
102 | req.session = {}
103 | prov.checkout_prepare(req, {})
104 | assert 'payment_stripe_payment_method_id' in req.session
105 | payment = order.payments.create(
106 | provider='stripe_cc', amount=order.total
107 | )
108 | prov.execute_payment(req, payment)
109 | order.refresh_from_db()
110 | assert order.status == Order.STATUS_PAID
111 |
112 |
113 | @pytest.mark.django_db
114 | def test_perform_success_zero_decimal_currency(env, factory, monkeypatch):
115 | event, order = env
116 | event.currency = 'JPY'
117 | event.save()
118 |
119 | def paymentintent_create(**kwargs):
120 | assert kwargs['amount'] == 13
121 | assert kwargs['currency'] == 'jpy'
122 | assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu'
123 | c = MockedPaymentintent()
124 | c.status = 'succeeded'
125 | c.charges.data[0].paid = True
126 | return c
127 |
128 | monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create)
129 | prov = StripeCreditCard(event)
130 | req = factory.post('/', {
131 | 'stripe_payment_method_id': 'pm_189fTT2eZvKYlo2CvJKzEzeu',
132 | 'stripe_last4': '4242',
133 | 'stripe_brand': 'Visa'
134 | })
135 | req.session = {}
136 | prov.checkout_prepare(req, {})
137 | assert 'payment_stripe_payment_method_id' in req.session
138 | payment = order.payments.create(
139 | provider='stripe_cc', amount=order.total
140 | )
141 | prov.execute_payment(req, payment)
142 | order.refresh_from_db()
143 | assert order.status == Order.STATUS_PAID
144 |
145 |
146 | @pytest.mark.django_db
147 | def test_perform_card_error(env, factory, monkeypatch):
148 | event, order = env
149 |
150 | def paymentintent_create(**kwargs):
151 | raise CardError(message='Foo', param='foo', code=100)
152 |
153 | monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create)
154 | prov = StripeCreditCard(event)
155 | req = factory.post('/', {
156 | 'stripe_payment_method_id': 'pm_189fTT2eZvKYlo2CvJKzEzeu',
157 | 'stripe_last4': '4242',
158 | 'stripe_brand': 'Visa'
159 | })
160 | req.session = {}
161 | prov.checkout_prepare(req, {})
162 | assert 'payment_stripe_payment_method_id' in req.session
163 | with pytest.raises(PaymentException):
164 | payment = order.payments.create(
165 | provider='stripe_cc', amount=order.total
166 | )
167 | prov.execute_payment(req, payment)
168 | order.refresh_from_db()
169 | assert order.status == Order.STATUS_PENDING
170 |
171 |
172 | @pytest.mark.django_db
173 | def test_perform_stripe_error(env, factory, monkeypatch):
174 | event, order = env
175 |
176 | def paymentintent_create(**kwargs):
177 | raise CardError(message='Foo', param='foo', code=100)
178 |
179 | monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create)
180 | prov = StripeCreditCard(event)
181 | req = factory.post('/', {
182 | 'stripe_payment_method_id': 'pm_189fTT2eZvKYlo2CvJKzEzeu',
183 | 'stripe_last4': '4242',
184 | 'stripe_brand': 'Visa'
185 | })
186 | req.session = {}
187 | prov.checkout_prepare(req, {})
188 | assert 'payment_stripe_payment_method_id' in req.session
189 | with pytest.raises(PaymentException):
190 | payment = order.payments.create(
191 | provider='stripe_cc', amount=order.total
192 | )
193 | prov.execute_payment(req, payment)
194 | order.refresh_from_db()
195 | assert order.status == Order.STATUS_PENDING
196 |
197 |
198 | @pytest.mark.django_db
199 | def test_perform_failed(env, factory, monkeypatch):
200 | event, order = env
201 |
202 | def paymentintent_create(**kwargs):
203 | assert kwargs['amount'] == 1337
204 | assert kwargs['currency'] == 'eur'
205 | assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu'
206 | c = MockedPaymentintent()
207 | c.status = 'failed'
208 | c.failure_message = 'Foo'
209 | c.charges.data[0].paid = True
210 | c.last_payment_error = Object()
211 | c.last_payment_error.message = "Foo"
212 | return c
213 |
214 | monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create)
215 | prov = StripeCreditCard(event)
216 | req = factory.post('/', {
217 | 'stripe_payment_method_id': 'pm_189fTT2eZvKYlo2CvJKzEzeu',
218 | 'stripe_last4': '4242',
219 | 'stripe_brand': 'Visa'
220 | })
221 | req.session = {}
222 | prov.checkout_prepare(req, {})
223 | assert 'payment_stripe_payment_method_id' in req.session
224 | with pytest.raises(PaymentException):
225 | payment = order.payments.create(
226 | provider='stripe_cc', amount=order.total
227 | )
228 | prov.execute_payment(req, payment)
229 | order.refresh_from_db()
230 | assert order.status == Order.STATUS_PENDING
231 |
232 |
233 | @pytest.mark.django_db
234 | def test_refund_success(env, factory, monkeypatch):
235 | event, order = env
236 |
237 | def charge_retr(*args, **kwargs):
238 | def refund_create(amount):
239 | r = MockedCharge()
240 | r.id = 'foo'
241 | r.status = 'succeeded'
242 | return r
243 |
244 | c = MockedCharge()
245 | c.refunds.create = refund_create
246 | return c
247 |
248 | monkeypatch.setattr("stripe.Charge.retrieve", charge_retr)
249 | order.status = Order.STATUS_PAID
250 | p = order.payments.create(provider='stripe_cc', amount=order.total, info=json.dumps({
251 | 'id': 'ch_123345345'
252 | }))
253 | order.save()
254 | prov = StripeCreditCard(event)
255 | refund = order.refunds.create(
256 | provider='stripe_cc', amount=order.total, payment=p,
257 | )
258 | prov.execute_refund(refund)
259 | refund.refresh_from_db()
260 | assert refund.state == OrderRefund.REFUND_STATE_DONE
261 |
262 |
263 | @pytest.mark.django_db
264 | def test_refund_unavailable(env, factory, monkeypatch):
265 | event, order = env
266 |
267 | def charge_retr(*args, **kwargs):
268 | def refund_create(amount):
269 | raise APIConnectionError(message='Foo')
270 |
271 | c = MockedCharge()
272 | c.refunds.create = refund_create
273 | return c
274 |
275 | monkeypatch.setattr("stripe.Charge.retrieve", charge_retr)
276 | order.status = Order.STATUS_PAID
277 | p = order.payments.create(provider='stripe_cc', amount=order.total, info=json.dumps({
278 | 'id': 'ch_123345345'
279 | }))
280 | order.save()
281 | prov = StripeCreditCard(event)
282 | refund = order.refunds.create(
283 | provider='stripe_cc', amount=order.total, payment=p
284 | )
285 | with pytest.raises(PaymentException):
286 | prov.execute_refund(refund)
287 | refund.refresh_from_db()
288 | assert refund.state != OrderRefund.REFUND_STATE_DONE
289 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/eventyay_stripe/tests/test_webhook.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import hmac
3 | import json
4 | import time
5 | from datetime import timedelta
6 | from decimal import Decimal
7 | from unittest import mock
8 |
9 | import pytest
10 | import stripe
11 | from django.test import RequestFactory
12 | from django.utils.timezone import now
13 | from django_scopes import scopes_disabled
14 |
15 | from eventyay_stripe.models import ReferencedStripeObject
16 | from eventyay_stripe.views import GlobalSettingsObject, webhook
17 | from pretix.base.models import (
18 | Event, Order, OrderPayment, OrderRefund, Organizer, Team, User,
19 | )
20 |
21 |
22 | @pytest.fixture
23 | def env():
24 | user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
25 | o = Organizer.objects.create(name='Dummy', slug='dummy')
26 | event = Event.objects.create(
27 | organizer=o, name='Dummy', slug='dummy', plugins='eventyay_stripe',
28 | date_from=now(), live=True
29 | )
30 | t = Team.objects.create(organizer=event.organizer, can_view_orders=True, can_change_orders=True)
31 | t.members.add(user)
32 | t.limit_events.add(event)
33 | o1 = Order.objects.create(
34 | code='FOOBAR', event=event, email='dummy@dummy.test',
35 | status=Order.STATUS_PAID,
36 | datetime=now(), expires=now() + timedelta(days=10),
37 | total=Decimal('13.37'),
38 | )
39 | return event, o1
40 |
41 |
42 | def generate_signature(payload, secret, timestamp=None):
43 | """Generate a valid Stripe webhook signature for testing."""
44 | timestamp = timestamp or int(time.time())
45 | signed_payload = f"{timestamp}.{payload}"
46 | signature = hmac.new(
47 | secret.encode("utf-8"),
48 | signed_payload.encode("utf-8"),
49 | hashlib.sha256
50 | ).hexdigest()
51 | return f"t={timestamp},v1={signature}"
52 |
53 |
54 | def get_test_charge(order: Order):
55 | return {
56 | "id": "ch_18TY6GGGWE2Is8TZHanef25",
57 | "object": "charge",
58 | "amount": 1337,
59 | "amount_refunded": 1000,
60 | "application_fee": None,
61 | "balance_transaction": "txn_18TY6GGGWE2Ias8TkwY6o51W",
62 | "captured": True,
63 | "created": 1467642664,
64 | "currency": "eur",
65 | "customer": None,
66 | "description": None,
67 | "destination": None,
68 | "dispute": None,
69 | "failure_code": None,
70 | "failure_message": None,
71 | "fraud_details": {},
72 | "invoice": None,
73 | "livemode": False,
74 | "metadata": {
75 | "code": order.code,
76 | "order": str(order.pk),
77 | "event": str(order.event.pk),
78 | },
79 | "order": None,
80 | "paid": True,
81 | "receipt_email": None,
82 | "receipt_number": None,
83 | "refunded": False,
84 | "refunds": {
85 | "object": "list",
86 | "data": [],
87 | "total_count": 0
88 | },
89 | "shipping": None,
90 | "source": {
91 | "id": "card_18TY5wGGWE2Ias8Td38PjyPy",
92 | "object": "card",
93 | "address_city": None,
94 | "address_country": None,
95 | "address_line1": None,
96 | "address_line1_check": None,
97 | "address_line2": None,
98 | "address_state": None,
99 | "address_zip": None,
100 | "address_zip_check": None,
101 | "brand": "Visa",
102 | "country": "US",
103 | "customer": None,
104 | "cvc_check": "pass",
105 | "dynamic_last4": None,
106 | "exp_month": 12,
107 | "exp_year": 2016,
108 | "fingerprint": "FNbGTMaFvhRU2Y0E",
109 | "funding": "credit",
110 | "last4": "4242",
111 | "metadata": {},
112 | "name": "Carl Cardholder",
113 | "tokenization_method": None,
114 | },
115 | "source_transfer": None,
116 | "statement_descriptor": None,
117 | "status": "succeeded"
118 | }
119 |
120 |
121 | @pytest.mark.django_db
122 | def test_webhook_all_good(env, client, monkeypatch):
123 | charge = get_test_charge(env[1])
124 | monkeypatch.setattr("stripe.Charge.retrieve", lambda *args, **kwargs: charge)
125 |
126 | client.post('/dummy/dummy/stripe/webhook/', json.dumps(
127 | {
128 | "id": "evt_18otImGGWE2Ias8TUyVRDB1G",
129 | "object": "event",
130 | "api_version": "2016-03-07",
131 | "created": 1472729052,
132 | "data": {
133 | "object": {
134 | "id": "ch_18TY6GGGWE2Ias8TZHanef25",
135 | "object": "charge",
136 | # Rest of object is ignored anway
137 | }
138 | },
139 | "livemode": True,
140 | "pending_webhooks": 1,
141 | "request": "req_977XOWC8zk51Z9",
142 | "type": "charge.refunded"
143 | }
144 | ), content_type='application_json')
145 |
146 | order = env[1]
147 | order.refresh_from_db()
148 | assert order.status == Order.STATUS_PAID
149 |
150 |
151 | @pytest.mark.django_db
152 | def test_webhook_mark_paid(env, client, monkeypatch):
153 | order = env[1]
154 | order.status = Order.STATUS_PENDING
155 | order.save()
156 | charge = get_test_charge(env[1])
157 | charge["amount_refunded"] = 0
158 | with scopes_disabled():
159 | payment = env[1].payments.create(
160 | provider='stripe', amount=env[1].total, info='{}', state=OrderPayment.PAYMENT_STATE_CREATED,
161 | )
162 | ReferencedStripeObject.objects.create(
163 | order=order,
164 | payment=payment,
165 | reference="pi_1",
166 | )
167 |
168 | monkeypatch.setattr("stripe.Charge.retrieve", lambda *args, **kwargs: charge)
169 |
170 | client.post('/dummy/dummy/stripe/webhook/', json.dumps(
171 | {
172 | "id": "evt_18otImGGWE2Ias8TUyVRDB1G",
173 | "object": "event",
174 | "api_version": "2016-03-07",
175 | "created": 1472729052,
176 | "data": {
177 | "object": {
178 | "object": "charge",
179 | "id": "pi_1",
180 | "amount": 2000,
181 | "currency": "usd",
182 | "status": "succeeded",
183 | "livemode": True,
184 | "pending_webhooks": 1,
185 | "request": "req_977XOWC8zk51Z9",
186 | "type": "charge.succeeded"
187 | }
188 | },
189 | "livemode": True,
190 | "pending_webhooks": 1,
191 | "request": "req_977XOWC8zk51Z9",
192 | "type": "payment_intent.succeeded"
193 | }
194 | ), content_type='application_json')
195 |
196 | order.refresh_from_db()
197 | assert order.status == Order.STATUS_PENDING
198 |
199 |
200 | @pytest.mark.django_db
201 | def test_webhook_partial_refund(env, client, monkeypatch):
202 | charge = get_test_charge(env[1])
203 |
204 | with scopes_disabled():
205 | payment = env[1].payments.create(
206 | provider='stripe', amount=env[1].total, info=json.dumps(charge)
207 | )
208 | ReferencedStripeObject.objects.create(order=env[1], reference="ch_18TY6GGGWE2Ias8TZHanef25",
209 | payment=payment)
210 |
211 | charge['refunds'] = {
212 | "object": "list",
213 | "data": [
214 | {
215 | "id": "re_18otImGGWE2Ias8TY0QvwKYQ",
216 | "object": "refund",
217 | "amount": "12300",
218 | "balance_transaction": "txn_18otImGGWE2Ias8T4fLOxesC",
219 | "charge": "ch_18TY6GGGWE2Ias8TZHanef25",
220 | "created": 1472729052,
221 | "currency": "eur",
222 | "metadata": {},
223 | "reason": None,
224 | "receipt_number": None,
225 | "status": "succeeded"
226 | }
227 | ],
228 | "total_count": 1
229 | }
230 | monkeypatch.setattr("stripe.Charge.retrieve", lambda *args, **kwargs: charge)
231 |
232 | payload = json.dumps(
233 | {
234 | "id": "evt_18otImGGWE2Ias8TUyVRDB1G",
235 | "object": "event",
236 | "api_version": "2016-03-07",
237 | "created": 1472729052,
238 | "data": {
239 | "object": {
240 | "id": "ch_18TY6GGGWE2Ias8TZHanef25",
241 | "object": "charge",
242 | # Rest of object is ignored anway
243 | }
244 | },
245 | "livemode": True,
246 | "pending_webhooks": 1,
247 | "request": "req_977XOWC8zk51Z9",
248 | "type": "charge.refunded"
249 | }
250 | )
251 |
252 | gs = GlobalSettingsObject()
253 | gs.settings.set('payment_stripe_webhook_secret', 'whsec_123')
254 | gs.settings.set('payment_stripe_connect_test_secret_key', 'sk_test_123')
255 |
256 | sig_header = generate_signature(payload, "whsec_123")
257 | client.post(
258 | '/dummy/dummy/stripe/webhook/',
259 | payload,
260 | content_type='application_json',
261 | HTTP_STRIPE_SIGNATURE=sig_header
262 | )
263 |
264 | order = env[1]
265 | order.refresh_from_db()
266 | assert order.status == Order.STATUS_PAID
267 |
268 | with scopes_disabled():
269 | ra = order.refunds.first()
270 | assert ra.state == OrderRefund.REFUND_STATE_EXTERNAL
271 | assert ra.source == 'external'
272 | assert ra.amount == Decimal('123.00')
273 |
274 |
275 | @pytest.mark.django_db
276 | def test_webhook_global(env, client, monkeypatch):
277 | order = env[1]
278 | order.status = Order.STATUS_PENDING
279 | order.save()
280 |
281 | charge = get_test_charge(env[1])
282 | charge["amount_refunded"] = 0
283 | monkeypatch.setattr("stripe.Charge.retrieve", lambda *args, **kwargs: charge)
284 |
285 | with scopes_disabled():
286 | payment = order.payments.create(
287 | provider='stripe', amount=order.total, info=json.dumps(charge), state=OrderPayment.PAYMENT_STATE_CREATED
288 | )
289 | ReferencedStripeObject.objects.create(order=order, reference="ch_18TY6GGGWE2Ias8TZHanef25",
290 | payment=payment)
291 | ReferencedStripeObject.objects.create(order=order, reference="pi_123456",
292 | payment=payment)
293 |
294 | payload = json.dumps(
295 | {
296 | "id": "evt_18otImGGWE2Ias8TUyVRDB1G",
297 | "object": "event",
298 | "api_version": "2016-03-07",
299 | "created": 1472729052,
300 | "data": {
301 | "object": {
302 | "id": "ch_18TY6GGGWE2Ias8TZHanef25",
303 | "object": "charge",
304 | "payment_intent": "pi_123456",
305 | "metadata": {
306 | "event": order.event_id,
307 | }
308 | }
309 | },
310 | "livemode": True,
311 | "pending_webhooks": 1,
312 | "request": "req_977XOWC8zk51Z9",
313 | "type": "payment_intent.succeeded"
314 | }
315 | )
316 | gs = GlobalSettingsObject()
317 | gs.settings.set('payment_stripe_webhook_secret', 'whsec_123')
318 | gs.settings.set('payment_stripe_connect_test_secret_key', 'sk_test_123')
319 |
320 | sig_header = generate_signature(payload, "whsec_123")
321 | response = client.post(
322 | '/_stripe/webhook/',
323 | payload,
324 | content_type='application_json',
325 | HTTP_STRIPE_SIGNATURE=sig_header
326 | )
327 | assert response.status_code == 200
328 |
329 | order.refresh_from_db()
330 | assert order.status == Order.STATUS_PAID
331 |
332 |
333 | @pytest.mark.django_db
334 | def test_webhook_global_legacy_reference(env, client, monkeypatch):
335 | order = env[1]
336 | order.status = Order.STATUS_PENDING
337 | order.save()
338 |
339 | charge = get_test_charge(env[1])
340 | charge["amount_refunded"] = 0
341 | monkeypatch.setattr("stripe.Charge.retrieve", lambda *args, **kwargs: charge)
342 |
343 | with scopes_disabled():
344 | payment = order.payments.create(
345 | provider='stripe', amount=order.total, info=json.dumps(charge), state=OrderPayment.PAYMENT_STATE_CREATED
346 | )
347 | ReferencedStripeObject.objects.create(order=order, reference="ch_18TY6GGGWE2Ias8TZHanef25")
348 | ReferencedStripeObject.objects.create(order=order, reference="pi_123456")
349 |
350 | payload = json.dumps(
351 | {
352 | "id": "evt_18otImGGWE2Ias8TUyVRDB1G",
353 | "object": "event",
354 | "api_version": "2016-03-07",
355 | "created": 1472729052,
356 | "data": {
357 | "object": {
358 | "id": "ch_18TY6GGGWE2Ias8TZHanef25",
359 | "object": "charge",
360 | "payment_intent": "pi_123456",
361 | "metadata": {
362 | "event": order.event_id,
363 | }
364 | }
365 | },
366 | "livemode": True,
367 | "pending_webhooks": 1,
368 | "request": "req_977XOWC8zk51Z9",
369 | "type": "payment_intent.succeeded"
370 | }
371 | )
372 | gs = GlobalSettingsObject()
373 | gs.settings.set('payment_stripe_webhook_secret', 'whsec_123')
374 | gs.settings.set('payment_stripe_connect_test_secret_key', 'sk_test_123')
375 | sig_header = generate_signature(payload, "whsec_123")
376 |
377 | response = client.post('/_stripe/webhook/', payload, content_type='application_json', HTTP_STRIPE_SIGNATURE=sig_header)
378 | assert response.status_code == 200
379 |
380 | order.refresh_from_db()
381 | assert order.status == Order.STATUS_PAID
382 | with scopes_disabled():
383 | assert list(order.payments.all()) == [payment]
384 |
385 |
386 | @pytest.fixture
387 | def factory():
388 | return RequestFactory()
389 |
390 |
391 | @pytest.fixture
392 | def mock_global_settings():
393 | with mock.patch('eventyay_stripe.views.GlobalSettingsObject') as MockGlobalSettings:
394 | instance = MockGlobalSettings.return_value
395 | instance.settings.payment_stripe_connect_secret_key = 'sk_test_123'
396 | instance.settings.payment_stripe_connect_test_secret_key = 'sk_test_123'
397 | instance.settings.payment_stripe_webhook_secret = 'whsec_123'
398 | yield instance
399 |
400 |
401 | @pytest.fixture
402 | def valid_payload():
403 | return json.dumps({
404 | "id": "evt_1",
405 | "object": "event",
406 | "type": "payment_intent.succeeded",
407 | "data": {
408 | "object": {
409 | "object": "charge",
410 | "id": "pi_1",
411 | "amount": 2000,
412 | "currency": "usd",
413 | "status": "succeeded",
414 | "livemode": True,
415 | "pending_webhooks": 1,
416 | "request": "req_977XOWC8zk51Z9",
417 | "type": "charge.succeeded"
418 | }
419 | }
420 | })
421 |
422 |
423 | @pytest.mark.django_db
424 | def test_webhook_invalid_payload(factory, mock_global_settings):
425 | invalid_payload = "invalid_payload"
426 | request = factory.post('/dummy/dummy/stripe/webhook', data=invalid_payload, content_type='application/json')
427 | with mock.patch('stripe.Webhook.construct_event', side_effect=ValueError("Invalid JSON")):
428 | response = webhook(request)
429 | assert response.status_code == 400
430 | assert response.content == b"Invalid payload"
431 |
432 |
433 | @pytest.mark.django_db
434 | def test_webhook_invalid_signature(factory, valid_payload, mock_global_settings):
435 | request = factory.post('/_stripe/webhook', data=valid_payload, content_type='application/json')
436 | request.META['HTTP_STRIPE_SIGNATURE'] = 'invalid_signature'
437 |
438 | with mock.patch('stripe.Webhook.construct_event', side_effect=stripe.error.SignatureVerificationError("Invalid signature", 'sig_123')):
439 | response = webhook(request)
440 | assert response.status_code == 400
441 | assert response.content == b"Invalid Stripe signature"
442 |
443 |
444 | @pytest.mark.django_db
445 | def test_webhook_success(factory, valid_payload, mock_global_settings):
446 | request = factory.post('/_stripe/webhook', data=valid_payload, content_type='application/json')
447 | request.META['HTTP_STRIPE_SIGNATURE'] = 'valid_signature'
448 |
449 | with mock.patch('stripe.Webhook.construct_event', return_value={"type": "payment_intent.succeeded"}):
450 | response = webhook(request)
451 | assert response.status_code == 200
452 |
453 |
454 | @pytest.mark.django_db
455 | def test_webhook_refund(factory, valid_payload, mock_global_settings):
456 | request = factory.post('/_stripe/webhook', data=valid_payload, content_type='application/json')
457 | request.META['HTTP_STRIPE_SIGNATURE'] = 'valid_signature'
458 |
459 | with mock.patch('stripe.Webhook.construct_event', return_value={"type": "payment_intent.succeeded"}):
460 | response = webhook(request)
461 | assert response.status_code == 200
462 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | requires-python = ">=3.9"
3 |
4 | [[package]]
5 | name = "certifi"
6 | version = "2025.1.31"
7 | source = { registry = "https://pypi.org/simple" }
8 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
9 | wheels = [
10 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
11 | ]
12 |
13 | [[package]]
14 | name = "charset-normalizer"
15 | version = "3.4.1"
16 | source = { registry = "https://pypi.org/simple" }
17 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
18 | wheels = [
19 | { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 },
20 | { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 },
21 | { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 },
22 | { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 },
23 | { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 },
24 | { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 },
25 | { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 },
26 | { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 },
27 | { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 },
28 | { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 },
29 | { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 },
30 | { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 },
31 | { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 },
32 | { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
33 | { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
34 | { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
35 | { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
36 | { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
37 | { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
38 | { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
39 | { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
40 | { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
41 | { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
42 | { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
43 | { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
44 | { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
45 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
46 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
47 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
48 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
49 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
50 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
51 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
52 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
53 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
54 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
55 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
56 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
57 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
58 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
59 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
60 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
61 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
62 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
63 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
64 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
65 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
66 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
67 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
68 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
69 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
70 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
71 | { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 },
72 | { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 },
73 | { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 },
74 | { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 },
75 | { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 },
76 | { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 },
77 | { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 },
78 | { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 },
79 | { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 },
80 | { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 },
81 | { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 },
82 | { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 },
83 | { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 },
84 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
85 | ]
86 |
87 | [[package]]
88 | name = "eventyay-stripe"
89 | version = "1.0.1"
90 | source = { editable = "." }
91 | dependencies = [
92 | { name = "stripe" },
93 | ]
94 |
95 | [package.metadata]
96 | requires-dist = [{ name = "stripe", specifier = "==11.3.*" }]
97 |
98 | [[package]]
99 | name = "idna"
100 | version = "3.10"
101 | source = { registry = "https://pypi.org/simple" }
102 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
103 | wheels = [
104 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
105 | ]
106 |
107 | [[package]]
108 | name = "requests"
109 | version = "2.32.3"
110 | source = { registry = "https://pypi.org/simple" }
111 | dependencies = [
112 | { name = "certifi" },
113 | { name = "charset-normalizer" },
114 | { name = "idna" },
115 | { name = "urllib3" },
116 | ]
117 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
118 | wheels = [
119 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
120 | ]
121 |
122 | [[package]]
123 | name = "stripe"
124 | version = "11.3.0"
125 | source = { registry = "https://pypi.org/simple" }
126 | dependencies = [
127 | { name = "requests" },
128 | { name = "typing-extensions" },
129 | ]
130 | sdist = { url = "https://files.pythonhosted.org/packages/0d/96/358f9f62960826ee77de447d70e7ed09741dd5d53d684be1371913e51d7f/stripe-11.3.0.tar.gz", hash = "sha256:98e625d9ddbabcecf02666867169696e113d9eaba27979fb310a7a8dfd44097c", size = 1367031 }
131 | wheels = [
132 | { url = "https://files.pythonhosted.org/packages/a4/85/2f4365a0112e49ff646bc701b93e8c4eb314812d84149a43898ea88d377d/stripe-11.3.0-py2.py3-none-any.whl", hash = "sha256:9d2e86943e1e4f325835d3860c4f58aa98d49229c9caf67588f9f9b2451e8e56", size = 1617363 },
133 | ]
134 |
135 | [[package]]
136 | name = "typing-extensions"
137 | version = "4.13.2"
138 | source = { registry = "https://pypi.org/simple" }
139 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
140 | wheels = [
141 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
142 | ]
143 |
144 | [[package]]
145 | name = "urllib3"
146 | version = "2.4.0"
147 | source = { registry = "https://pypi.org/simple" }
148 | sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
149 | wheels = [
150 | { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
151 | ]
152 |
--------------------------------------------------------------------------------
/eventyay_stripe/static/plugins/stripe/eventyay-stripe.js:
--------------------------------------------------------------------------------
1 | /*global $, stripe_pubkey, stripe_loadingmessage, gettext */
2 | 'use strict';
3 |
4 | var stripeObj = {
5 | stripe: null,
6 | elements: null,
7 | card: null,
8 | paymentRequest: null,
9 | paymentRequestButton: null,
10 |
11 | 'cc_request': function () {
12 | waitingDialog.show(gettext("Contacting Stripe …"));
13 | $(".stripe-errors").hide();
14 |
15 | // ToDo: 'card' --> proper type of payment method
16 | stripeObj.stripe.createPaymentMethod('card', stripeObj.card).then(function (result) {
17 | waitingDialog.hide();
18 | if (result.error) {
19 | $(".stripe-errors").stop().hide().removeClass("sr-only");
20 | $(".stripe-errors").html("
" + result.error.message + "
");
21 | $(".stripe-errors").slideDown();
22 | } else {
23 | let $form = $("#stripe_payment_method_id").closest("form");
24 | // Insert the token into the form so it gets submitted to the server
25 | $("#stripe_payment_method_id").val(result.paymentMethod.id);
26 | $("#stripe_card_brand").val(result.paymentMethod.card.brand);
27 | $("#stripe_card_last4").val(result.paymentMethod.card.last4);
28 | // and submit
29 | $form.get(0).submit();
30 | }
31 | });
32 | },
33 | 'pm_request': function (method, element, kwargs = {}) {
34 | waitingDialog.show(gettext("Contacting Stripe …"));
35 | $(".stripe-errors").hide();
36 |
37 | stripeObj.stripe.createPaymentMethod(method, element, kwargs).then(function (result) {
38 | waitingDialog.hide();
39 | if (result.error) {
40 | $(".stripe-errors").stop().hide().removeClass("sr-only");
41 | $(".stripe-errors").html("
" + result.error.message + "
");
42 | $(".stripe-errors").slideDown();
43 | } else {
44 | var $form = $("#stripe_" + method + "_payment_method_id").closest("form");
45 | // Insert the token into the form so it gets submitted to the server
46 | $("#stripe_" + method + "_payment_method_id").val(result.paymentMethod.id);
47 | if (method === 'card') {
48 | $("#stripe_card_brand").val(result.paymentMethod.card.brand);
49 | $("#stripe_card_last4").val(result.paymentMethod.card.last4);
50 | }
51 | if (method === 'sepa_debit') {
52 | $("#stripe_sepa_debit_last4").val(result.paymentMethod.sepa_debit.last4);
53 | }
54 | // and submit
55 | $form.get(0).submit();
56 | }
57 | }).catch((e) => {
58 | waitingDialog.hide();
59 | $(".stripe-errors").stop().hide().removeClass("sr-only");
60 | $(".stripe-errors").html("
Technical error, please contact support: " + e + "