diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 8273bcad..64c3bdb9 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -16,7 +16,7 @@ jobs: run: | docker compose -f docker-compose-pipeline.yml build docker compose -f docker-compose-pipeline.yml up -d - docker compose -f docker-compose-pipeline.yml exec -T api pytest tests/ --cov --cov-report=xml --cov-fail-under=94 --junit-xml=test-reports/report.xml + docker compose -f docker-compose-pipeline.yml exec -T api pytest tests/ --cov --cov-report=xml --cov-fail-under=93 --junit-xml=test-reports/report.xml echo "STATUS=$(cat pytest-coverage.txt | grep 'Required test' | awk '{ print $1 }')" >> $GITHUB_ENV echo "FAILED=$(cat test-reports/report.xml | awk -F'=' '{print $5}' | awk -F' ' '{gsub(/"/, "", $1); print $1}')" >> $GITHUB_ENV env: diff --git a/apps/quickbooks_online/migrations/0016_bill_is_retired.py b/apps/quickbooks_online/migrations/0016_bill_is_retired.py new file mode 100644 index 00000000..68baf46e --- /dev/null +++ b/apps/quickbooks_online/migrations/0016_bill_is_retired.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2024-09-03 15:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('quickbooks_online', '0015_add_bill_number'), + ] + + operations = [ + migrations.AddField( + model_name='bill', + name='is_retired', + field=models.BooleanField(default=False, help_text='Is Payment sync retried'), + ), + ] diff --git a/apps/quickbooks_online/models.py b/apps/quickbooks_online/models.py index cfbf250a..18a94dd4 100644 --- a/apps/quickbooks_online/models.py +++ b/apps/quickbooks_online/models.py @@ -213,6 +213,7 @@ class Bill(models.Model): private_note = models.TextField(help_text='Bill Description') payment_synced = models.BooleanField(help_text='Payment synced status', default=False) paid_on_qbo = models.BooleanField(help_text='Payment status in QBO', default=False) + is_retired = models.BooleanField(help_text='Is Payment sync retried', default=False) exchange_rate = models.FloatField(help_text='Exchange rate', 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/quickbooks_online/tasks.py b/apps/quickbooks_online/tasks.py index 315ad8d5..bf54844d 100644 --- a/apps/quickbooks_online/tasks.py +++ b/apps/quickbooks_online/tasks.py @@ -4,6 +4,9 @@ from datetime import datetime, timezone 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, EmployeeMapping, ExpenseAttribute, Mapping from fyle_integrations_platform_connector import PlatformConnector @@ -643,6 +646,30 @@ def process_bill_payments(bill: Bill, workspace_id: int, task_log: TaskLog): 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_BILL_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_bill_payment(workspace_id): fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) @@ -656,6 +683,9 @@ def create_bill_payment(workspace_id): for bill in bills: expense_group_reimbursement_status = check_expenses_reimbursement_status(bill.expense_group.expenses.all(), workspace_id=workspace_id, platform=platform, filter_credit_expenses=filter_credit_expenses) 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), defaults={'status': 'IN_PROGRESS', 'type': 'CREATING_BILL_PAYMENT'}) process_bill_payments(bill, workspace_id, task_log) diff --git a/tests/sql_fixtures/migration_fixtures/create_migration.sh b/tests/sql_fixtures/migration_fixtures/create_migration.sh index be6960de..e5734643 100644 --- a/tests/sql_fixtures/migration_fixtures/create_migration.sh +++ b/tests/sql_fixtures/migration_fixtures/create_migration.sh @@ -10,7 +10,7 @@ bash tests/sql_fixtures/reset_db_fixtures/reset_db.sh export DATABASE_URL=postgres://postgres:postgres@db:5432/test_qbo_db # # Running migrations on the fixture database -python manage.py migrate +# python manage.py migrate read -p "Add SQL script paths separated by spaces if any, else press enter to continue? " scripts diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index 5248d558..543072d2 100644 --- a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql +++ b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql @@ -264,7 +264,8 @@ CREATE TABLE public.bills ( paid_on_qbo boolean NOT NULL, payment_synced boolean NOT NULL, exchange_rate double precision, - bill_number character varying(255) + bill_number character varying(255), + is_retired boolean NOT NULL ); @@ -2638,7 +2639,7 @@ COPY public.bill_payments (id, private_note, vendor_id, amount, currency, paymen -- Data for Name: bills; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.bills (id, accounts_payable_id, vendor_id, department_id, transaction_date, currency, private_note, created_at, updated_at, expense_group_id, paid_on_qbo, payment_synced, exchange_rate, bill_number) FROM stdin; +COPY public.bills (id, accounts_payable_id, vendor_id, department_id, transaction_date, currency, private_note, created_at, updated_at, expense_group_id, paid_on_qbo, payment_synced, exchange_rate, bill_number, is_retired) FROM stdin; \. @@ -2916,6 +2917,7 @@ COPY public.destination_attributes (id, attribute_type, display_name, value, des 225 ACCOUNTS_PAYABLE Accounts Payable Sales of Product Income 79 2022-05-23 04:13:50.086029+00 2022-05-23 04:13:50.086076+00 2 t {"account_type": "Income", "fully_qualified_name": "Sales of Product Income"} f \N 226 ACCOUNTS_PAYABLE Accounts Payable Services 1 2022-05-23 04:13:50.086195+00 2022-05-23 04:13:50.086238+00 2 t {"account_type": "Income", "fully_qualified_name": "Services"} f \N 227 ACCOUNTS_PAYABLE Accounts Payable Stationery & Printing 19 2022-05-23 04:13:50.086351+00 2022-05-23 04:13:50.086392+00 2 t {"account_type": "Expense", "fully_qualified_name": "Stationery & Printing"} f \N +830 CLASS class cc2 5000000000000142240 2022-05-23 11:33:57.280177+00 2022-05-23 11:33:57.280207+00 4 t \N f \N 228 ACCOUNTS_PAYABLE Accounts Payable Supplies 20 2022-05-23 04:13:50.08669+00 2022-05-23 04:13:50.08674+00 2 t {"account_type": "Expense", "fully_qualified_name": "Supplies"} f \N 229 ACCOUNTS_PAYABLE Accounts Payable Taxes & Licenses 21 2022-05-23 04:13:50.086858+00 2022-05-23 04:13:50.086901+00 2 t {"account_type": "Expense", "fully_qualified_name": "Taxes & Licenses"} f \N 230 ACCOUNTS_PAYABLE Accounts Payable Travel 22 2022-05-23 04:13:50.087009+00 2022-05-23 04:13:50.087126+00 2 t {"account_type": "Expense", "fully_qualified_name": "Travel"} f \N @@ -3284,6 +3286,7 @@ COPY public.destination_attributes (id, attribute_type, display_name, value, des 593 ACCOUNT Account Cost of Goods Sold 80 2022-05-23 11:21:16.959749+00 2022-05-23 11:21:16.959775+00 3 t {"account_type": "Cost of Goods Sold", "fully_qualified_name": "Cost of Goods Sold"} f \N 594 ACCOUNT Account Depreciation 40 2022-05-23 11:21:16.959937+00 2022-05-23 11:21:16.959965+00 3 t {"account_type": "Other Expense", "fully_qualified_name": "Depreciation"} f \N 595 ACCOUNT Account Disposal Fees 28 2022-05-23 11:21:16.960029+00 2022-05-23 11:21:16.960057+00 3 t {"account_type": "Expense", "fully_qualified_name": "Disposal Fees"} f \N +755 VENDOR vendor Brosnahan Insurance Agency 31 2022-05-23 11:33:51.369394+00 2022-05-23 11:33:51.369422+00 4 t {"email": null} f \N 596 ACCOUNT Account Dues & Subscriptions 10 2022-05-23 11:21:16.96012+00 2022-05-23 11:21:16.960148+00 3 t {"account_type": "Expense", "fully_qualified_name": "Dues & Subscriptions"} f \N 597 ACCOUNT Account Equipment Rental 29 2022-05-23 11:21:16.960211+00 2022-05-23 11:21:16.960239+00 3 t {"account_type": "Expense", "fully_qualified_name": "Equipment Rental"} f \N 598 ACCOUNT Account Insurance 11 2022-05-23 11:21:16.960303+00 2022-05-23 11:21:16.960331+00 3 t {"account_type": "Expense", "fully_qualified_name": "Insurance"} f \N @@ -3364,6 +3367,7 @@ COPY public.destination_attributes (id, attribute_type, display_name, value, des 665 ACCOUNTS_PAYABLE Accounts Payable Board of Equalization Payable 90 2022-05-23 11:33:46.953486+00 2022-05-23 11:33:46.953514+00 4 t {"account_type": "Other Current Liability", "fully_qualified_name": "Board of Equalization Payable"} f \N 666 ACCOUNTS_PAYABLE Accounts Payable Commissions & fees 9 2022-05-23 11:33:46.953577+00 2022-05-23 11:33:46.953727+00 4 t {"account_type": "Expense", "fully_qualified_name": "Commissions & fees"} f \N 667 ACCOUNTS_PAYABLE Accounts Payable Cost of Goods Sold 80 2022-05-23 11:33:46.953807+00 2022-05-23 11:33:46.953835+00 4 t {"account_type": "Cost of Goods Sold", "fully_qualified_name": "Cost of Goods Sold"} f \N +756 VENDOR vendor Cal Telephone 32 2022-05-23 11:33:51.369484+00 2022-05-23 11:33:51.369512+00 4 t {"email": null} f \N 668 ACCOUNTS_PAYABLE Accounts Payable Depreciation 40 2022-05-23 11:33:46.953898+00 2022-05-23 11:33:46.953926+00 4 t {"account_type": "Other Expense", "fully_qualified_name": "Depreciation"} f \N 669 ACCOUNTS_PAYABLE Accounts Payable Design income 82 2022-05-23 11:33:46.953989+00 2022-05-23 11:33:46.954017+00 4 t {"account_type": "Income", "fully_qualified_name": "Design income"} f \N 670 ACCOUNTS_PAYABLE Accounts Payable Discounts given 86 2022-05-23 11:33:46.95408+00 2022-05-23 11:33:46.954107+00 4 t {"account_type": "Income", "fully_qualified_name": "Discounts given"} f \N @@ -3401,6 +3405,7 @@ COPY public.destination_attributes (id, attribute_type, display_name, value, des 707 ACCOUNTS_PAYABLE Accounts Payable Maintenance and Repair:Equipment Repairs 75 2022-05-23 11:33:46.965624+00 2022-05-23 11:33:46.965846+00 4 t {"account_type": "Expense", "fully_qualified_name": "Maintenance and Repair:Equipment Repairs"} f \N 708 ACCOUNTS_PAYABLE Accounts Payable Meals and Entertainment 13 2022-05-23 11:33:46.965977+00 2022-05-23 11:33:46.966009+00 4 t {"account_type": "Expense", "fully_qualified_name": "Meals and Entertainment"} f \N 709 ACCOUNTS_PAYABLE Accounts Payable Miscellaneous 14 2022-05-23 11:33:46.966294+00 2022-05-23 11:33:46.96634+00 4 t {"account_type": "Other Expense", "fully_qualified_name": "Miscellaneous"} f \N +757 VENDOR vendor Chin's Gas and Oil 33 2022-05-23 11:33:51.369573+00 2022-05-23 11:33:51.369601+00 4 t {"email": null} f \N 710 ACCOUNTS_PAYABLE Accounts Payable Notes Payable 44 2022-05-23 11:33:46.966481+00 2022-05-23 11:33:46.966513+00 4 t {"account_type": "Long Term Liability", "fully_qualified_name": "Notes Payable"} f \N 711 ACCOUNTS_PAYABLE Accounts Payable Office Expenses 15 2022-05-23 11:33:46.966588+00 2022-05-23 11:33:46.96673+00 4 t {"account_type": "Expense", "fully_qualified_name": "Office Expenses"} f \N 712 ACCOUNTS_PAYABLE Accounts Payable Opening Balance Equity 34 2022-05-23 11:33:46.966996+00 2022-05-23 11:33:46.96716+00 4 t {"account_type": "Equity", "fully_qualified_name": "Opening Balance Equity"} f \N @@ -3446,9 +3451,6 @@ COPY public.destination_attributes (id, attribute_type, display_name, value, des 752 VENDOR vendor Bob's Burger Joint 56 2022-05-23 11:33:51.369122+00 2022-05-23 11:33:51.36915+00 4 t {"email": null} f \N 753 VENDOR vendor Books by Bessie 30 2022-05-23 11:33:51.369213+00 2022-05-23 11:33:51.369241+00 4 t {"email": "Books@Intuit.com"} f \N 754 VENDOR vendor Brian Foster 76 2022-05-23 11:33:51.369303+00 2022-05-23 11:33:51.369331+00 4 t {"email": "user2@fyleforfyleforme.org"} f \N -755 VENDOR vendor Brosnahan Insurance Agency 31 2022-05-23 11:33:51.369394+00 2022-05-23 11:33:51.369422+00 4 t {"email": null} f \N -756 VENDOR vendor Cal Telephone 32 2022-05-23 11:33:51.369484+00 2022-05-23 11:33:51.369512+00 4 t {"email": null} f \N -757 VENDOR vendor Chin's Gas and Oil 33 2022-05-23 11:33:51.369573+00 2022-05-23 11:33:51.369601+00 4 t {"email": null} f \N 758 VENDOR vendor Cigna Health Care 34 2022-05-23 11:33:51.369761+00 2022-05-23 11:33:51.369789+00 4 t {"email": null} f \N 759 VENDOR vendor Computers by Jenni 35 2022-05-23 11:33:51.369852+00 2022-05-23 11:33:51.36988+00 4 t {"email": "Msfixit@Intuit.com"} f \N 760 VENDOR vendor Credit Card Misc 74 2022-05-23 11:33:51.369942+00 2022-05-23 11:33:51.369969+00 4 t {"email": null} f \N @@ -3516,7 +3518,6 @@ COPY public.destination_attributes (id, attribute_type, display_name, value, des 827 CUSTOMER customer Weiskopf Consulting 29 2022-05-23 11:33:54.828007+00 2022-05-23 11:33:54.828035+00 4 t \N f \N 828 CLASS class Adidas 5000000000000142238 2022-05-23 11:33:57.279868+00 2022-05-23 11:33:57.279915+00 4 t \N f \N 829 CLASS class cc1 5000000000000142239 2022-05-23 11:33:57.280087+00 2022-05-23 11:33:57.280118+00 4 t \N f \N -830 CLASS class cc2 5000000000000142240 2022-05-23 11:33:57.280177+00 2022-05-23 11:33:57.280207+00 4 t \N f \N 831 CLASS class Coachella 5000000000000142241 2022-05-23 11:33:57.280265+00 2022-05-23 11:33:57.280295+00 4 t \N f \N 832 CLASS class Radio 5000000000000142242 2022-05-23 11:33:57.280352+00 2022-05-23 11:33:57.280382+00 4 t \N f \N 833 DEPARTMENT Department Bangalore 2 2022-05-23 11:34:01.996179+00 2022-05-23 11:34:01.996254+00 4 t \N f \N @@ -3602,6 +3603,7 @@ COPY public.destination_attributes (id, attribute_type, display_name, value, des 897 CREDIT_CARD_ACCOUNT Credit Card Account 2285 Fyle Credit Card 106 2022-05-25 14:39:12.017261+00 2022-05-25 14:39:12.017339+00 5 t {"account_type": "Credit Card", "fully_qualified_name": "2285 Fyle Credit Card"} f \N 898 CREDIT_CARD_ACCOUNT Credit Card Account 3420 Fyle Credit Card 107 2022-05-25 14:39:12.017495+00 2022-05-25 14:39:12.017545+00 5 t {"account_type": "Credit Card", "fully_qualified_name": "3420 Fyle Credit Card"} f \N 899 CREDIT_CARD_ACCOUNT Credit Card Account Credit Card 103 2022-05-25 14:39:12.017683+00 2022-05-25 14:39:12.017731+00 5 t {"account_type": "Credit Card", "fully_qualified_name": "Credit Card"} f \N +1022 CUSTOMER customer Whitehead and Sons 5 2022-05-25 14:39:19.866292+00 2022-05-25 14:39:19.866335+00 5 t \N f \N 900 BANK_ACCOUNT Bank Account Auto 95 2022-05-25 14:39:12.032124+00 2022-05-25 14:39:12.032169+00 5 t {"account_type": "Bank", "fully_qualified_name": "Auto"} f \N 901 BANK_ACCOUNT Bank Account Cash on hand 94 2022-05-25 14:39:12.032243+00 2022-05-25 14:39:12.032274+00 5 t {"account_type": "Bank", "fully_qualified_name": "Cash on hand"} f \N 902 BANK_ACCOUNT Bank Account Current 81 2022-05-25 14:39:12.032344+00 2022-05-25 14:39:12.032375+00 5 t {"account_type": "Bank", "fully_qualified_name": "Current"} f \N @@ -3714,7 +3716,6 @@ COPY public.destination_attributes (id, attribute_type, display_name, value, des 1019 CUSTOMER customer Oxon Insurance Agency:Oxon - Retreat 2014 62 2022-05-25 14:39:19.865835+00 2022-05-25 14:39:19.865864+00 5 t \N f \N 1020 CUSTOMER customer Rob deMontarnal 22 2022-05-25 14:39:19.865921+00 2022-05-25 14:39:19.86607+00 5 t \N f \N 1021 CUSTOMER customer Vendor KSKS 90 2022-05-25 14:39:19.866163+00 2022-05-25 14:39:19.866204+00 5 t \N f \N -1022 CUSTOMER customer Whitehead and Sons 5 2022-05-25 14:39:19.866292+00 2022-05-25 14:39:19.866335+00 5 t \N f \N 1023 CUSTOMER customer Whitehead and Sons:QBO 77 2022-05-25 14:39:19.866426+00 2022-05-25 14:39:19.866471+00 5 t \N f \N 1024 CUSTOMER customer Whitehead and Sons:Whitehead - Employee celebration 60 2022-05-25 14:39:19.866563+00 2022-05-25 14:39:19.866638+00 5 t \N f \N 1025 CLASS class Adidas 5100000000000030664 2022-05-25 14:39:22.066011+00 2022-05-25 14:39:22.066058+00 5 t \N f \N @@ -4045,6 +4046,8 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 187 workspaces 0046_workspacegeneralsettings_import_code_fields 2024-08-02 07:52:56.494868+00 188 fyle_accounting_mappings 0026_destinationattribute_code 2024-08-02 08:35:52.537882+00 189 quickbooks_online 0015_add_bill_number 2024-08-29 14:29:50.588003+00 +190 quickbooks_online 0015_bill_is_retired 2024-09-03 15:08:15.085332+00 +191 fyle 0038_expensegroup_export_url 2024-08-03 14:24:57.600169+00 \. @@ -33973,7 +33976,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 47, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_migrations_id_seq', 188, true); +SELECT pg_catalog.setval('public.django_migrations_id_seq', 191, true); -- diff --git a/tests/test_quickbooks_online/test_tasks.py b/tests/test_quickbooks_online/test_tasks.py index 3d59fad2..eaedff3f 100644 --- a/tests/test_quickbooks_online/test_tasks.py +++ b/tests/test_quickbooks_online/test_tasks.py @@ -1,7 +1,7 @@ import json import logging import random -from datetime import datetime +from datetime import datetime, timedelta, timezone from unittest import mock from django_q.models import Schedule @@ -1286,3 +1286,71 @@ def test_skipping_cheque_creation(db, mocker): task_log = TaskLog.objects.filter(expense_group_id=expense_group.id).first() assert task_log.type == 'CREATING_CHECK' + + +def test_skipping_bill_payment(mocker, db): + mocker.patch('apps.quickbooks_online.tasks.load_attachments', return_value=[]) + mocker.patch('fyle_integrations_platform_connector.apis.Reimbursements.sync', return_value=None) + mocker.patch('fyle_integrations_platform_connector.apis.Expenses.get', return_value=[]) + mocker.patch('qbosdk.apis.Bills.post', return_value=data['post_bill']) + mocker.patch('qbosdk.apis.BillPayments.post', return_value=data['post_bill']) + mocker.patch('qbosdk.apis.Attachments.post', return_value=None) + workspace_id = 3 + task_log = TaskLog.objects.filter(workspace_id=workspace_id).first() + task_log.status = 'READY' + task_log.save() + + expense_group = ExpenseGroup.objects.get(id=14) + expenses = expense_group.expenses.all() + + expense_group.id = random.randint(100, 1500000) + expense_group.save() + + for expense in expenses: + expense.expense_group_id = expense_group.id + expense.save() + + expense_group.expenses.set(expenses) + expense_group.save() + + create_bill(expense_group, task_log.id, False) + + bill = Bill.objects.last() + task_log = TaskLog.objects.get(id=task_log.id) + task_log.expense_group = bill.expense_group + task_log.save() + + reimbursements = data['reimbursements'] + + Reimbursement.create_or_update_reimbursement_objects(reimbursements=reimbursements, workspace_id=workspace_id) + + task_log = TaskLog.objects.create(workspace_id=workspace_id, type='CREATING_BILL_PAYMENT', task_id='PAYMENT_{}'.format(bill.expense_group.id), status='FAILED') + updated_at = task_log.updated_at + create_bill_payment(workspace_id) + + task_log = TaskLog.objects.get(workspace_id=workspace_id, type='CREATING_BILL_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_bill_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_bill_payment(workspace_id) + task_log.refresh_from_db() + assert task_log.updated_at == updated_at