From e32e5faf35a74472f814028eafca37c26132a6e2 Mon Sep 17 00:00:00 2001
From: Hugo Osvaldo Barrera <hugo@whynothugo.nl>
Date: Fri, 17 Nov 2023 05:55:37 +0800
Subject: [PATCH] Add a helper to approximate receipt dates

---
 django_afip/models.py |  53 ++++++++++++++++++++++
 docs/changelog.rst    |   3 ++
 tests/test_models.py  | 103 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 159 insertions(+)

diff --git a/django_afip/models.py b/django_afip/models.py
index b6ce2a9a..09da9eeb 100644
--- a/django_afip/models.py
+++ b/django_afip/models.py
@@ -1336,6 +1336,59 @@ def revalidate(self) -> ReceiptValidation | None:
             return validation
         return None
 
+    def approximate_date(receipt: models.Receipt) -> bool:
+        """Approximate the date of the receipt as close as possible.
+
+        If a receipt should have been validated in a past date, adjust its date as close
+        as possible:
+
+            - Receipts can only be validated with dates as far as 14 days ago. If the
+              receipt date is older than that, set it to 14 days ago.
+            - If other receipts have been validated on a more recent date, the receipt
+              cannot be older than the most recent one.
+
+        If the ``issued_date`` needs to be changed, the field in the input receipt will
+        be updated and atomically saved to the database.
+
+        Returns ``True`` if the date has been changed.
+        """
+        today = datetime.now(TZ_AR).date()
+
+        if receipt.issued_date == today:
+            return False
+
+        most_recent = (
+            Receipt.objects.filter(
+                point_of_sales=receipt.point_of_sales,
+                receipt_type=receipt.receipt_type,
+                validation__result=ReceiptValidation.RESULT_APPROVED,
+            )
+            .order_by("issued_date")
+            .last()
+        )
+
+        fortnight_ago = today - timedelta(days=14)
+        if most_recent is not None:
+            oldest_possible = max(most_recent.issued_date, fortnight_ago)
+        else:
+            oldest_possible = fortnight_ago
+
+        if receipt.issued_date >= oldest_possible:
+            return False
+
+        # Commit this atomically to avoid race conditions.
+        Receipt.objects.filter(
+            pk=receipt.id,
+            receipt_number__isnull=True,
+        ).update(
+            issued_date=oldest_possible,
+        )
+
+        # Mutate the input object to avoid inconsistency issues.
+        receipt.issued_date = oldest_possible
+
+        return True
+
     def __repr__(self) -> str:
         return "<Receipt {}: {} {} for {}>".format(
             self.pk,
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 1a8c5928..0296bece 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -25,6 +25,9 @@ acá.
 - **BREAKING**: The signal that auto-generated receipt pdfs for validated
   ReceiptPDFs  has been removed. Applications now need to explicitly call
   :meth:`~.ReceiptPDF.save_pdf()`.
+- Add a new helper helper method :meth:`~.Receipt.approximate_date`. It is
+  intended to be used to automatically approximate dates on systems which
+  perform automatic or unattended receipt validation.
 
 11.3.1
 ------
diff --git a/tests/test_models.py b/tests/test_models.py
index c7cf8115..4f28ec52 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,5 +1,8 @@
 from __future__ import annotations
 
+from datetime import date
+from datetime import datetime
+from datetime import timedelta
 from decimal import Decimal
 from typing import TYPE_CHECKING
 from unittest.mock import MagicMock
@@ -8,11 +11,13 @@
 
 import pytest
 from django.db.models import DecimalField
+from freezegun import freeze_time
 from pytest_django.asserts import assertQuerysetEqual
 
 from django_afip import exceptions
 from django_afip import factories
 from django_afip import models
+from django_afip.clients import TZ_AR
 from django_afip.factories import ReceiptFactory
 from django_afip.factories import ReceiptFCEAWithVatAndTaxFactory
 from django_afip.factories import ReceiptFCEAWithVatTaxAndOptionalsFactory
@@ -456,3 +461,101 @@ def test_receipt_entry_manage_decimal_quantities() -> None:
     last = models.ReceiptEntry.objects.last()
     assert last is not None
     assert last.quantity == Decimal("5.23")
+
+
+@pytest.mark.django_db()
+@freeze_time("2023-11-16 18:39:40")
+def test_approximate_noop_today() -> None:
+    today = datetime.now(TZ_AR).date()
+
+    factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=20))
+
+    receipt = factories.ReceiptFactory(issued_date=today)
+    changed = receipt.approximate_date()
+
+    assert changed is False
+    assert receipt.issued_date == today
+
+
+@pytest.mark.django_db()
+@freeze_time("2023-11-16 18:39:40")
+def test_approximate_noop_two_days_ago() -> None:
+    today = datetime.now(TZ_AR).date()
+    two_days_ago = date(2023, 11, 14)
+
+    factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=20))
+
+    receipt = factories.ReceiptFactory(issued_date=two_days_ago)
+    changed = receipt.approximate_date()
+
+    assert changed is False
+    assert receipt.issued_date == two_days_ago
+
+
+@pytest.mark.django_db()
+@freeze_time("2023-11-16 18:39:40")
+def test_approximate_date_today_with_most_recent() -> None:
+    today = datetime.now(TZ_AR).date()
+
+    factories.ReceiptWithApprovedValidation(issued_date=today)
+
+    receipt = factories.ReceiptFactory(issued_date=today - timedelta(days=30))
+    changed = receipt.approximate_date()
+
+    assert changed is True
+    assert receipt.issued_date == date(2023, 11, 16)
+
+
+@pytest.mark.django_db()
+@freeze_time("2023-11-16 18:39:40")
+def test_approximate_date_yesterday_with_most_recent() -> None:
+    today = datetime.now(TZ_AR).date()
+
+    factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=1))
+
+    receipt = factories.ReceiptFactory(issued_date=today - timedelta(days=30))
+    changed = receipt.approximate_date()
+
+    assert changed is True
+    assert receipt.issued_date == date(2023, 11, 15)
+
+
+@pytest.mark.django_db()
+@freeze_time("2023-11-16 18:39:40")
+def test_approximate_date_30_days_ago_with_most_recent_20_days_ago() -> None:
+    today = datetime.now(TZ_AR).date()
+
+    factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=20))
+
+    receipt = factories.ReceiptFactory(issued_date=today - timedelta(days=30))
+    changed = receipt.approximate_date()
+
+    assert changed is True
+    assert receipt.issued_date == date(2023, 11, 2)
+
+
+@pytest.mark.django_db()
+@freeze_time("2023-11-16 18:39:40")
+def test_approximate_date_2_days_ago_with_most_recent_20_days_ago() -> None:
+    today = datetime.now(TZ_AR).date()
+    two_days_ago = date(2023, 11, 14)
+
+    factories.ReceiptWithApprovedValidation(issued_date=today - timedelta(days=20))
+
+    receipt = factories.ReceiptFactory(issued_date=two_days_ago)
+    changed = receipt.approximate_date()
+
+    assert changed is False
+    assert receipt.issued_date == two_days_ago
+
+
+@pytest.mark.django_db()
+@freeze_time("2023-11-16 18:39:40")
+def test_approximate_date_two_days_ago_without_most_recent() -> None:
+    two_days_ago = date(2023, 11, 14)
+
+    receipt = factories.ReceiptFactory(issued_date=two_days_ago)
+    changed = receipt.approximate_date()
+
+    assert changed is False
+    assert receipt.issued_date == two_days_ago