Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement schedule task to collect billing invoice #417

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
33c32e1
Implement schedule task to collect billing invoice
Oct 29, 2024
fb65f1f
consider sourcery-ai and fix isort
Oct 29, 2024
75a08f6
Implement billing settings form for organizer
odkhang Oct 31, 2024
b0d7932
Resolve conflict
odkhang Oct 31, 2024
9b499db
Fix flake8 in pipeline
odkhang Oct 31, 2024
a17173a
implement create and save payment_information
odkhang Oct 21, 2024
055b2d7
Fix isort, flake8 in pipeline
odkhang Oct 31, 2024
b7dbc11
Merge branch 'development' into feat-383-implement-schedule-task
Oct 31, 2024
045bfe4
implement payment-information-v1
odkhang Oct 24, 2024
e7510d2
src/pretix/control/views/organizer_views/organizer_view.py
odkhang Oct 31, 2024
0c4b5f6
Code refactoring
odkhang Oct 28, 2024
f596f95
Code refactoring
odkhang Oct 28, 2024
ede87ff
Remove payment_information attribute
odkhang Oct 28, 2024
eb22165
Implement tax validation
odkhang Oct 30, 2024
8cf14e5
Update code
odkhang Oct 30, 2024
25f70a9
Update code
odkhang Oct 30, 2024
abc5731
Fix flake8 in pipeline
odkhang Oct 30, 2024
dbf24b2
Add pyvat package
odkhang Oct 30, 2024
7b1fe49
Fix flake8 in pipeline
odkhang Oct 31, 2024
27b71a6
Latest code
odkhang Oct 31, 2024
063d2a6
Fix flake8 in pipeline
odkhang Oct 31, 2024
c4b9ce5
Add logger error
odkhang Oct 31, 2024
7133019
Fix flake8 in pipeline
odkhang Oct 31, 2024
891d3e8
implement trigger invoice to organizer
Nov 1, 2024
723ce29
Merge remote-tracking branch 'upstream/feat-383-implement-schedule-ta…
odkhang Nov 1, 2024
9a5929c
Merge branch 'development' into feature-380
odkhang Nov 1, 2024
35f3376
Merge branch 'development' into feature-380
odkhang Nov 1, 2024
3d4249a
Fix conflict pretix/base/migration
odkhang Nov 1, 2024
072ef08
Merge branch 'feature-380' of github.com:odkhang/eventyay-tickets int…
odkhang Nov 1, 2024
edd7d64
Implement auto billing charge
odkhang Nov 1, 2024
97fbc9c
Merge branch 'development' into feature-380
odkhang Nov 1, 2024
8b5f87e
Fix conflict pretix base migration
odkhang Nov 1, 2024
17064b1
Update pretix base migration
odkhang Nov 1, 2024
db4f7b2
Add logging information and modify error logging
odkhang Nov 4, 2024
fa91edd
Merge branch 'feature-380' into feature-380-v3
odkhang Nov 4, 2024
d7d0ea9
Merge remote-tracking branch 'upstream/feature-380' into feat-383-imp…
Nov 4, 2024
3fb85e2
Implement auto billing charge
odkhang Nov 4, 2024
022a8fb
update schedule task
odkhang Nov 5, 2024
5fdbe3d
Merge branch 'development' into feat-383-implement-schedule-task
odkhang Nov 5, 2024
c2c8524
fix isort, flake8 and update branch
odkhang Nov 5, 2024
c6f5fdd
fix isort pipeline
lcduong Nov 5, 2024
582f94b
Merge remote-tracking branch 'upstream/feature-380-v3' into feat-383-…
lcduong Nov 5, 2024
f70b561
Implement automatic payment charging
odkhang Nov 6, 2024
1d20c45
update invoice template
lcduong Nov 6, 2024
2b932f5
Merge remote-tracking branch 'upstream/feature-380-v3' into feat-383-…
lcduong Nov 6, 2024
b0faf51
change var name, format code
lcduong Nov 6, 2024
bd8dfc4
Implement automatic payment charging
odkhang Nov 6, 2024
9922347
Merge remote-tracking branch 'upstream/feature-383' into feat-383-imp…
lcduong Nov 6, 2024
3a1118b
Add comment
odkhang Nov 6, 2024
26e10d4
Merge remote-tracking branch 'upstream/feature-383' into feat-383-imp…
lcduong Nov 6, 2024
7605c38
Implement show error message
odkhang Nov 6, 2024
7abf71c
Merge remote-tracking branch 'upstream/feature-383' into feat-383-imp…
lcduong Nov 6, 2024
e19de5c
handle case update invoice to expired
lcduong Nov 6, 2024
67be7eb
fix isort, flake8 pipeline
lcduong Nov 6, 2024
ffcf3f5
Update api to trigger billing task for testing
lcduong Nov 6, 2024
3b19d22
Implement stripe webhook secret key in global setting
odkhang Nov 7, 2024
5d5e638
Add comment,save tax_id, show error and sucess message
odkhang Nov 7, 2024
9afbad4
Merge remote-tracking branch 'upstream/feature-383' into feat-383-imp…
lcduong Nov 8, 2024
8f48821
Merge remote-tracking branch 'upstream/feature-380' into feat-383-imp…
lcduong Nov 8, 2024
e555acd
handle case get mail backend for global settings
lcduong Nov 8, 2024
0673121
fix isort pipeline
lcduong Nov 8, 2024
4eee5a7
fix sending custom mail smtp
lcduong Nov 8, 2024
62f7bf7
tickets fee should be calculated based on net amount
lcduong Nov 8, 2024
b04b50f
remove unsed import to fix pipeline
lcduong Nov 8, 2024
2e7cfd3
correct import to fix isort
lcduong Nov 8, 2024
13b2944
Update code
odkhang Nov 13, 2024
38154c7
Fix flake8 in pipeline
odkhang Nov 13, 2024
ea52541
Merge branch 'development' into feature-380
odkhang Nov 13, 2024
8512f8c
Update code
odkhang Nov 14, 2024
4aa0e3c
Update code
odkhang Nov 14, 2024
e41650b
Update code
odkhang Nov 14, 2024
aa86746
fix flake8 in pipeline
odkhang Nov 14, 2024
f76094a
move validation to clean method and move get_country_name to countrie…
odkhang Nov 15, 2024
ddde2c6
Merge remote-tracking branch 'upstream/feature-380' into feat-383-imp…
lcduong Nov 15, 2024
80e1fe1
Merge branch 'development' into feat-383-implement-schedule-task
lcduong Nov 18, 2024
5ed3a49
formar code
lcduong Nov 18, 2024
14a7b09
Merge branch 'development' into feat-383-implement-schedule-task
odkhang Nov 18, 2024
4cc2bb8
Merge branch 'development' into feat-383-implement-schedule-task
lcduong Nov 19, 2024
fe0627b
merge migration file
lcduong Nov 19, 2024
1ef5f46
fix isort
lcduong Nov 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions deployment/docker/pretix.bash
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ if [ "$1" == "taskworker" ]; then
exec celery -A pretix.celery_app worker -l info "$@"
fi

if [ "$1" == "taskbeat" ]; then
shift
exec celery -A pretix.celery_app beat -l info "$@"
fi

if [ "$1" == "upgrade" ]; then
exec python3 -m pretix updatestyles
fi
Expand Down
10 changes: 10 additions & 0 deletions deployment/docker/supervisord/pretixbeat.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[program:pretixtask]
command=/usr/local/bin/pretix taskbeat
autostart=true
autorestart=true
priority=5
user=pretixuser
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
4 changes: 4 additions & 0 deletions src/pretix/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

from pretix.api.views import cart

from ..eventyay_common.views.billing import BillingInvoicePreview
from .views import (
checkin, device, event, exporters, item, oauth, order, organizer, upload,
user, version, voucher, waitinglist, webhooks,
)
from .views.stripe import stripe_webhook_view

router = routers.DefaultRouter()
router.register(r'organizers', organizer.OrganizerViewSet)
Expand Down Expand Up @@ -98,6 +100,8 @@
url(r"^upload$", upload.UploadView.as_view(), name="upload"),
url(r"^me$", user.MeView.as_view(), name="user.me"),
url(r"^version$", version.VersionView.as_view(), name="version"),
url(r"^billing-testing/(?P<task>[^/]+)", BillingInvoicePreview.as_view(), name="billing-testing"),
url(r'^webhook/stripe$', stripe_webhook_view, name='stripe-webhook'),
url(r"(?P<organizer>[^/]+)/(?P<event>[^/]+)/schedule-public", event.talk_schedule_public,
name="event.schedule-public"),
url(r"(?P<organizer>[^/]+)/(?P<event>[^/]+)/ticket-check", event.CustomerOrderCheckView.as_view(),
Expand Down
37 changes: 37 additions & 0 deletions src/pretix/api/views/stripe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logging

import stripe
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

from pretix.eventyay_common.tasks import update_billing_invoice_information
from pretix.helpers.stripe_utils import (
get_stripe_secret_key, get_stripe_webhook_secret_key,
)

logger = logging.getLogger(__name__)


@csrf_exempt
def stripe_webhook_view(request):
stripe.api_key = get_stripe_secret_key()
payload = request.body
webhook_secret_key = get_stripe_webhook_secret_key()
sig_header = request.META['HTTP_STRIPE_SIGNATURE']

try:
event = stripe.Webhook.construct_event(
payload, sig_header, webhook_secret_key
)
except ValueError as e:
logger.error("Error parsing payload: %s", str(e))
return HttpResponse("Invalid payload", status=400)
except stripe.error.SignatureVerificationError as e:
logger.error("Error verifying webhook signature: %s", str(e))
return HttpResponse("Invalid signature", status=400)

if event.type == 'payment_intent.succeeded':
invoice_id = event.data.object.get('metadata', {}).get('invoice_id')
update_billing_invoice_information.delay(invoice_id=invoice_id)

return HttpResponse("Success", status=200)
78 changes: 78 additions & 0 deletions src/pretix/base/migrations/0004_create_billing_invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Generated by Django 5.1.3 on 2024-11-19 04:50

import datetime

import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models

import pretix.base.models.base


class Migration(migrations.Migration):

dependencies = [
(
"pretixbase",
"0003_alter_cachedcombinedticket_id_alter_cachedticket_id_and_more",
),
]

operations = [
migrations.CreateModel(
name="BillingInvoice",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("status", models.CharField(default="n", max_length=1)),
("amount", models.DecimalField(decimal_places=2, max_digits=10)),
("currency", models.CharField(max_length=3)),
("ticket_fee", models.DecimalField(decimal_places=2, max_digits=10)),
("payment_method", models.CharField(max_length=20, null=True)),
("paid_datetime", models.DateTimeField(blank=True, null=True)),
("note", models.TextField(null=True)),
("monthly_bill", models.DateField(default=datetime.date.today)),
("created_at", models.DateTimeField(auto_now_add=True)),
("created_by", models.CharField(max_length=50)),
("updated_at", models.DateTimeField(auto_now=True)),
("updated_by", models.CharField(max_length=50)),
("last_reminder_datetime", models.DateTimeField(blank=True, null=True)),
("next_reminder_datetime", models.DateTimeField(blank=True, null=True)),
(
"reminder_schedule",
django.contrib.postgres.fields.ArrayField(
base_field=models.IntegerField(), default=list, size=None
),
),
("reminder_enabled", models.BooleanField(default=True)),
(
"stripe_payment_intent_id",
models.CharField(max_length=50, null=True),
),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.event",
),
),
(
"organizer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.organizer",
),
),
],
options={
"verbose_name": "Billing Invoice",
"verbose_name_plural": "Billing Invoices",
"ordering": ("-created_at",),
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
]
1 change: 1 addition & 0 deletions src/pretix/base/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User, WebAuthnDevice
from .base import CachedFile, LoggedModel, cachedfile_name
from .billing import BillingInvoice
from .checkin import Checkin, CheckinList
from .devices import Device, Gate
from .event import (
Expand Down
59 changes: 59 additions & 0 deletions src/pretix/base/models/billing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from datetime import date

from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager

from pretix.base.models import LoggedModel


class BillingInvoice(LoggedModel):
STATUS_PENDING = "n"
STATUS_PAID = "p"
STATUS_EXPIRED = "e"
STATUS_CANCELED = "c"

STATUS_CHOICES = [
(STATUS_PENDING, _("pending")),
(STATUS_PAID, _("paid")),
(STATUS_EXPIRED, _("expired")),
(STATUS_CANCELED, _("canceled")),
]

organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE)
# organizer_billing = models.ForeignKey('OrganizerBilling', on_delete=models.CASCADE)
event = models.ForeignKey('Event', on_delete=models.CASCADE)

status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=STATUS_PENDING)
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3)

ticket_fee = models.DecimalField(max_digits=10, decimal_places=2)
payment_method = models.CharField(max_length=20, null=True, blank=True)
paid_datetime = models.DateTimeField(null=True, blank=True)
note = models.TextField(null=True, blank=True)

monthly_bill = models.DateField(default=date.today)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.CharField(max_length=50)
updated_at = models.DateTimeField(auto_now=True)
updated_by = models.CharField(max_length=50)

last_reminder_datetime = models.DateTimeField(null=True, blank=True)
next_reminder_datetime = models.DateTimeField(null=True, blank=True)
reminder_schedule = ArrayField(
models.IntegerField(),
default=list, # Sets the default to an empty list
blank=True,
help_text="Days after creation for reminders, e.g., [14, 28]"
)
reminder_enabled = models.BooleanField(default=True)
stripe_payment_intent_id = models.CharField(max_length=50, null=True, blank=True)

objects = ScopedManager(organizer='organizer')

class Meta:
verbose_name = "Billing Invoice"
verbose_name_plural = "Billing Invoices"
ordering = ("-created_at",)
2 changes: 1 addition & 1 deletion src/pretix/base/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ def get_mail_backend(self, timeout=None, force_custom=False):
if gs.settings.email_vendor == "sendgrid":
return SendGridEmail(api_key=gs.settings.send_grid_api_key)
else:
CustomSMTPBackend(
return CustomSMTPBackend(
host=gs.settings.smtp_host,
port=gs.settings.smtp_port,
username=gs.settings.smtp_username,
Expand Down
37 changes: 35 additions & 2 deletions src/pretix/base/services/mail.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import inspect
import logging
import os
Expand Down Expand Up @@ -35,6 +36,7 @@
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import email_filter, global_email_filter
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
Expand Down Expand Up @@ -277,7 +279,8 @@ def _create_mime_attachment(self, content, mimetype):
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int = None, position: int = None, headers: dict = None, bcc: List[str] = None,
invoices: List[int] = None, order: int = None, attach_tickets=False, user=None,
attach_ical=False, attach_cached_files: List[int] = None) -> bool:
attach_ical=False, attach_cached_files: List[int] = None, attach_file_base64: str = None,
attach_file_name: str = None) -> bool:
email = CustomEmail(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET)
Expand All @@ -295,7 +298,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
backend = event.get_mail_backend()
def cm(): return scope(organizer=event.organizer) # noqa
else:
backend = get_connection(fail_silently=False)
backend = get_mail_backend()
def cm(): return scopes_disabled() # noqa
with cm():

Expand Down Expand Up @@ -385,6 +388,9 @@ def cm(): return scopes_disabled() # noqa
pass

email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
if attach_file_base64:
attach_file_content = base64.b64decode(attach_file_base64)
email.attach(attach_file_name, attach_file_content, "application/pdf")

try:
backend.send_messages([email])
Expand Down Expand Up @@ -592,3 +598,30 @@ def normalize_image_url(url):
else:
url = urljoin(settings.MEDIA_URL, url)
return url


def get_mail_backend(timeout=None):
"""
Returns an email server connection, either by using the system-wide connection
or by returning a custom one based on the system's settings.
"""
from pretix.base.email import CustomSMTPBackend, SendGridEmail

gs = GlobalSettingsObject()

if gs.settings.email_vendor is not None:
if gs.settings.email_vendor == "sendgrid":
return SendGridEmail(api_key=gs.settings.send_grid_api_key)
else:
return CustomSMTPBackend(
host=gs.settings.smtp_host,
port=gs.settings.smtp_port,
username=gs.settings.smtp_username,
password=gs.settings.smtp_password,
use_tls=gs.settings.smtp_use_tls,
use_ssl=gs.settings.smtp_use_ssl,
fail_silently=False,
timeout=timeout,
)
else:
return get_connection(fail_silently=False)
6 changes: 6 additions & 0 deletions src/pretix/control/forms/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ def __init__(self, *args, **kwargs):

self.fields['banner_message'].widget.attrs['rows'] = '2'
self.fields['banner_message_detail'].widget.attrs['rows'] = '3'
self.fields = OrderedDict(list(self.fields.items()) + [
('stripe_webhook_secret_key', SecretKeySettingsField(
label=_('Stripe Webhook: Secret key'),
required=False,
)),
])


class UpdateSettingsForm(SettingsForm):
Expand Down
Loading
Loading