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

Commit

Permalink
YC-1149 paiement prolongation
Browse files Browse the repository at this point in the history
  • Loading branch information
danickfort committed Feb 8, 2024
1 parent 9483ece commit ae5da12
Show file tree
Hide file tree
Showing 13 changed files with 450 additions and 55 deletions.
63 changes: 56 additions & 7 deletions geocity/apps/submissions/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def save(self):
return self.instance


class FormsPriceSelectForm(forms.Form):
class AbstractFormsPriceSelectForm(forms.Form):

selected_price = forms.ChoiceField(
label=False, widget=SingleFormRadioSelectWidget(), required=True
Expand All @@ -361,22 +361,71 @@ class FormsPriceSelectForm(forms.Form):
def __init__(self, instance, *args, **kwargs):
self.instance = instance
initial = {}
if self.instance.submission_price is not None:
if (
self.instance.submission_price is not None
and self.instance.submission_price.original_price is not None
):
initial = {
"selected_price": self.instance.submission_price.original_price.pk
}
super(FormsPriceSelectForm, self).__init__(
*args, **{**kwargs, "initial": initial}
)
if self.instance.status != self.instance.STATUS_DRAFT:
self.fields["selected_price"].widget.attrs["disabled"] = "disabled"
super().__init__(*args, **{**kwargs, "initial": initial})
form_for_payment = self.instance.get_form_for_payment()

choices = []
for price in form_for_payment.prices.order_by("formprice"):
choices.append((price.pk, price.str_for_choice()))
self.fields["selected_price"].choices = choices


class FormsPriceSelectForm(AbstractFormsPriceSelectForm):

selected_price = forms.ChoiceField(
label=False, widget=SingleFormRadioSelectWidget(), required=True
)

def __init__(self, instance, *args, **kwargs):
super().__init__(instance, *args, **kwargs)
if self.instance.status != self.instance.STATUS_DRAFT:
self.fields["selected_price"].widget.attrs["disabled"] = "disabled"

@transaction.atomic
def save(self):
selected_price_id = self.cleaned_data["selected_price"]
selected_price = Price.objects.get(pk=selected_price_id)
price_data = {
"amount": selected_price.amount,
"currency": selected_price.currency,
"text": selected_price.text,
}
current_submission_price = self.instance.get_submission_price()
if current_submission_price is None:
SubmissionPrice.objects.create(
**{
**price_data,
"original_price": selected_price,
"submission": self.instance,
}
)
else:
if self.instance.status != self.instance.STATUS_DRAFT:
raise forms.ValidationError(
_("Le prix ne peut pas être modifié pour cette demande.")
)
current_submission_price.amount = price_data["amount"]
current_submission_price.text = price_data["text"]
current_submission_price.currency = price_data["currency"]
current_submission_price.original_price = selected_price
current_submission_price.save()

return self.instance


class ProlongationFormsPriceSelectForm(AbstractFormsPriceSelectForm):
def __init__(self, instance, *args, **kwargs):
super().__init__(instance, *args, **kwargs)
if len(self.fields["selected_price"].choices) == 1:
self.initial["selected_price"] = self.fields["selected_price"].choices[0][0]

@transaction.atomic
def save(self):
selected_price_id = self.cleaned_data["selected_price"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 4.2.9 on 2024-02-08 10:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("submissions", "0024_geom_field_input_type_for_form"),
]

operations = [
migrations.AddField(
model_name="historicalpostfinancetransaction",
name="transaction_type",
field=models.CharField(
choices=[
("submission", "Soumission"),
("prolongation", "Prolongation"),
],
default="submission",
max_length=20,
verbose_name="Type de transaction",
),
),
migrations.AddField(
model_name="postfinancetransaction",
name="transaction_type",
field=models.CharField(
choices=[
("submission", "Soumission"),
("prolongation", "Prolongation"),
],
default="submission",
max_length=20,
verbose_name="Type de transaction",
),
),
]
12 changes: 12 additions & 0 deletions geocity/apps/submissions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,18 @@ def get_transactions(self):
return None
return self.submission_price.get_transactions()

def get_last_prolongation_transaction(self):
from .payments.models import Transaction

if self.get_transactions() is None:
return None
return (
self.get_transactions()
.filter(transaction_type=Transaction.TYPE_PROLONGATION)
.order_by("-updated_date")
.first()
)

def get_history(self):
# Transactions history
if self.submission_price is None:
Expand Down
12 changes: 12 additions & 0 deletions geocity/apps/submissions/payments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class Transaction(models.Model):
STATUS_TO_REFUND = "to_refund"
STATUS_REFUNDED = "refunded"
STATUS_FAILED = "failed"
TYPE_SUBMISSION = "submission"
TYPE_PROLONGATION = "prolongation"
submission_price = models.ForeignKey(
"SubmissionPrice",
on_delete=models.CASCADE,
Expand Down Expand Up @@ -55,6 +57,16 @@ class Transaction(models.Model):
)
payment_url = models.CharField(_("URL de paiement"), max_length=255, null=True)

transaction_type = models.CharField(
_("Type de transaction"),
choices=[
(TYPE_SUBMISSION, _("Soumission")),
(TYPE_PROLONGATION, _("Prolongation")),
],
max_length=20,
default=TYPE_SUBMISSION,
)

class Meta:
abstract = True
ordering = ("-creation_date",)
Expand Down
66 changes: 48 additions & 18 deletions geocity/apps/submissions/payments/postfinance/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def _get_transaction_payment_page_service_api(self):
)
return self.transaction_payment_page_service

def create_merchant_transaction(self, request, submission, transaction):
def create_merchant_transaction(
self, request, submission, transaction, extra_kwargs=None
):
"""
Creates a transaction on PostFinance Checkout. The transaction contains 1 line item with:
- Name: The submission's price's text (description of the price)
Expand Down Expand Up @@ -82,13 +84,34 @@ def create_merchant_transaction(self, request, submission, transaction):

attribute = LineItemAttribute(label="Compte interne", value=internal_account)

if extra_kwargs is None:
amount = submission.price.amount
currency = submission.price.currency
text = submission.price.text
price_pk = submission.price.original_price.pk
success_url = request.build_absolute_uri(
reverse(
"submissions:confirm_transaction", kwargs={"pk": transaction.pk}
)
)
failed_url = request.build_absolute_uri(
reverse("submissions:fail_transaction", kwargs={"pk": transaction.pk})
)
else:
amount = extra_kwargs["amount"]
currency = extra_kwargs["currency"]
text = extra_kwargs["text"]
price_pk = extra_kwargs["pk"]
success_url = extra_kwargs["success_url"]
failed_url = extra_kwargs["failed_url"]

line_item = LineItem(
name=submission.price.text,
name=text,
unique_id=str(submission.pk),
sku=str(submission.price.original_price.pk),
sku=str(price_pk),
quantity=1,
attributes={"internal_account": attribute},
amount_including_tax=float(submission.price.amount),
amount_including_tax=float(amount),
type=LineItemType.PRODUCT,
)
environment_selection_strategy = (
Expand All @@ -101,20 +124,14 @@ def create_merchant_transaction(self, request, submission, transaction):
if settings.PAYMENT_PROCESSING_TEST_ENVIRONMENT
else Environment.LIVE
)
success_url = request.build_absolute_uri(
reverse("submissions:confirm_transaction", kwargs={"pk": transaction.pk})
)
failed_url = request.build_absolute_uri(
reverse("submissions:fail_transaction", kwargs={"pk": transaction.pk})
)
merchant_reference = f"GEOCITY-{submission.id}"
customer_id = f"GEOCITY-{request.user.id}"
customer_email_address = request.user.email

merchant_transaction = TransactionCreate(
line_items=[line_item],
auto_confirmation_enabled=True,
currency=submission.price.currency,
currency=currency,
environment=environment,
environment_selection_strategy=environment_selection_strategy,
success_url=success_url,
Expand Down Expand Up @@ -153,24 +170,37 @@ def is_transaction_authorized(self, transaction):
TransactionState.AUTHORIZED,
)

def _create_internal_transaction(self, submission):
def _create_internal_transaction(
self, submission, transaction_type=None, override_price=None
):
# If there is a related existing transaction, which:
# 1. Is still within the PostFinance authorization time window
# 2. Has the same amount and currency
# (if it is different, it means that the user has chosen a different price)
# 3. Is unpaid
# Then we can reuse it, instead of re-generating another one
if override_price is None:
amount = submission.price.amount
currency = submission.price.currency
else:
amount = override_price.amount
currency = override_price.currency
filter_kwargs = {
"submission_price": submission.submission_price,
"amount": amount,
"currency": currency,
"status": self.transaction_class.STATUS_UNPAID,
"authorization_timeout_on__gt": timezone.now(),
}
if transaction_type is not None:
filter_kwargs["transaction_type"] = transaction_type
existing_transaction = self.transaction_class.objects.filter(
submission_price=submission.submission_price,
amount=submission.submission_price.amount,
currency=submission.submission_price.currency,
status=self.transaction_class.STATUS_UNPAID,
authorization_timeout_on__gt=timezone.now(),
**filter_kwargs
).first()
if existing_transaction:
return existing_transaction, False
return super(PostFinanceCheckoutProcessor, self)._create_internal_transaction(
submission
submission, transaction_type, override_price
)

def _save_merchant_data(self, transaction, merchant_transaction_data):
Expand Down
70 changes: 62 additions & 8 deletions geocity/apps/submissions/payments/processors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.conf import settings
from django.urls import reverse


class MissingPaymentProcessorSettingError(Exception):
Expand All @@ -24,7 +25,9 @@ def _check_required_settings(self):
def __init__(self):
self._check_required_settings()

def create_merchant_transaction(self, request, submission, transaction):
def create_merchant_transaction(
self, request, submission, transaction, extra_kwargs=None
):
"""
Returns a dict with the merchant transaction data. Example:
{
Expand All @@ -34,15 +37,26 @@ def create_merchant_transaction(self, request, submission, transaction):
"""
raise NotImplementedError

def _create_internal_transaction(self, submission):
def _create_internal_transaction(
self, submission, transaction_type=None, override_price=None
):
price = submission.get_submission_price()
if override_price is None:
currency = price.currency
amount = price.amount
else:
currency = override_price.currency
amount = override_price.amount
create_kwargs = {
"submission_price": price,
"transaction_id": 0,
"amount": amount,
"currency": currency,
}
if transaction_type is not None:
create_kwargs["transaction_type"] = transaction_type
return (
self.transaction_class.objects.create(
submission_price=price,
transaction_id=0, # TODO: field null=True instead of this?
amount=price.amount,
currency=price.currency,
),
self.transaction_class.objects.create(**create_kwargs),
True,
)

Expand All @@ -68,6 +82,46 @@ def create_transaction_and_return_payment_page_url(self, submission, request):
else:
return transaction.payment_url

def create_prolongation_transaction_and_return_payment_page_url(
self, submission, prolongation_price, prolongation_date, request
):
transaction, is_new_transaction = self._create_internal_transaction(
submission,
transaction_type=self.transaction_class.TYPE_PROLONGATION,
override_price=prolongation_price,
)
if is_new_transaction:
merchant_transaction_data = self.create_merchant_transaction(
request,
submission,
transaction,
extra_kwargs={
"amount": prolongation_price.amount,
"currency": prolongation_price.currency,
"text": prolongation_price.text,
"pk": prolongation_price.pk,
"success_url": request.build_absolute_uri(
reverse(
"submissions:confirm_prolongation_transaction",
kwargs={
"pk": transaction.pk,
"prolongation_date": prolongation_date,
},
)
),
"failed_url": request.build_absolute_uri(
reverse(
"submissions:fail_prolongation_transaction",
kwargs={"pk": transaction.pk},
)
),
},
)
self._save_merchant_data(transaction, merchant_transaction_data)
return merchant_transaction_data["payment_page_url"]
else:
return transaction.payment_url

def _get_transaction_status(self, transaction):
raise NotImplementedError

Expand Down
Loading

0 comments on commit ae5da12

Please sign in to comment.