diff --git a/apps/xero/migrations/0010_bill_is_retired.py b/apps/xero/migrations/0010_bill_is_retired.py new file mode 100644 index 00000000..64313010 --- /dev/null +++ b/apps/xero/migrations/0010_bill_is_retired.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2024-09-03 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('xero', '0009_auto_20220614_1320'), + ] + + operations = [ + migrations.AddField( + model_name='bill', + name='is_retired', + field=models.BooleanField(default=False, help_text='Is Payment sync retried'), + ), + ] diff --git a/apps/xero/models.py b/apps/xero/models.py index d83a1214..09bae97a 100644 --- a/apps/xero/models.py +++ b/apps/xero/models.py @@ -202,6 +202,7 @@ class Bill(models.Model): paid_on_xero = models.BooleanField( help_text="Payment status in Xero", default=False ) + is_retired = models.BooleanField(help_text='Is Payment sync retried', default=False) export_id = models.CharField(max_length=255, help_text="Export ID", null=True) created_at = models.DateTimeField(auto_now_add=True, help_text="Created at") updated_at = models.DateTimeField(auto_now=True, help_text="Updated at") diff --git a/apps/xero/tasks.py b/apps/xero/tasks.py index 53d92c52..4832b11b 100644 --- a/apps/xero/tasks.py +++ b/apps/xero/tasks.py @@ -4,6 +4,9 @@ from time import sleep from typing import List +from dateutil.relativedelta import relativedelta +from django.utils import timezone as django_timezone + from django.db import transaction from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, Mapping from fyle_integrations_platform_connector import PlatformConnector @@ -699,6 +702,30 @@ def process_payments( task_log.save() +def validate_for_skipping_payment(bill: Bill, workspace_id: int): + task_log = TaskLog.objects.filter(task_id='PAYMENT_{}'.format(bill.expense_group.id), workspace_id=workspace_id, type='CREATING_PAYMENT').first() + if task_log: + now = django_timezone.now() + + if now - relativedelta(months=2) > task_log.created_at: + bill.is_retired = True + bill.save() + return True + + elif now - relativedelta(months=1) > task_log.created_at and now - relativedelta(months=2) < task_log.created_at: + # if updated_at is within 1 months will be skipped + if task_log.updated_at > now - relativedelta(months=1): + return True + + # If created is within 1 month + elif now - relativedelta(months=1) < task_log.created_at: + # Skip if updated within the last week + if task_log.updated_at > now - relativedelta(weeks=1): + return True + + return False + + def create_payment(workspace_id): fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) @@ -722,6 +749,11 @@ def create_payment(workspace_id): ) if expense_group_reimbursement_status: + + skip_payment = validate_for_skipping_payment(bill=bill, workspace_id=workspace_id) + if skip_payment: + continue + task_log, _ = TaskLog.objects.update_or_create( workspace_id=workspace_id, task_id="PAYMENT_{}".format(bill.expense_group.id), diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index 835b4b2e..d7b6a151 100644 --- a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql +++ b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version 15.7 (Debian 15.7-1.pgdg120+1) --- Dumped by pg_dump version 15.7 (Debian 15.7-1.pgdg120+1) +-- Dumped by pg_dump version 15.8 (Debian 15.8-1.pgdg120+1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -298,7 +298,8 @@ CREATE TABLE public.bills ( expense_group_id integer NOT NULL, paid_on_xero boolean NOT NULL, payment_synced boolean NOT NULL, - export_id character varying(255) + export_id character varying(255), + is_retired boolean NOT NULL ); @@ -2245,11 +2246,11 @@ COPY public.bill_lineitems (id, tracking_categories, item_code, account_id, desc -- Data for Name: bills; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.bills (id, currency, contact_id, reference, date, created_at, updated_at, expense_group_id, paid_on_xero, payment_synced, export_id) FROM stdin; -1 USD 9eecdd86-78bb-47c9-95df-986369748151 2 - ashwin.t@fyle.in 2022-08-02 00:00:00+00 2022-08-02 20:27:44.058777+00 2022-08-02 20:27:44.877194+00 2 f f c35cf4b3-784a-408b-9ddf-df111dd2e073 -2 USD 229b7701-21a2-4539-b39e-5c34f56e1711 4 - sravan.kumar@fyle.in 2022-08-02 00:00:00+00 2022-08-02 20:27:48.290698+00 2022-08-02 20:27:48.932223+00 4 f f 2780aebc-2f8c-4b47-a7e2-64b920c5e7c1 -3 USD 9eecdd86-78bb-47c9-95df-986369748151 1 - ashwin.t@fyle.in 2022-08-02 00:00:00+00 2022-08-02 20:27:51.443082+00 2022-08-02 20:27:52.020404+00 1 f f c70ce61b-5157-4e11-97c7-6d1f843b2a5f -4 USD 9eecdd86-78bb-47c9-95df-986369748151 3 - ashwin.t@fyle.in 2022-08-02 00:00:00+00 2022-08-02 20:27:54.558597+00 2022-08-02 20:27:55.12968+00 3 f f 9520557e-c20c-4fa4-b4b4-702102866beb +COPY public.bills (id, currency, contact_id, reference, date, created_at, updated_at, expense_group_id, paid_on_xero, payment_synced, export_id, is_retired) FROM stdin; +1 USD 9eecdd86-78bb-47c9-95df-986369748151 2 - ashwin.t@fyle.in 2022-08-02 00:00:00+00 2022-08-02 20:27:44.058777+00 2022-08-02 20:27:44.877194+00 2 f f c35cf4b3-784a-408b-9ddf-df111dd2e073 f +2 USD 229b7701-21a2-4539-b39e-5c34f56e1711 4 - sravan.kumar@fyle.in 2022-08-02 00:00:00+00 2022-08-02 20:27:48.290698+00 2022-08-02 20:27:48.932223+00 4 f f 2780aebc-2f8c-4b47-a7e2-64b920c5e7c1 f +3 USD 9eecdd86-78bb-47c9-95df-986369748151 1 - ashwin.t@fyle.in 2022-08-02 00:00:00+00 2022-08-02 20:27:51.443082+00 2022-08-02 20:27:52.020404+00 1 f f c70ce61b-5157-4e11-97c7-6d1f843b2a5f f +4 USD 9eecdd86-78bb-47c9-95df-986369748151 3 - ashwin.t@fyle.in 2022-08-02 00:00:00+00 2022-08-02 20:27:54.558597+00 2022-08-02 20:27:55.12968+00 3 f f 9520557e-c20c-4fa4-b4b4-702102866beb f \. @@ -2632,6 +2633,7 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 151 workspaces 0038_alter_workspace_onboarding_state 2024-05-07 09:15:02.787555+00 152 fyle 0019_expense_paid_on_fyle 2024-06-18 16:26:06.802359+00 153 fyle 0020_expensegroup_export_url 2024-08-03 14:33:54.988078+00 +154 xero 0010_bill_is_retired 2024-09-03 11:40:50.93784+00 \. @@ -5169,7 +5171,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 40, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_migrations_id_seq', 153, true); +SELECT pg_catalog.setval('public.django_migrations_id_seq', 154, true); -- @@ -6786,3 +6788,4 @@ ALTER TABLE ONLY public.xero_credentials -- -- PostgreSQL database dump complete -- + diff --git a/tests/test_xero/test_tasks.py b/tests/test_xero/test_tasks.py index 367e84e9..79ac9022 100644 --- a/tests/test_xero/test_tasks.py +++ b/tests/test_xero/test_tasks.py @@ -1,6 +1,6 @@ import logging import random -from datetime import datetime +from datetime import datetime, timedelta, timezone from unittest import mock from django_q.models import Schedule @@ -746,6 +746,11 @@ def test_create_payment_exceptions(mocker, db): mock_call.side_effect = WrongParamsError( msg="wrong parameter", response="invalid parameter" ) + + now = datetime.now().replace(tzinfo=timezone.utc) + updated_at = now - timedelta(days=10) + + task_log = TaskLog.objects.filter(task_id='PAYMENT_{}'.format(bill.expense_group.id)).update(updated_at=updated_at) create_payment(workspace_id) task_log = TaskLog.objects.filter( workspace_id=workspace_id, detail="wrong parameter" @@ -1105,3 +1110,64 @@ def test_skipping_schedule_bank_transaction_creation(db): task_log = TaskLog.objects.filter(expense_group_id=expense_group.id).first() assert task_log.type == 'CREATING_BANK_TRANSACTION' + + +def test_skipping_payment(mocker, db): + mocker.patch("apps.xero.utils.XeroConnector.post_payment", return_value={}) + workspace_id = 1 + + mocker.patch( + "fyle.platform.apis.v1beta.admin.Reimbursements.list_all", + return_value=fyle_data["get_all_reimbursements"], + ) + + mocker.patch('fyle_integrations_platform_connector.apis.Expenses.get', return_value=data['expense']) + + bills = Bill.objects.all() + expenses = [] + + for bill in bills: + expenses.extend(bill.expense_group.expenses.all()) + + for expense in expenses: + Reimbursement.objects.update_or_create( + settlement_id=expense.settlement_id, + reimbursement_id="qwertyuio", + state="COMPLETE", + workspace_id=workspace_id, + ) + + general_mappings = GeneralMapping.objects.filter(workspace_id=workspace_id).first() + general_mappings.payment_account_id = "2" + general_mappings.save() + + task_log = TaskLog.objects.create(workspace_id=workspace_id, type='CREATING_PAYMENT', task_id='PAYMENT_{}'.format(bill.expense_group.id), status='FAILED') + updated_at = task_log.updated_at + create_payment(workspace_id) + + task_log = TaskLog.objects.get(workspace_id=workspace_id, type='CREATING_PAYMENT', task_id='PAYMENT_{}'.format(bill.expense_group.id)) + assert task_log.updated_at == updated_at + + now = datetime.now().replace(tzinfo=timezone.utc) + updated_at = now - timedelta(days=25) + # Update created_at to more than 2 months ago (more than 60 days) + TaskLog.objects.filter(task_id='PAYMENT_{}'.format(bill.expense_group.id)).update( + created_at=now - timedelta(days=61), # More than 2 months ago + updated_at=updated_at # Updated within the last 1 month + ) + + task_log = TaskLog.objects.get(task_id='PAYMENT_{}'.format(bill.expense_group.id)) + + create_payment(workspace_id) + task_log.refresh_from_db() + assert task_log.updated_at == updated_at + + updated_at = now - timedelta(days=25) + # Update created_at to between 1 and 2 months ago (between 30 and 60 days) + TaskLog.objects.filter(task_id='PAYMENT_{}'.format(bill.expense_group.id)).update( + created_at=now - timedelta(days=45), # Between 1 and 2 months ago + updated_at=updated_at # Updated within the last 1 month + ) + create_payment(workspace_id) + task_log.refresh_from_db() + assert task_log.updated_at == updated_at