Skip to content

Commit

Permalink
Implement schedule task to collect billing invoice (fossasia#417)
Browse files Browse the repository at this point in the history
* Implement schedule task to collect billing invoice

* consider sourcery-ai and fix isort

* Implement billing settings form for organizer

* Fix flake8 in pipeline

* implement create and save payment_information

* Fix isort, flake8 in pipeline

* implement payment-information-v1

* src/pretix/control/views/organizer_views/organizer_view.py

* Code refactoring

* Code refactoring

* Remove payment_information attribute

* Implement tax validation

* Update code

* Update code

* Fix flake8 in pipeline

* Add pyvat package

* Fix flake8 in pipeline

* Latest code

* Fix flake8 in pipeline

* Add logger error

* Fix flake8 in pipeline

* implement trigger invoice to organizer

* Fix conflict pretix/base/migration

* Implement auto billing charge

* Fix conflict pretix base migration

* Update pretix base migration

* Add logging information and modify error logging

* Implement auto billing charge

* update schedule task

* fix isort, flake8 and update branch

* fix isort pipeline

* Implement automatic payment charging

* update invoice template

* change var name, format code

* Implement automatic payment charging

* Add comment

* Implement show error message

* handle case update invoice to expired

* fix isort, flake8 pipeline

* Update api to trigger billing task for testing

* Implement stripe webhook secret key in global setting

* Add comment,save tax_id, show error and sucess message

* handle case get mail backend for global settings

* fix isort pipeline

* fix sending custom mail smtp

* tickets fee should be calculated based on net amount

* remove unsed import to fix pipeline

* correct import to fix isort

* Update code

* Fix flake8 in pipeline

* Update code

* Update code

* Update code

* fix flake8 in pipeline

* move validation to clean method and move get_country_name to countries helper

* formar code

* merge migration file

* fix isort

---------

Co-authored-by: lcduong <lcduong>
Co-authored-by: odkhang <[email protected]>
  • Loading branch information
lcduong and odkhang authored Nov 21, 2024
1 parent 979abbb commit eca6fad
Show file tree
Hide file tree
Showing 16 changed files with 1,103 additions and 32 deletions.
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

0 comments on commit eca6fad

Please sign in to comment.