diff --git a/compose/django/supercronic_crontab b/compose/django/supercronic_crontab index ca3a264c..bd4be3ed 100644 --- a/compose/django/supercronic_crontab +++ b/compose/django/supercronic_crontab @@ -4,8 +4,11 @@ # Run every 6 hours 0 */6 * * * /usr/local/bin/python /app/manage.py simple_dummy_job -# Run every hour -0 */1 * * * /usr/local/bin/python /app/manage.py edoniq_import_results +# Run every hour at minute 17 +17 * * * * /usr/local/bin/python /app/manage.py edoniq_import_results + +# Run every hour at minute 43 +43 * * * * /usr/local/bin/python /app/manage.py abacus_export_job # every day at 19:30 30 19 * * * /usr/local/bin/python /app/manage.py send_email_reminders --type=all diff --git a/server/integration_tests/abacus_sftp/test_abacus_sftp.py b/server/integration_tests/abacus_sftp/test_abacus_sftp.py index 483f7102..77178ef0 100644 --- a/server/integration_tests/abacus_sftp/test_abacus_sftp.py +++ b/server/integration_tests/abacus_sftp/test_abacus_sftp.py @@ -1,13 +1,14 @@ import os import shutil import tempfile -from datetime import datetime +from datetime import date from io import StringIO from subprocess import Popen from time import sleep import pytest from django.conf import settings +from freezegun import freeze_time from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.create_default_users import create_default_users @@ -70,62 +71,111 @@ def test_upload_abacus_xml(setup_abacus_env): ) feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch") - feuz_checkout_info = CheckoutInformationFactory( - user=feuz, - transaction_id="24021508331287484", - first_name="Andreas", - last_name="Feuz", - street="Eggersmatt", - street_number="32", - postal_code="1719", - city="Zumholz", - country_id="CH", - invoice_address="org", - organisation_detail_name="VBV", - organisation_street="Laupenstrasse", - organisation_street_number="10", - organisation_postal_code="3000", - organisation_city="Bern", - organisation_country_id="CH", - ) - feuz_checkout_info.created_at = datetime(2024, 2, 15, 8, 33, 12, 0) - abacus_ssh_upload(feuz_checkout_info) - - # check if files got created - debitor_filepath = os.path.join(tmppath, "debitor/myVBV_debi_60000012.xml") - assert os.path.exists(debitor_filepath) - - with open(debitor_filepath) as debitor_file: - debi_content = debitor_file.read() - assert "60000012" in debi_content - assert "andreas.feuz@eiger-versicherungen.ch" in debi_content - - order_filepath = os.path.join( - tmppath, "order/myVBV_orde_20240215083312_60000012_6000000124.xml" - ) - assert os.path.exists(order_filepath) - with open(order_filepath) as order_file: - order_content = order_file.read() - assert ( - "24021508331287484" - in order_content + with freeze_time("2024-02-15 08:33:12"): + feuz_checkout_info = CheckoutInformationFactory( + user=feuz, + transaction_id="24021508331287484", + first_name="Andreas", + last_name="Feuz", + street="Eggersmatt", + street_number="32", + postal_code="1719", + city="Zumholz", + country_id="CH", + invoice_address="org", + organisation_detail_name="VBV", + organisation_street="Laupenstrasse", + organisation_street_number="10", + organisation_postal_code="3000", + organisation_city="Bern", + organisation_country_id="CH", ) - assert "60000012" in order_content - feuz_checkout_info.refresh_from_db() - assert feuz_checkout_info.abacus_ssh_upload_done + with freeze_time("2024-02-15 09:37:41"): + abacus_ssh_upload(feuz_checkout_info) - # calling `abacus_ssh_upload` a second time will not upload files again... - os.remove(debitor_filepath) - os.remove(order_filepath) + # check transmission data + feuz_checkout_info.refresh_from_db() + assert feuz_checkout_info.invoice_transmitted_at.date() == date(2024, 2, 15) + abacus_transmission_list = feuz_checkout_info.additional_json_data.get( + "abacus_transmission_list", [] + ) + assert len(abacus_transmission_list) == 1 + assert abacus_transmission_list[0]["transmitted_at"][0:10] == "2024-02-15" - abacus_ssh_upload(feuz_checkout_info) + # check if files got created + debitor_filepath = os.path.join(tmppath, "debitor/myVBV_debi_60000012.xml") + assert os.path.exists(debitor_filepath) - debitor_filepath = os.path.join(tmppath, "debitor/myVBV_debi_60000012.xml") - assert not os.path.exists(debitor_filepath) + with open(debitor_filepath) as debitor_file: + debi_content = debitor_file.read() + assert "60000012" in debi_content + assert "andreas.feuz@eiger-versicherungen.ch" in debi_content - order_filepath = os.path.join( - tmppath, "order/myVBV_orde_60000012_20240215083312_6000000124.xml" - ) - assert not os.path.exists(order_filepath) + order_filepath = os.path.join( + tmppath, "order/myVBV_orde_20240215083312_60000012_6000000124.xml" + ) + assert os.path.exists(order_filepath) + with open(order_filepath) as order_file: + order_content = order_file.read() + assert ( + "24021508331287484" + in order_content + ) + assert "60000012" in order_content + + feuz_checkout_info.refresh_from_db() + assert feuz_checkout_info.abacus_ssh_upload_done + + # calling `abacus_ssh_upload` a second time will not upload files again... + os.remove(debitor_filepath) + os.remove(order_filepath) + + abacus_ssh_upload(feuz_checkout_info) + + debitor_filepath = os.path.join(tmppath, "debitor/myVBV_debi_60000012.xml") + assert not os.path.exists(debitor_filepath) + + order_filepath = os.path.join( + tmppath, "order/myVBV_orde_60000012_20240215083312_6000000124.xml" + ) + assert not os.path.exists(order_filepath) + + with freeze_time("2024-04-21 17:33:19"): + # change customer data later will retrigger and redo abacus upload + feuz_checkout_info.first_name = "Peter" + feuz_checkout_info.last_name = "Egger" + feuz_checkout_info.save() + + abacus_ssh_upload(feuz_checkout_info) + + # check transmission data + feuz_checkout_info.refresh_from_db() + assert feuz_checkout_info.invoice_transmitted_at.date() == date(2024, 4, 21) + abacus_transmission_list = feuz_checkout_info.additional_json_data.get( + "abacus_transmission_list", [] + ) + assert len(abacus_transmission_list) == 2 + assert abacus_transmission_list[1]["transmitted_at"][0:10] == "2024-04-21" + + # check if files got created + debitor_filepath = os.path.join(tmppath, "debitor/myVBV_debi_60000012.xml") + assert os.path.exists(debitor_filepath) + + with open(debitor_filepath) as debitor_file: + debi_content = debitor_file.read() + assert "60000012" in debi_content + assert "andreas.feuz@eiger-versicherungen.ch" in debi_content + + order_filepath = os.path.join( + tmppath, "order/myVBV_orde_20240215083312_60000012_6000000124.xml" + ) + assert os.path.exists(order_filepath) + with open(order_filepath) as order_file: + order_content = order_file.read() + assert ( + "24021508331287484" + in order_content + ) + assert "60000012" in order_content diff --git a/server/vbv_lernwelt/shop/admin.py b/server/vbv_lernwelt/shop/admin.py index 30b64141..33160a48 100644 --- a/server/vbv_lernwelt/shop/admin.py +++ b/server/vbv_lernwelt/shop/admin.py @@ -64,6 +64,8 @@ class CheckoutInformationAdmin(admin.ModelAdmin): list_display = ( "product_sku", customer, + "first_name", + "last_name", "product_name", "product_price", "created_at", @@ -78,7 +80,11 @@ class CheckoutInformationAdmin(admin.ModelAdmin): "user__first_name", "user__last_name", "user__username", + "first_name", + "last_name", "transaction_id", + "refno2", + "organisation_detail_name", "abacus_order_id", "user__abacus_debitor_number", ] @@ -91,6 +97,14 @@ class CheckoutInformationAdmin(admin.ModelAdmin): "state", "product_price", "webhook_history", + "refno2", + "product_sku", + "invoice_transmitted_at", + "cembra_byjuno_invoice", + "ip_address", + "device_fingerprint_session_key", + "abacus_order_id", + "abacus_ssh_upload_done", ] diff --git a/server/vbv_lernwelt/shop/invoice/abacus.py b/server/vbv_lernwelt/shop/invoice/abacus.py index 2e7f9491..6aaed41e 100644 --- a/server/vbv_lernwelt/shop/invoice/abacus.py +++ b/server/vbv_lernwelt/shop/invoice/abacus.py @@ -4,6 +4,8 @@ from xml.dom import minidom from xml.etree.ElementTree import Element, SubElement, tostring import structlog +from django.db.models import F, Q +from django.utils import timezone from vbv_lernwelt.shop.const import VV_PRODUCT_NUMBER from vbv_lernwelt.shop.invoice.abacus_sftp_client import AbacusSftpClient @@ -12,14 +14,34 @@ from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState logger = structlog.get_logger(__name__) +def filter_checkout_information_for_abacus_upload_qs(): + return CheckoutInformation.objects.filter( + Q(abacus_ssh_upload_done=False) + | Q(updated_at__gt=F("invoice_transmitted_at") + datetime.timedelta(minutes=1)), + state=CheckoutState.PAID, + ) + + def abacus_ssh_upload(checkout_information: CheckoutInformation): if checkout_information.state != CheckoutState.PAID: # only upload invoice if checkout is paid return True try: - if not checkout_information.abacus_ssh_upload_done: - # only upload data for not yet uploaded invoices + if ( + # invoice not in abacus yet + not checkout_information.abacus_ssh_upload_done + # invoice updated more than 5 minutes ago + or ( + checkout_information.abacus_ssh_upload_done + and checkout_information.invoice_transmitted_at is not None + and ( + checkout_information.updated_at + - checkout_information.invoice_transmitted_at + ) + > datetime.timedelta(minutes=1) + ) + ): invoice_xml_filename, invoice_xml_content = create_invoice_xml( checkout_information ) @@ -35,7 +57,22 @@ def abacus_ssh_upload(checkout_information: CheckoutInformation): ) checkout_information.abacus_ssh_upload_done = True - checkout_information.invoice_transmitted_at = datetime.datetime.now() + checkout_information.invoice_transmitted_at = timezone.now() + + abacus_transmission_list = checkout_information.additional_json_data.get( + "abacus_transmission_list", [] + ) + abacus_transmission_list.append( + { + "invoice_filename": invoice_xml_filename, + "customer_filename": customer_xml_filename, + "transmitted_at": checkout_information.invoice_transmitted_at.isoformat(), + } + ) + checkout_information.additional_json_data["abacus_transmission_list"] = ( + abacus_transmission_list + ) + checkout_information.save() return True @@ -45,6 +82,7 @@ def abacus_ssh_upload(checkout_information: CheckoutInformation): checkout_information_id=checkout_information.id, exception=str(e), exc_info=True, + label="abacus_ssh_upload", ) return False diff --git a/server/vbv_lernwelt/shop/management/commands/__init__.py b/server/vbv_lernwelt/shop/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/shop/management/commands/abacus_export_job.py b/server/vbv_lernwelt/shop/management/commands/abacus_export_job.py new file mode 100644 index 00000000..fe80bf7b --- /dev/null +++ b/server/vbv_lernwelt/shop/management/commands/abacus_export_job.py @@ -0,0 +1,45 @@ +import structlog + +from vbv_lernwelt.core.base import LoggedCommand +from vbv_lernwelt.shop.invoice.abacus import ( + abacus_ssh_upload, + filter_checkout_information_for_abacus_upload_qs, +) + +logger = structlog.get_logger(__name__) + + +class Command(LoggedCommand): + help = "Export `CheckoutInformation` invoices to Abacus" + + def handle(self, *args, **options): + logger.info( + "abacus_export_job started", + label="abacus_ssh_upload", + ) + + stat_results = { + "num_invoices": 0, + "num_uploaded": 0, + "num_error": 0, + } + + abacus_ci_qs = filter_checkout_information_for_abacus_upload_qs() + stat_results["num_invoices"] = abacus_ci_qs.count() + + for checkout_info in abacus_ci_qs: + abacus_ssh_upload_result = abacus_ssh_upload(checkout_info) + + if abacus_ssh_upload_result: + stat_results["num_uploaded"] += 1 + else: + stat_results["num_error"] += 1 + + self.job_log.json_data = stat_results + self.job_log.save() + + logger.info( + "abacus_export_job finished", + label="abacus_ssh_upload", + stat_results=stat_results, + ) diff --git a/server/vbv_lernwelt/shop/migrations/0018_checkoutinformation_additional_json_data.py b/server/vbv_lernwelt/shop/migrations/0018_checkoutinformation_additional_json_data.py new file mode 100644 index 00000000..e443b43e --- /dev/null +++ b/server/vbv_lernwelt/shop/migrations/0018_checkoutinformation_additional_json_data.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.13 on 2024-08-30 13:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("shop", "0017_checkoutinformation_chosen_profile"), + ] + + operations = [ + migrations.AddField( + model_name="checkoutinformation", + name="additional_json_data", + field=models.JSONField(blank=True, default=dict), + ), + ] diff --git a/server/vbv_lernwelt/shop/migrations/0019_alter_checkoutinformation_refno2.py b/server/vbv_lernwelt/shop/migrations/0019_alter_checkoutinformation_refno2.py new file mode 100644 index 00000000..488c808b --- /dev/null +++ b/server/vbv_lernwelt/shop/migrations/0019_alter_checkoutinformation_refno2.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.13 on 2024-08-30 13:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("shop", "0018_checkoutinformation_additional_json_data"), + ] + + operations = [ + migrations.AlterField( + model_name="checkoutinformation", + name="refno2", + field=models.CharField(blank=True, default="", max_length=255), + ), + ] diff --git a/server/vbv_lernwelt/shop/models.py b/server/vbv_lernwelt/shop/models.py index 5a6da1ac..34be44bb 100644 --- a/server/vbv_lernwelt/shop/models.py +++ b/server/vbv_lernwelt/shop/models.py @@ -62,7 +62,7 @@ class CheckoutInformation(models.Model): invoice_transmitted_at = models.DateTimeField(blank=True, null=True) transaction_id = models.CharField(max_length=255) - refno2 = models.CharField(max_length=255) + refno2 = models.CharField(max_length=255, blank=True, default="") # end user (required) first_name = models.CharField(max_length=255) @@ -116,6 +116,8 @@ class CheckoutInformation(models.Model): abacus_order_id = models.BigIntegerField(unique=True, null=True, blank=True) abacus_ssh_upload_done = models.BooleanField(default=False) + additional_json_data = models.JSONField(default=dict, blank=True) + def set_increment_abacus_order_id(self): if self.abacus_order_id: return self diff --git a/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py b/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py index b9a83148..bca8bc08 100644 --- a/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py +++ b/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py @@ -1,6 +1,8 @@ from datetime import date, datetime from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.create_default_users import create_default_users @@ -8,9 +10,11 @@ from vbv_lernwelt.core.model_utils import add_countries from vbv_lernwelt.shop.invoice.abacus import ( create_customer_xml, create_invoice_xml, + filter_checkout_information_for_abacus_upload_qs, render_customer_xml, render_invoice_xml, ) +from vbv_lernwelt.shop.models import CheckoutState from vbv_lernwelt.shop.tests.factories import CheckoutInformationFactory USER_USERNAME = "testuser" @@ -23,6 +27,44 @@ class AbacusInvoiceTestCase(TestCase): add_countries(small_set=True) create_default_users() + def test_filter_checkout_information_for_abacus_upload(self): + feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch") + + with freeze_time("2024-02-15 08:33:12"): + feuz_checkout_info = CheckoutInformationFactory( + user=feuz, + transaction_id="24021508331287484", + first_name="Andreas", + last_name="Feuz", + street="Eggersmatt", + street_number="32", + postal_code="1719", + city="Zumholz", + country_id="CH", + invoice_address="prv", + state=CheckoutState.ONGOING, + ) + + qs = filter_checkout_information_for_abacus_upload_qs() + self.assertEqual(qs.count(), 0) + + feuz_checkout_info.state = CheckoutState.PAID + feuz_checkout_info.save() + qs = filter_checkout_information_for_abacus_upload_qs() + self.assertEqual(qs.count(), 1) + + feuz_checkout_info.abacus_ssh_upload_done = True + feuz_checkout_info.invoice_transmitted_at = timezone.now() + feuz_checkout_info.save() + qs = filter_checkout_information_for_abacus_upload_qs() + self.assertEqual(qs.count(), 0) + + with freeze_time("2024-02-15 09:44:44"): + feuz_checkout_info.first_name = "Peter" + feuz_checkout_info.save() + qs = filter_checkout_information_for_abacus_upload_qs() + self.assertEqual(qs.count(), 1) + def test_create_invoice_xml(self): # set abacus_number before _pat = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch") diff --git a/server/vbv_lernwelt/shop/views.py b/server/vbv_lernwelt/shop/views.py index 43a1755b..eac0bcd9 100644 --- a/server/vbv_lernwelt/shop/views.py +++ b/server/vbv_lernwelt/shop/views.py @@ -167,6 +167,7 @@ def checkout_vv(request): checkout_info.set_increment_abacus_order_id() refno2 = f"{request.user.abacus_debitor_number}_{VV_PRODUCT_NUMBER}" + checkout_info.refno2 = refno2 try: datatrans_customer_data = None