Skip to content
This repository has been archived by the owner on Jun 24, 2024. It is now read-only.

YC-1188 - Update unpaid PF transactions status in cron task, refactor c… #1018

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ OAUTH2_PKCE_REQUIRED=false
# Available payment processor. Comma-separated values.
PAYMENT_CURRENCY=CHF
PAYMENT_PROCESSING_TEST_ENVIRONMENT=true
PAYMENT_PENDING_TRANSACTION_MAX_AGE_MINS=1440
# Used to convert dates to the correct UTC, in hours. Choose a value between -12 and +14 (has to be integer)
LOCAL_TIME_ZONE_UTC=+1
# Use "localhost" in local
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ services:
AUTHOR_IBAN_VISIBLE:
PAYMENT_PROCESSING_TEST_ENVIRONMENT:
PAYMENT_CURRENCY:
PAYMENT_PENDING_TRANSACTION_MAX_AGE_MINS:
LOCAL_TIME_ZONE_UTC:
SITE_DOMAIN:
USE_THUMBOR:
Expand Down
20 changes: 20 additions & 0 deletions geocity/apps/submissions/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,23 @@ def do(self):
inquiry.submission.save()

logger.info("The submission opening Cronjob finished")


class PaymentTransactionsStatusUpdate(CronJobBase):
RUN_EVERY_MINS = 1
schedule = Schedule(run_every_mins=RUN_EVERY_MINS)

code = "submissions.payment_transactions_status_update"

def do(self):
try:
call_command("update_payment_transactions_status")
except CommandError:
logger.error(
"Error occured while trying to update the payment transactions "
"status from the Cronjob."
)
else:
logger.info(
"The payment transactions status update Cronjob finished successfully"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import datetime

from django.conf import settings
from django.core.management import BaseCommand
from django.utils import timezone
from django.utils.translation import gettext

from geocity.apps.submissions.payments.models import Transaction
from geocity.apps.submissions.payments.postfinance.models import PostFinanceTransaction
from geocity.apps.submissions.payments.services import get_payment_processor


class Command(BaseCommand):
help = gettext(
"Update the status of transactions that are pending and not older than %s hours."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Update the status of transactions that are pending and not older than %s hours."
"Update the status of transactions that are pending and not older than %s minutes."

% settings.PAYMENT_PENDING_TRANSACTION_MAX_AGE_MINS
)

def handle(self, *args, **options):
self.stdout.write(
"Checking status of unpaid transations that are not older than %s minutes..."
% settings.PAYMENT_PENDING_TRANSACTION_MAX_AGE_MINS
)

nb_transactions_confirmed = 0
nb_transactions_failed = 0
# Get all unpaid transactions that are not older than the specified time
transactions_to_update = PostFinanceTransaction.objects.filter(
status=Transaction.STATUS_UNPAID,
authorization_timeout_on__gte=timezone.now()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an older issue here, for a reason I don't get, the value of this attribute in DB is truncated to the day, the time is not stored (2024-06-12 --> 2024-06-12 00:00:00.000 +0200). So with the default value of 30 minutes, it will never work.

If we use the value as provided in env.example, it works because it's 24 hours and not 30 minutes.

But it's not clean, is there a way to store the timeout value with the hours?

- datetime.timedelta(
minutes=settings.PAYMENT_PENDING_TRANSACTION_MAX_AGE_MINS
),
)

for transaction in transactions_to_update:
submission = transaction.submission_price.submission
processor = get_payment_processor(submission.get_form_for_payment())

if processor.is_transaction_authorized(transaction):
transaction.confirm_payment()
nb_transactions_confirmed += 1
elif processor.is_transaction_failed(transaction):
transaction.set_failed()
nb_transactions_failed += 1

if nb_transactions_confirmed:
self.stdout.write(
"Marked %d transactions as confirmed." % nb_transactions_confirmed
)
if nb_transactions_failed:
self.stdout.write(
"Marked %d transactions as failed." % nb_transactions_failed
)
if not nb_transactions_confirmed and not nb_transactions_failed:
self.stdout.write("No transactions to update.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.11 on 2024-06-06 12:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("submissions", "0030_alter_servicefeetype_fix_price_editable"),
]

operations = [
migrations.AddField(
model_name="historicalpostfinancetransaction",
name="extra_data",
field=models.JSONField(
default=dict, verbose_name="Données supplémentaires"
),
),
migrations.AddField(
model_name="postfinancetransaction",
name="extra_data",
field=models.JSONField(
default=dict, verbose_name="Données supplémentaires"
),
),
]
33 changes: 33 additions & 0 deletions geocity/apps/submissions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,39 @@ def get_last_prolongation_transaction(self):
.first()
)

def set_prolongation_requested_and_notify(self, prolongation_date):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai profité de l'occasion pour déplacer dans le modèle cette fonction qui sert à setter la date de prolongation . Aussi, j'ai enlevé l'argument request, qui était utilisé auparavant pour construire l'URL absolue de la soumission, parce qu'il est possible qu'une prolongation soit trigger depuis le cronjob, où on ne peut pas avoir le contexte d'une requête HTTP.

from . import services

self.prolongation_status = self.PROLONGATION_STATUS_PENDING
self.prolongation_date = prolongation_date
self.save()

attachments = []
if self.requires_online_payment() and self.author.userprofile.notify_per_email:
attachments = self.get_submission_payment_attachments("confirmation")
data = {
"subject": "{} ({})".format(
_("Votre demande de prolongation"), self.get_forms_names_list()
),
"users_to_notify": [self.author.email],
"template": "submission_acknowledgment.txt",
"submission": self,
"absolute_uri_func": Submission.get_absolute_url,
}
services.send_email_notification(data, attachments=attachments)

data = {
"subject": "{} ({})".format(
_("Une demande de prolongation vient d'être soumise"),
self.get_forms_names_list(),
),
"users_to_notify": self.get_secretary_email(),
"template": "submission_prolongation_for_services.txt",
"submission": self,
"absolute_uri_func": Submission.get_absolute_url,
}
services.send_email_notification(data, attachments=attachments)

# ServiceFees for submission
def get_service_fees(self):
return ServiceFee.objects.filter(submission=self)
Expand Down
21 changes: 21 additions & 0 deletions geocity/apps/submissions/payments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class Transaction(models.Model):
default=TYPE_SUBMISSION,
)

extra_data = models.JSONField(_("Données supplémentaires"), default=dict)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Utilisé pour stocker la date de prolongation, quand il y a un paiement requis. Elle était avant renvoyée par paramètre dans l'URL au retour de PostFinance, mais il est nécéssaire de la stocker avant de déclencher le paiement, parce qu'il est possible que le paiement soit validé par le cronjob (donc pas de retour depuis PF)


class Meta:
abstract = True
ordering = ("-creation_date",)
Expand All @@ -82,6 +84,14 @@ class Meta:
def can_have_status_changed(self):
return self.amount > 0

@property
def has_been_confirmed(self):
return self.status in (
self.STATUS_PAID,
self.STATUS_TO_REFUND,
self.STATUS_REFUNDED,
)

def set_refunded(self):
self.status = self.STATUS_REFUNDED
self.save()
Expand Down Expand Up @@ -136,6 +146,17 @@ def get_refund_pdf(self, read=False):
output = output.read()
return f"refund_{self.transaction_id}.pdf", output

def confirm_payment(self):
if self.transaction_type == self.TYPE_PROLONGATION:
prolongation_date = self.extra_data.get("prolongation_date")
if prolongation_date is None:
raise SuspiciousOperation
self.submission_price.submission.set_prolongation_requested_and_notify(
timezone.datetime.fromtimestamp(prolongation_date)
)
self.set_paid()
self.submission_price.submission.generate_and_save_pdf("confirmation", self)


class ServiceFeeType(models.Model):
administrative_entity = models.ForeignKey(
Expand Down
12 changes: 10 additions & 2 deletions geocity/apps/submissions/payments/postfinance/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,16 @@ def is_transaction_authorized(self, transaction):
TransactionState.AUTHORIZED,
)

def is_transaction_failed(self, transaction):
status = self._get_transaction_status(transaction)
return status in (
TransactionState.FAILED,
TransactionState.VOIDED,
TransactionState.DECLINE,
)

def _create_internal_transaction(
self, submission, transaction_type=None, override_price=None
self, submission, transaction_type=None, override_price=None, extra_data=None
):
# If there is a related existing transaction, which:
# 1. Is still within the PostFinance authorization time window
Expand Down Expand Up @@ -203,7 +211,7 @@ def _create_internal_transaction(
if existing_transaction:
return existing_transaction, False
return super(PostFinanceCheckoutProcessor, self)._create_internal_transaction(
submission, transaction_type, override_price
submission, transaction_type, override_price, extra_data
)

def _save_merchant_data(self, transaction, merchant_transaction_data):
Expand Down
6 changes: 4 additions & 2 deletions geocity/apps/submissions/payments/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def create_merchant_transaction(
raise NotImplementedError

def _create_internal_transaction(
self, submission, transaction_type=None, override_price=None
self, submission, transaction_type=None, override_price=None, extra_data=None
):
price = submission.get_submission_price()
if override_price is None:
Expand All @@ -53,6 +53,8 @@ def _create_internal_transaction(
"amount": amount,
"currency": currency,
}
if extra_data is not None:
create_kwargs.update({"extra_data": extra_data})
if transaction_type is not None:
create_kwargs["transaction_type"] = transaction_type
return (
Expand Down Expand Up @@ -89,6 +91,7 @@ def create_prolongation_transaction_and_return_payment_page_url(
submission,
transaction_type=self.transaction_class.TYPE_PROLONGATION,
override_price=prolongation_price,
extra_data={"prolongation_date": prolongation_date},
)
if is_new_transaction:
merchant_transaction_data = self.create_merchant_transaction(
Expand All @@ -105,7 +108,6 @@ def create_prolongation_transaction_and_return_payment_page_url(
"submissions:confirm_prolongation_transaction",
kwargs={
"pk": transaction.pk,
"prolongation_date": prolongation_date,
},
)
),
Expand Down
2 changes: 1 addition & 1 deletion geocity/apps/submissions/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
name="confirm_transaction",
),
path(
"transactions/confirm_prolongation/<int:pk>/<int:prolongation_date>",
"transactions/confirm_prolongation/<int:pk>",
views.ConfirmProlongationTransactionView.as_view(),
name="confirm_prolongation_transaction",
),
Expand Down
Loading
Loading