Skip to content

Reccur - #33

Merged
paperplate merged 8 commits into
mainfrom
reccur
Jun 11, 2026
Merged

Reccur#33
paperplate merged 8 commits into
mainfrom
reccur

Conversation

@paperplate

@paperplate paperplate commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Implement #28

Summary by CodeRabbit

  • New Features

    • Added recurring transactions with daily/weekly/monthly/yearly frequencies
    • New "Recurring" page to create/manage recurring transactions and a manual "Run Recurring" action in Settings
    • Background scheduler to auto-generate transactions from active recurring entries
    • "Recurring" badge shown for transactions linked to a recurring entry
  • Tests

    • UI and API tests added for recurring workflows and scheduler behavior

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds recurring transactions: data model and enum, scheduled background processing (APScheduler + process_recurring_transactions), CRUD routes and settings trigger, frontend templates and JS/TS UI with Tagify, build wiring, and comprehensive unit/E2E tests.

Changes

Recurring Transactions Feature

Layer / File(s) Summary
Data model and DTOs
JustAnotherExpenseManager/models/dtos.py, JustAnotherExpenseManager/models/__init__.py
Adds RecurringFrequency enum, recurring_transaction_tags association table, RecurringTransaction ORM model, updates Tag/Transaction timestamp defaults, adds Transaction.recurring_id and includes it in to_dict().
Scheduler and background processing
JustAnotherExpenseManager/utils/scheduler.py, JustAnotherExpenseManager/utils/services.py, JustAnotherExpenseManager/__init__.py
Installs APScheduler, initializes it at app startup, registers a daily cron job, and implements process_recurring_transactions() to spawn Transaction rows for due recurring items and advance next_date according to frequency (including month/year edge handling).
API routes and settings endpoint
JustAnotherExpenseManager/routes/recurring.py, JustAnotherExpenseManager/routes/settings.py, JustAnotherExpenseManager/static/js/settings.js, static_src/js/settings.ts
Adds recurring blueprint with page (GET /recurring), JSON endpoints (GET /recurring/api, POST create, DELETE), and a settings POST /api/run-recurring to trigger processing; includes request parsing, enum/date mapping, tag/category wiring, and client-side trigger UI.
Frontend templates and build wiring
JustAnotherExpenseManager/templates/recurring.html, JustAnotherExpenseManager/templates/base.html, JustAnotherExpenseManager/templates/transactions_list.html, vite.config.ts, JustAnotherExpenseManager/templates/settings.html
Adds recurring.html with Tagify styling and form, nav link in base, “Recurring” badge in transactions list, settings UI, and registers recurring entry point in Vite build.
Frontend JS/TS implementation
JustAnotherExpenseManager/static/js/recurring.js, static_src/js/recurring.ts, static_src/js/types.ts
Implements client-side recurring UI: loads categories/tags, initializes Tagify, renders recurring list from /recurring/api, handles create/delete, exposes functions on window, and adds RecurringTransaction TS interface and Window globals.
Tests, fixtures, and E2E
tests/test_recurring.py, tests/conftest.py, tests/pages/RecurringPage.ts, tests/fixtures.ts, tests/12-recurring-transactions.spec.ts
Adds API and service tests for recurring routes and processing (including edge cases for monthly/yearly schedules), Playwright page object and spec, and updates test cleanup and fixtures to handle recurring tables.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Reccur" is vague and generic, appearing to be a misspelled or abbreviated form of "recurring" with no clear indication of the actual changes or objectives. Replace with a clear, descriptive title such as "Add recurring transactions feature" or "Implement recurring transaction scheduling with background processing".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch reccur

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (9)
JustAnotherExpenseManager/models/__init__.py (1)

197-201: ⚡ Quick win

Add default_factory=list to RecurringTransaction.tags for consistency.

The Transaction.tags relationship includes default_factory=list (line 101), but RecurringTransaction.tags does not. This inconsistency may cause issues when accessing rt.tags in code that expects a list (e.g., in to_dict() at lines 248, the if self.tags check and iteration, or in process_recurring_transactions at line 568 where tags are copied). While the properties handle None gracefully, adding the default ensures consistent behavior.

🔧 Suggested fix
 tags: Mapped[Optional[List['Tag']]] = relationship(
     secondary=recurring_transaction_tags,
     lazy='select',
+    default_factory=list,
     init=False
 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/models/__init__.py` around lines 197 - 201,
RecurringTransaction.tags relationship lacks default_factory=list causing
inconsistent defaults versus Transaction.tags; update the
RecurringTransaction.tags relationship (the Mapped[Optional[List['Tag']]]
declaration named tags) to include default_factory=list so rt.tags is
initialized to an empty list by default, ensuring code paths like to_dict()
(which checks/iterates self.tags) and process_recurring_transactions (where tags
are copied) see a consistent list value.
JustAnotherExpenseManager/utils/services.py (1)

551-591: ⚡ Quick win

Consider adding error handling to prevent batch rollback on single-item failure.

If processing one recurring transaction fails (e.g., due to a database constraint violation), the entire batch rolls back and no transactions are spawned. Consider committing after each recurring item or using savepoints to isolate failures.

🛡️ Suggested approach
 for rt in active_recurring:
+    try:
         while rt.next_date <= now:
             ...
+        session.commit()
+    except Exception as e:
+        session.rollback()
+        # Log the error for this specific recurring transaction
+        import logging
+        logging.error(f"Failed to process recurring transaction {rt.id}: {e}")
+        continue
     
-session.commit()

Alternatively, use nested transactions with savepoints for finer-grained control.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/utils/services.py` around lines 551 - 591, The loop
that spawns transactions for each rt in active_recurring can cause the entire
batch to roll back on a single failure; wrap the per-recurring-item work (the
body that creates new_tx, copies tags, updates rt fields) in its own isolated
transaction/savepoint so one failure doesn't abort others: either open a nested
transaction/savepoint via session.begin_nested() around the code that adds
new_tx and updates rt and commit/rollback that nested block on error, or commit
after each successful rt iteration and catch exceptions to rollback only that
iteration (logging the error) while continuing the loop; key symbols to change
are the for rt in active_recurring loop, the Transaction creation, tag copying,
rt.last_processed_date/rt.next_date updates, and the final session.commit().
JustAnotherExpenseManager/templates/transactions_list.html (1)

36-40: 💤 Low value

Consider moving inline styles to CSS class.

The recurring badge uses an inline style. For consistency with the existing type-badge classes and better maintainability, consider defining a .type-recurring class in the stylesheet.

♻️ Optional refactor

In your CSS file:

.type-badge.type-recurring {
    background: `#0984e3`;
    color: white;
}

In the template:

                     {% if trans.recurring_id %}
-                    <span class="type-badge type-recurring" style="background: `#0984e3`; color: white;">
+                    <span class="type-badge type-recurring">
                         Recurring
                     </span>
                     {% endif %}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/templates/transactions_list.html` around lines 36 -
40, The recurring badge currently uses an inline style in the
transactions_list.html span; extract those declarations into the stylesheet by
adding a .type-recurring rule (targeting .type-badge.type-recurring) with
background: `#0984e3` and color: white, then remove the style attribute from the
<span class="type-badge type-recurring"> so the template uses the CSS class
instead of inline styles.
JustAnotherExpenseManager/templates/recurring.html (1)

55-55: ⚡ Quick win

Prefer addEventListener over inline event handlers.

Inline onsubmit attributes reduce Content Security Policy effectiveness and make the code harder to maintain. Consider moving this to the JavaScript module.

♻️ Proposed refactor

In the template:

-    <form id="recurring-form" onsubmit="window.submitRecurring(event)">
+    <form id="recurring-form">

In recurring.ts, add to the DOMContentLoaded handler:

const form = document.getElementById('recurring-form') as HTMLFormElement;
if (form) {
    form.addEventListener('submit', submitRecurring);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/templates/recurring.html` at line 55, Remove the
inline onsubmit attribute from the form with id "recurring-form" in
recurring.html and instead register the handler in the recurring.ts
DOMContentLoaded initialization: find the element by id "recurring-form" and
call addEventListener('submit', submitRecurring) (ensuring submitRecurring is
imported/visible in that module); keep submitRecurring's signature and
preventDefault logic intact so behavior doesn't change.
tests/pages/RecurringPage.ts (1)

67-67: ⚡ Quick win

Use page.once() instead of page.on() for the dialog handler.

The current dialog handler persists across the entire test and could interfere with subsequent dialog interactions. Use once() to ensure it fires only for this deletion:

-    this.page.on('dialog', dialog => dialog.accept());
+    this.page.once('dialog', dialog => dialog.accept());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/pages/RecurringPage.ts` at line 67, The dialog handler currently uses
this.page.on('dialog', dialog => dialog.accept()) which persists across tests;
change it to this.page.once('dialog', dialog => dialog.accept()) so the handler
fires only for the immediate deletion action; locate the dialog registration in
RecurringPage (the this.page.on(...) call) and replace .on with .once, keeping
the same handler logic.
tests/test_recurring.py (1)

104-104: 💤 Low value

Simplify the assertion.

The or condition is redundant. If tx.next_date > today (datetime comparison) is true, the second date-only comparison is unnecessary. Simplify to:

-            assert tx.next_date > today or tx.next_date.date() > today.date()
+            assert tx.next_date > today
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_recurring.py` at line 104, The assertion uses a redundant or
check; replace the compound check with a single datetime comparison by changing
the assertion to assert tx.next_date > today (remove the "or tx.next_date.date()
> today.date()") so it relies on the datetime comparison between tx.next_date
and today in the tests/test_recurring.py assertion.
JustAnotherExpenseManager/routes/recurring.py (3)

6-6: 💤 Low value

Remove unused import.

The Tag class is imported but never directly referenced. The _get_or_create_tag() method returns a Tag, but Python doesn't require the type to be imported for that usage.

🧹 Cleanup
-from JustAnotherExpenseManager.models import RecurringTransaction, Tag
+from JustAnotherExpenseManager.models import RecurringTransaction
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/routes/recurring.py` at line 6, Remove the unused
Tag import from the top-level import line; update the import in the module so
only RecurringTransaction is imported (e.g., change "from
JustAnotherExpenseManager.models import RecurringTransaction, Tag" to import
just RecurringTransaction), since Tag is never directly referenced (the helper
_get_or_create_tag returns a Tag but does not require the type to be imported).

16-19: 💤 Low value

Consider using SQLAlchemy 2.x select() API for consistency.

The delete handler (line 59) uses db.session.get(), while this route uses the legacy db.session.query() API. For consistency with SQLAlchemy 2.x patterns, consider refactoring to:

from sqlalchemy import select

`@recurring_bp.route`('/api', methods=['GET'])
def list_recurring():
    txs = db.session.scalars(select(RecurringTransaction)).all()
    return jsonify([tx.to_dict() for tx in txs])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/routes/recurring.py` around lines 16 - 19, Replace
the legacy query call in list_recurring: instead of using
db.session.query(RecurringTransaction).all(), import sqlalchemy.select and use
db.session.scalars(select(RecurringTransaction)).all() so the route handler
list_recurring and the model RecurringTransaction follow SQLAlchemy 2.x select()
API; adjust imports accordingly.

37-46: 💤 Low value

Optimize service instantiation.

TransactionService is instantiated separately on lines 38 and 43. When both category and tags are present, this creates two instances unnecessarily.

♻️ Refactor to single service instance
+        svc = None
         if 'category' in data and data['category']:
-            svc = TransactionService(db.session)
+            if not svc:
+                svc = TransactionService(db.session)
             tag = svc._get_or_create_tag(f"category:{data['category']}")
             tx.tags.append(tag)
             
         if 'tags' in data and data['tags']:
-            svc = TransactionService(db.session)
+            if not svc:
+                svc = TransactionService(db.session)
             for t in data['tags']:

Or more simply:

+        svc = TransactionService(db.session)
+        
         if 'category' in data and data['category']:
-            svc = TransactionService(db.session)
             tag = svc._get_or_create_tag(f"category:{data['category']}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/routes/recurring.py` around lines 37 - 46,
Instantiate TransactionService once and reuse it instead of creating two
instances: move the creation of svc = TransactionService(db.session) out of the
two conditional blocks and use the same svc when calling svc._get_or_create_tag
for both the 'category' branch (for f"category:{data['category']}") and the
'tags' loop, ensuring tx.tags.append calls remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@JustAnotherExpenseManager/routes/recurring.py`:
- Line 27: Replace the fragile float-to-int conversion for amount_cents
(currently int(data['amount_dollars'] * 100)) with a Decimal-based conversion to
avoid floating-point truncation: import Decimal, construct Decimal from the
original numeric string (Decimal(str(data['amount_dollars']))) or from the raw
JSON string, multiply by Decimal('100'), then convert to an integer via
to_integral_value() or int(...) so cents are correctly rounded (e.g.,
amount_cents = int((Decimal(str(data['amount_dollars'])) *
Decimal('100')).to_integral_value()) ); update the assignment to use this new
expression wherever amount_cents is computed.
- Around line 34-35: When parsing data['end_date'] into tx.end_date (using
datetime.strptime), validate it is not earlier than the transaction's start date
(tx.start_date or data['start_date'] parsed similarly); if end_date <
start_date, reject the request by raising/returning a clear validation error
(e.g., BadRequest/400) and include a descriptive message. Ensure you handle the
case where start_date may not be present or not yet parsed by
parsing/normalizing start_date before comparing, and perform the comparison
immediately after setting tx.end_date in the recurring transaction
creation/update logic.
- Around line 26-32: The code constructs a recurring record using direct
data[...] access (e.g., amount_cents=int(data['amount_dollars'] * 100],
TransactionType(data['type']), RecurringFrequency(data['frequency']),
start_date=datetime.strptime(data['start_date'], '%Y-%m-%d') ) which will raise
KeyError for missing fields; add explicit validation at the start of the route
handler that reads request JSON into data: define a required_fields list
(description, amount_dollars, type, frequency, start_date), collect any missing
= [f for f in required_fields if f not in data or data[f] in (None, '')], and if
missing raise/return a 400 Bad Request with a clear message naming the missing
fields instead of letting the generic exception handler catch it; keep the
existing conversions (amount -> amount_cents, TransactionType,
RecurringFrequency, datetime.strptime) after validation so the unique symbols
data, amount_cents=int(...), TransactionType(...), RecurringFrequency(...), and
datetime.strptime(...) remain unchanged.
- Around line 54-55: The current broad except block catches all exceptions and
returns HTTP 400; change it to distinguish client vs server errors and ensure DB
rollback: replace the single "except Exception as e:" with specific handlers
such as "except (ValueError, BadRequest) as e:" to return jsonify error with
400, "except SQLAlchemyError as e:" to call db.session.rollback() and return a
500 error, and a final "except Exception as e:" that also calls
db.session.rollback() and returns 500; reference the route's use of jsonify and
db.session.rollback()/db.session to locate where to apply these changes.
- Line 23: The assignment data = request.json can yield None and later cause
AttributeError; update the route handler (the code that reads request.json into
data) to validate that request.json (or request.get_json()) is not None before
accessing it and return a 400/Bad Request with a clear message if the body or
Content-Type is invalid; specifically check the data variable after reading
request.json, and handle the None case by returning an error response instead of
proceeding to dictionary access.
- Around line 51-53: In create_recurring, the current except ValidationError
block is unreachable because parsing/enum construction raises
ValueError/KeyError; update exception handling to catch ValueError and KeyError
(log the error and return jsonify({"error": str(e)}), 400) for client input
errors, and change the generic except Exception to call db.session.rollback(),
log the exception, and return a 500 response (e.g., jsonify({"error": "Internal
Server Error"}), 500) to match other routes; reference the create_recurring
function, the db.session.rollback() call, and the current_app.logger.error
invocations when making these changes.

In `@JustAnotherExpenseManager/templates/recurring.html`:
- Line 6: The CDN stylesheet link for Tagify in recurring.html is missing
Subresource Integrity; update the <link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css"> tag to
include a valid integrity="sha384-..." attribute and crossorigin="anonymous" (or
appropriate) to enable SRI checks; generate the base64 SHA-384 hash for the
exact file version (using https://www.srihash.org/ or openssl dgst -sha384
-binary tagify.js | openssl base64 -A) and paste it into the integrity attribute
so the browser rejects tampered resources.
- Around line 82-84: The tags input isn't reliably announced by assistive tech
after Tagify transforms it: add an explicit label id and reference it from the
input so screen readers keep the association — give the existing label element a
unique id (e.g., id="tags-label") and add aria-labelledby="tags-label" to the
input with id="tags" (and also add aria-placeholder or aria-describedby if you
have a helper text). Ensure you keep label for="tags" and the input id="tags"
intact so both native and ARIA associations are present.

In `@JustAnotherExpenseManager/utils/scheduler.py`:
- Around line 6-14: init_scheduler currently always initializes and starts the
APScheduler and registers the cron job; change it to skip initialization and
starting when the Flask app is in testing mode by checking
app.config.get('TESTING') (or a dedicated flag like
app.config.get('SCHEDULER_ENABLED') defaulting to True). Concretely, in
init_scheduler(app) return early (do not call scheduler.init_app,
scheduler.start, or register the `@scheduler.task`) when app.config['TESTING'] is
True; keep the existing scheduler, run_process_recurring_transactions, and
process_recurring_transactions names so the change is minimal and localized.

In `@JustAnotherExpenseManager/utils/services.py`:
- Around line 546-549: Replace the non-idiomatic equality check in the query
building for active_recurring: instead of comparing
RecurringTransaction.is_active == True, use the boolean attribute directly
(e.g., RecurringTransaction.is_active) inside the filter passed to
session.query(...) so the filter becomes RecurringTransaction.is_active and
RecurringTransaction.next_date <= now; update the code around the
active_recurring assignment to remove the "== True" comparison.
- Around line 584-589: The YEARLY branch should compute the target year and
clamp the day to the last day of that month rather than unconditionally falling
back to day=28; capture the original day (orig_day = rt.next_date.day), set
target_year = rt.next_date.year + 1, get last_day =
calendar.monthrange(target_year, rt.next_date.month)[1], then set rt.next_date =
rt.next_date.replace(year=target_year, day=min(orig_day, last_day)); update the
code around RecurringFrequency.YEARLY and rt.next_date to import/use
calendar.monthrange so Feb 29 advances correctly to Feb 28 on non-leap years
while preserving a proper day cap.
- Around line 580-583: The MONTHLY branch currently does rt.next_date =
rt.next_date + timedelta(days=days_in_month), which miscalculates month
advances; replace that logic in the elif rt.frequency ==
RecurringFrequency.MONTHLY block so you advance by one calendar month instead of
adding days — either import and use dateutil.relativedelta and do rt.next_date =
rt.next_date + relativedelta(months=1) (add python-dateutil to deps), or
implement a small helper that increments rt.next_date by one month while
clamping the day to the end of the target month (handle year rollovers and
months with fewer days) and assign the result back to rt.next_date.

In `@static_src/js/recurring.ts`:
- Around line 62-77: The rendering in loadRecurring builds raw HTML with
listDiv.innerHTML and interpolates user-controlled fields like tx.description,
causing an XSS risk; replace the string-concatenation/template-literal approach
with DOM APIs: create table/thead/tbody/tr/td elements via
document.createElement, set user data using textContent (for tx.description,
tx.frequency, tx.next_date, amount text, and tx.type) to ensure escaping, and
attach the delete action using deleteBtn.addEventListener('click', () =>
deleteRecurring(tx.id)) instead of inline onclick; update loadRecurring to clear
listDiv (e.g., listDiv.textContent = '') and append the constructed DOM tree.
- Line 107: The code currently assigns amount_dollars using
parseFloat((form.querySelector('`#amount`') as HTMLInputElement).value) without
validating the result; if parseFloat returns NaN the backend will receive an
invalid value. Fix by grabbing the amount input element (use the same
form.querySelector('`#amount`') cast), check its validity via HTML5 validity
(amountInput.validity.valid) or validate with isFinite/Number.isNaN on the
parsed value, show a user-facing error and return early if invalid, and only set
amount_dollars when the parsed number is a valid finite number.

In `@static_src/js/types.ts`:
- Around line 7-18: The RecurringTransaction interface currently declares
category as required; change it to optional to match the backend contract by
updating the RecurringTransaction definition so category?: string instead of
category: string; ensure any code referencing RecurringTransaction (e.g., form
submission logic that constructs RecurringTransaction objects) still handles
undefined or empty-string category values safely.

---

Nitpick comments:
In `@JustAnotherExpenseManager/models/__init__.py`:
- Around line 197-201: RecurringTransaction.tags relationship lacks
default_factory=list causing inconsistent defaults versus Transaction.tags;
update the RecurringTransaction.tags relationship (the
Mapped[Optional[List['Tag']]] declaration named tags) to include
default_factory=list so rt.tags is initialized to an empty list by default,
ensuring code paths like to_dict() (which checks/iterates self.tags) and
process_recurring_transactions (where tags are copied) see a consistent list
value.

In `@JustAnotherExpenseManager/routes/recurring.py`:
- Line 6: Remove the unused Tag import from the top-level import line; update
the import in the module so only RecurringTransaction is imported (e.g., change
"from JustAnotherExpenseManager.models import RecurringTransaction, Tag" to
import just RecurringTransaction), since Tag is never directly referenced (the
helper _get_or_create_tag returns a Tag but does not require the type to be
imported).
- Around line 16-19: Replace the legacy query call in list_recurring: instead of
using db.session.query(RecurringTransaction).all(), import sqlalchemy.select and
use db.session.scalars(select(RecurringTransaction)).all() so the route handler
list_recurring and the model RecurringTransaction follow SQLAlchemy 2.x select()
API; adjust imports accordingly.
- Around line 37-46: Instantiate TransactionService once and reuse it instead of
creating two instances: move the creation of svc =
TransactionService(db.session) out of the two conditional blocks and use the
same svc when calling svc._get_or_create_tag for both the 'category' branch (for
f"category:{data['category']}") and the 'tags' loop, ensuring tx.tags.append
calls remain unchanged.

In `@JustAnotherExpenseManager/templates/recurring.html`:
- Line 55: Remove the inline onsubmit attribute from the form with id
"recurring-form" in recurring.html and instead register the handler in the
recurring.ts DOMContentLoaded initialization: find the element by id
"recurring-form" and call addEventListener('submit', submitRecurring) (ensuring
submitRecurring is imported/visible in that module); keep submitRecurring's
signature and preventDefault logic intact so behavior doesn't change.

In `@JustAnotherExpenseManager/templates/transactions_list.html`:
- Around line 36-40: The recurring badge currently uses an inline style in the
transactions_list.html span; extract those declarations into the stylesheet by
adding a .type-recurring rule (targeting .type-badge.type-recurring) with
background: `#0984e3` and color: white, then remove the style attribute from the
<span class="type-badge type-recurring"> so the template uses the CSS class
instead of inline styles.

In `@JustAnotherExpenseManager/utils/services.py`:
- Around line 551-591: The loop that spawns transactions for each rt in
active_recurring can cause the entire batch to roll back on a single failure;
wrap the per-recurring-item work (the body that creates new_tx, copies tags,
updates rt fields) in its own isolated transaction/savepoint so one failure
doesn't abort others: either open a nested transaction/savepoint via
session.begin_nested() around the code that adds new_tx and updates rt and
commit/rollback that nested block on error, or commit after each successful rt
iteration and catch exceptions to rollback only that iteration (logging the
error) while continuing the loop; key symbols to change are the for rt in
active_recurring loop, the Transaction creation, tag copying,
rt.last_processed_date/rt.next_date updates, and the final session.commit().

In `@tests/pages/RecurringPage.ts`:
- Line 67: The dialog handler currently uses this.page.on('dialog', dialog =>
dialog.accept()) which persists across tests; change it to
this.page.once('dialog', dialog => dialog.accept()) so the handler fires only
for the immediate deletion action; locate the dialog registration in
RecurringPage (the this.page.on(...) call) and replace .on with .once, keeping
the same handler logic.

In `@tests/test_recurring.py`:
- Line 104: The assertion uses a redundant or check; replace the compound check
with a single datetime comparison by changing the assertion to assert
tx.next_date > today (remove the "or tx.next_date.date() > today.date()") so it
relies on the datetime comparison between tx.next_date and today in the
tests/test_recurring.py assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 77e8b5fe-bb24-48a3-8d5f-5f43a2ddfecc

📥 Commits

Reviewing files that changed from the base of the PR and between 02d08ff and 1bd750c.

📒 Files selected for processing (19)
  • JustAnotherExpenseManager/__init__.py
  • JustAnotherExpenseManager/models/__init__.py
  • JustAnotherExpenseManager/models/dtos.py
  • JustAnotherExpenseManager/routes/recurring.py
  • JustAnotherExpenseManager/static/js/recurring.js
  • JustAnotherExpenseManager/templates/base.html
  • JustAnotherExpenseManager/templates/recurring.html
  • JustAnotherExpenseManager/templates/transactions_list.html
  • JustAnotherExpenseManager/utils/scheduler.py
  • JustAnotherExpenseManager/utils/services.py
  • pyproject.toml
  • static_src/js/recurring.ts
  • static_src/js/types.ts
  • tests/12-recurring-transactions.spec.ts
  • tests/conftest.py
  • tests/fixtures.ts
  • tests/pages/RecurringPage.ts
  • tests/test_recurring.py
  • vite.config.ts

Comment thread JustAnotherExpenseManager/routes/recurring.py
Comment thread JustAnotherExpenseManager/routes/recurring.py
Comment thread JustAnotherExpenseManager/routes/recurring.py Outdated
Comment thread JustAnotherExpenseManager/routes/recurring.py
Comment thread JustAnotherExpenseManager/routes/recurring.py Outdated
Comment thread JustAnotherExpenseManager/utils/services.py Outdated
Comment thread JustAnotherExpenseManager/utils/services.py Outdated
Comment thread static_src/js/recurring.ts Outdated
Comment thread static_src/js/recurring.ts Outdated
Comment thread static_src/js/types.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
JustAnotherExpenseManager/routes/recurring.py (1)

24-25: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix set literal bug in error response.

Line 25 uses a set literal {'error', 'Request body must be valid JSON'} instead of a dictionary. This will cause jsonify() to fail or return incorrect JSON.

🐛 Proposed fix
     if not data:
-        return jsonify({'error', 'Request body must be valid JSON'}), 400
+        return jsonify({'error': 'Request body must be valid JSON'}), 400
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/routes/recurring.py` around lines 24 - 25, The
error response is currently built as a set literal causing jsonify to fail;
update the return to use a dictionary mapping the "error" key to the message
(i.e., replace the set {'error', 'Request body must be valid JSON'} with
{'error': 'Request body must be valid JSON'}) where the route checks "if not
data" and returns via jsonify so the response is valid JSON.
🧹 Nitpick comments (3)
static_src/js/settings.ts (2)

530-532: 💤 Low value

Remove duplicate window export.

window.exportTransactions is assigned twice (lines 520 and 531). The duplicate on line 531 can be removed.

♻️ Proposed cleanup
 window.populateTestData = populateTestData;
-window.exportTransactions = exportTransactions;
 window.runRecurringTransactions = runRecurringTransactions;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static_src/js/settings.ts` around lines 530 - 532, There is a duplicate
export assignment for window.exportTransactions; remove the redundant statement
so each global is only set once—leave window.populateTestData and
window.runRecurringTransactions as-is and delete the second
window.exportTransactions assignment to avoid the duplicate global binding.

443-468: ⚖️ Poor tradeoff

Consider using textContent for error messages (defense in depth).

Lines 452, 459, and 464 interpolate API response messages into innerHTML. While the backend currently returns static strings, using textContent would provide defense-in-depth protection against potential XSS if backend responses change in the future.

However, this pattern is consistent with other error handling in the file (e.g., populateTestData, exportTransactions), so this is an optional improvement for the entire module rather than a specific issue with this function.

🛡️ Optional defensive improvement
if (result.success) {
  if (resultDiv) {
    const p = document.createElement('p');
    p.style.color = '`#00b894`';
    p.style.fontWeight = '600';
    p.textContent = `✓ ${result.message}`;
    resultDiv.innerHTML = '';
    resultDiv.appendChild(p);
    setTimeout(() => { resultDiv.innerHTML = ''; }, 3000);
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static_src/js/settings.ts` around lines 443 - 468, In
runRecurringTransactions replace uses of resultDiv.innerHTML that interpolate
API strings with DOM-safe construction: create a <p> element, set its style
properties (color, fontWeight), set p.textContent to the success message (`✓
${result.message}`) or error text (`❌ ${(result as ApiError).error}` / `❌ Error:
${(error as Error).message}`), clear resultDiv (e.g., resultDiv.textContent =
'') and append the p element, and keep the existing timeout logic; this prevents
injecting untrusted HTML while preserving the current behavior and references to
resultDiv, ApiResult and ApiError.
tests/test_recurring.py (1)

181-206: Verify monthly end-of-month expectation: test aligns with current relativedelta(months=1) logic
process_recurring_transactions() advances RecurringFrequency.MONTHLY by updating rt.next_date = rt.next_date + relativedelta(months=1), so an end-of-month date is clamped once and that clamped day carries forward (e.g., 2023-01-31 → 2023-02-28 → 2023-03-28), matching the test expectation on line 205. If the intended UX is “last day of each month” (expecting 2023-03-31), adjust the monthly advancement to preserve the original target day/month-end each step and update the test accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_recurring.py` around lines 181 - 206, The test assumes "clamped
carry-forward" behavior but your code currently uses relativedelta(months=1) in
process_recurring_transactions to advance rt.next_date; decide which behavior
you want and fix accordingly: either update the test to expect the clamped
sequence (2023-01-31 → 2023-02-28 → 2023-03-28) or change
process_recurring_transactions to implement “last day of each month” semantics
by computing an original_target_day = rt.start_date.day (or store it on
RecurringTransaction), then when advancing use next_month = rt.next_date +
relativedelta(months=1) and set rt.next_date = date(next_month.year,
next_month.month, min(original_target_day, last_day_of_month(next_month))) (use
calendar.monthrange to get last_day_of_month); update references to
rt.next_date, process_recurring_transactions, and
RecurringTransaction.start_date accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@JustAnotherExpenseManager/routes/settings.py`:
- Around line 108-118: The /api/run-recurring endpoint (function run_recurring)
currently calls process_recurring_transactions() with no authorization; restrict
it by checking the existing _test_routes_enabled() guard or an
authentication/admin check before invoking process_recurring_transactions(),
returning a 403 if not allowed; also consider adding rate limiting/CSRF
protection hooks where similar endpoints do (see patterns used in
/api/populate-test-data and /api/transactions/clear-all) so only authorized or
test-mode requests can trigger DB updates.

In `@JustAnotherExpenseManager/static/js/settings.js`:
- Line 2227: Remove the redundant global binding by deleting the duplicate
statement that assigns window.exportTransactions = exportTransactions; and keep
only the intended new function assignment that defines/export the updated
exportTransactions implementation; locate both occurrences by searching for the
symbol exportTransactions and the duplicate window.exportTransactions assignment
and remove the earlier/duplicated one so the new function binding is the sole
export.

---

Duplicate comments:
In `@JustAnotherExpenseManager/routes/recurring.py`:
- Around line 24-25: The error response is currently built as a set literal
causing jsonify to fail; update the return to use a dictionary mapping the
"error" key to the message (i.e., replace the set {'error', 'Request body must
be valid JSON'} with {'error': 'Request body must be valid JSON'}) where the
route checks "if not data" and returns via jsonify so the response is valid
JSON.

---

Nitpick comments:
In `@static_src/js/settings.ts`:
- Around line 530-532: There is a duplicate export assignment for
window.exportTransactions; remove the redundant statement so each global is only
set once—leave window.populateTestData and window.runRecurringTransactions as-is
and delete the second window.exportTransactions assignment to avoid the
duplicate global binding.
- Around line 443-468: In runRecurringTransactions replace uses of
resultDiv.innerHTML that interpolate API strings with DOM-safe construction:
create a <p> element, set its style properties (color, fontWeight), set
p.textContent to the success message (`✓ ${result.message}`) or error text (`❌
${(result as ApiError).error}` / `❌ Error: ${(error as Error).message}`), clear
resultDiv (e.g., resultDiv.textContent = '') and append the p element, and keep
the existing timeout logic; this prevents injecting untrusted HTML while
preserving the current behavior and references to resultDiv, ApiResult and
ApiError.

In `@tests/test_recurring.py`:
- Around line 181-206: The test assumes "clamped carry-forward" behavior but
your code currently uses relativedelta(months=1) in
process_recurring_transactions to advance rt.next_date; decide which behavior
you want and fix accordingly: either update the test to expect the clamped
sequence (2023-01-31 → 2023-02-28 → 2023-03-28) or change
process_recurring_transactions to implement “last day of each month” semantics
by computing an original_target_day = rt.start_date.day (or store it on
RecurringTransaction), then when advancing use next_month = rt.next_date +
relativedelta(months=1) and set rt.next_date = date(next_month.year,
next_month.month, min(original_target_day, last_day_of_month(next_month))) (use
calendar.monthrange to get last_day_of_month); update references to
rt.next_date, process_recurring_transactions, and
RecurringTransaction.start_date accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: edc340d5-1b4d-42b0-9ec8-1b73564d2530

📥 Commits

Reviewing files that changed from the base of the PR and between 1bd750c and 841be01.

📒 Files selected for processing (12)
  • JustAnotherExpenseManager/routes/recurring.py
  • JustAnotherExpenseManager/routes/settings.py
  • JustAnotherExpenseManager/static/js/recurring.js
  • JustAnotherExpenseManager/static/js/settings.js
  • JustAnotherExpenseManager/templates/recurring.html
  • JustAnotherExpenseManager/templates/settings.html
  • JustAnotherExpenseManager/utils/scheduler.py
  • JustAnotherExpenseManager/utils/services.py
  • static_src/js/recurring.ts
  • static_src/js/settings.ts
  • static_src/js/types.ts
  • tests/test_recurring.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • static_src/js/types.ts
  • JustAnotherExpenseManager/utils/scheduler.py
  • JustAnotherExpenseManager/templates/recurring.html
  • JustAnotherExpenseManager/utils/services.py
  • JustAnotherExpenseManager/static/js/recurring.js

Comment on lines +108 to +118
@settings_bp.route('/api/run-recurring', methods=['POST'])
def run_recurring() -> Union[Response, Tuple[Response, int]]:
"""
Manually trigger processing of recurring transactions.
"""
try:
from JustAnotherExpenseManager.utils.services import process_recurring_transactions
process_recurring_transactions()
return jsonify({'success': True, 'message': 'Successfully processed recurring transactions'})
except Exception as exc: # pylint: disable=broad-except
return jsonify({'error': str(exc)}), 500

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's authentication middleware or decorators used elsewhere
rg -n "`@login_required`|`@auth_required`|`@require_auth`|check_auth|verify_auth" JustAnotherExpenseManager/routes/

Repository: paperplate/JustAnotherExpenseManager

Length of output: 62


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== settings.py around run_recurring =="
sed -n '1,220p' JustAnotherExpenseManager/routes/settings.py | nl -ba | sed -n '70,160p'

echo
echo "== Find _test_routes_enabled usage in settings.py =="
rg -n "_test_routes_enabled" JustAnotherExpenseManager/routes/settings.py

echo
echo "== Locate /api/run-recurring references =="
rg -n "/api/run-recurring" -S JustAnotherExpenseManager

echo
echo "== Locate process_recurring_transactions definition =="
rg -n "def process_recurring_transactions" -S JustAnotherExpenseManager

echo
echo "== Show process_recurring_transactions implementation =="
python3 - <<'PY'
import os, re, subprocess, textwrap, json, sys
root="JustAnotherExpenseManager"
# Use rg to get file path and line for the def, then print a small window using sed
import subprocess
res=subprocess.check_output(["bash","-lc",f"rg -n \"def process_recurring_transactions\" -S {root}"],stderr=subprocess.STDOUT,text=True)
print(res.strip().splitlines()[0] if res.strip() else "No match")
PY

Repository: paperplate/JustAnotherExpenseManager

Length of output: 180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== settings.py (relevant region) =="
# Print a focused window around the provided line range
sed -n '1,220p' JustAnotherExpenseManager/routes/settings.py | cat -n | sed -n '70,160p'

echo
echo "== _test_routes_enabled usage in settings.py =="
rg -n "_test_routes_enabled" JustAnotherExpenseManager/routes/settings.py

echo
echo "== Find /api/run-recurring =="
rg -n "/api/run-recurring" -S JustAnotherExpenseManager

echo
echo "== Find process_recurring_transactions definition =="
rg -n "process_recurring_transactions" -S JustAnotherExpenseManager/utils JustAnotherExpenseManager

echo
echo "== Show process_recurring_transactions implementation (small window) =="
# Get the first matching file+line for the function definition and print nearby lines
python3 - <<'PY'
import subprocess, re
root="JustAnotherExpenseManager"
res=subprocess.check_output(["bash","-lc",f"rg -n \"def process_recurring_transactions\" -S {root}"],text=True)
first=res.strip().splitlines()[0]
m=re.match(r"^(.*?):(\\d+):", first)
if not m:
    print(first); raise SystemExit
path=m.group(1); line=int(m.group(2))
start=max(1,line-40); end=line+120
print(f"Matched: {path}:{line}")
print("---- context ----")
subprocess.run(["bash","-lc",f"sed -n '{start},{end}p' {path} | cat -n | sed -n '1,180p'"],check=False)
PY

echo
echo "== Search for settings_bp registration / protection =="
rg -n "settings_bp" JustAnotherExpenseManager | head -n 50

echo
echo "== Search for auth/permission patterns outside routes =="
rg -n "before_request|current_user|g\\.|login_required|require_auth|permission|is_admin|admin" JustAnotherExpenseManager | head -n 80

Repository: paperplate/JustAnotherExpenseManager

Length of output: 13976


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== settings.py top (including _test_routes_enabled) =="
sed -n '1,120p' JustAnotherExpenseManager/routes/settings.py | cat -n

echo
echo "== settings.py around run-recurring =="
sed -n '100,140p' JustAnotherExpenseManager/routes/settings.py | cat -n

echo
echo "== services.py: process_recurring_transactions implementation (around match line) =="
# Print a window around the function definition line using sed, based on rg line number.
line=$(rg -n "def process_recurring_transactions\(" JustAnotherExpenseManager/utils/services.py | head -n 1 | sed -E 's/^[^:]+:([0-9]+):.*/\1/')
start=$((line-40)); end=$((line+220))
sed -n "${start},${end}p" JustAnotherExpenseManager/utils/services.py | cat -n

echo
echo "== scheduler.py (recurring job gating) =="
sed -n '1,120p' JustAnotherExpenseManager/utils/scheduler.py | cat -n

echo
echo "== __init__.py (app factory / before_request hooks) =="
sed -n '1,220p' JustAnotherExpenseManager/__init__.py | cat -n

echo
echo "== Search repo for auth/permission hooks outside routes =="
rg -n "before_request|current_user|g\.user|session\[|login_required|require_auth|is_admin|admin" JustAnotherExpenseManager | head -n 120

Repository: paperplate/JustAnotherExpenseManager

Length of output: 6198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== process_recurring_transactions context =="
python3 - <<'PY'
import subprocess, re
root="JustAnotherExpenseManager/utils/services.py"
# find the first line number of def process_recurring_transactions(
res=subprocess.check_output(["bash","-lc",f"rg -n \"def process_recurring_transactions\\(\" {root}"],text=True).strip().splitlines()
if not res:
    raise SystemExit("No match for process_recurring_transactions in services.py")
m=re.match(r"^(.*?):(\d+):", res[0])
path, line = m.group(1), int(m.group(2))
start=max(1, line-40); end=line+220
print(f"Matched: {path}:{line}")
print("---- context ----")
subprocess.run(["bash","-lc",f"sed -n '{start},{end}p' {path} | cat -n"],check=False)
PY

echo
echo "== scheduler.py =="
sed -n '1,120p' JustAnotherExpenseManager/utils/scheduler.py | cat -n

echo
echo "== __init__.py =="
sed -n '1,240p' JustAnotherExpenseManager/__init__.py | cat -n

echo
echo "== repo-wide search for before_request/auth/permission patterns =="
rg -n "before_request|`@before_request`|login_required|auth_required|require_auth|current_user|g\\.user|session\\[|is_admin|admin_required|permission|role" JustAnotherExpenseManager | head -n 200

Repository: paperplate/JustAnotherExpenseManager

Length of output: 374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Exact rg match for process_recurring_transactions =="
rg -n "def[[:space:]]+process_recurring_transactions[[:space:]]*\\(" JustAnotherExpenseManager/utils/services.py

echo
echo "== services.py context around that line (first match) =="
line=$(rg -n "def[[:space:]]+process_recurring_transactions[[:space:]]*\\(" JustAnotherExpenseManager/utils/services.py | head -n 1 | sed -E 's/^[^:]+:([0-9]+):.*/\1/')
start=$((line-60))
end=$((line+260))
sed -n "${start},${end}p" JustAnotherExpenseManager/utils/services.py | cat -n

echo
echo "== scheduler.py =="
sed -n '1,120p' JustAnotherExpenseManager/utils/scheduler.py | cat -n

echo
echo "== __init__.py app factory =="
sed -n '1,240p' JustAnotherExpenseManager/__init__.py | cat -n

echo
echo "== repo-wide search for request auth gating =="
rg -n "before_request|`@before_request`|login_required|require_auth|auth_required|current_user|session\\[|is_admin|admin_required|permission|role" JustAnotherExpenseManager | head -n 200

Repository: paperplate/JustAnotherExpenseManager

Length of output: 594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== services.py context around process_recurring_transactions (line 536) =="
sed -n '476,650p' JustAnotherExpenseManager/utils/services.py | cat -n

echo
echo "== scheduler.py =="
sed -n '1,160p' JustAnotherExpenseManager/utils/scheduler.py | cat -n

echo
echo "== __init__.py app factory =="
sed -n '1,260p' JustAnotherExpenseManager/__init__.py | cat -n

echo
echo "== settings_bp protection / request hooks search =="
rg -n "before_request|teardown_request|`@settings_bp`|register_blueprint\\(settings_bp\\)|login_required|require_auth|auth_required|current_user|session\\[|g\\.user|is_admin|admin_required|permission|role" JustAnotherExpenseManager | head -n 200

Repository: paperplate/JustAnotherExpenseManager

Length of output: 16851


Add authorization (or test-mode gating) to /api/run-recurring to prevent unauthenticated DB mutations.

  • JustAnotherExpenseManager/routes/settings.py (POST /api/run-recurring, lines ~108-118) calls process_recurring_transactions() directly and is not guarded by _test_routes_enabled() or any auth check (unlike /api/populate-test-data and /api/transactions/clear-all).
  • JustAnotherExpenseManager/utils/services.py::process_recurring_transactions() creates new Transaction rows for due RecurringTransactions and updates next_date/last_processed_date in the database, so anyone can force recurring transaction processing on demand.

Restrict this endpoint to authenticated/admin users (or gate it to debug/testing), and consider rate limiting (and CSRF protection if it uses cookie-based sessions).

🧰 Tools
🪛 Ruff (0.15.15)

[warning] 117-117: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/routes/settings.py` around lines 108 - 118, The
/api/run-recurring endpoint (function run_recurring) currently calls
process_recurring_transactions() with no authorization; restrict it by checking
the existing _test_routes_enabled() guard or an authentication/admin check
before invoking process_recurring_transactions(), returning a 403 if not
allowed; also consider adding rate limiting/CSRF protection hooks where similar
endpoints do (see patterns used in /api/populate-test-data and
/api/transactions/clear-all) so only authorized or test-mode requests can
trigger DB updates.

window.saveEditTag = saveEditTag;
window.deleteTag = deleteTag;
window.populateTestData = populateTestData;
window.exportTransactions = exportTransactions;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove duplicate window assignment.

Line 2227 duplicates the assignment from line 2217. Only the new function assignment on line 2228 is needed here.

🧹 Proposed fix
-window.exportTransactions = exportTransactions;
 window.runRecurringTransactions = runRecurringTransactions;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JustAnotherExpenseManager/static/js/settings.js` at line 2227, Remove the
redundant global binding by deleting the duplicate statement that assigns
window.exportTransactions = exportTransactions; and keep only the intended new
function assignment that defines/export the updated exportTransactions
implementation; locate both occurrences by searching for the symbol
exportTransactions and the duplicate window.exportTransactions assignment and
remove the earlier/duplicated one so the new function binding is the sole
export.

@paperplate
paperplate merged commit 37263b7 into main Jun 11, 2026
8 checks passed
@paperplate
paperplate deleted the reccur branch June 11, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant