[GH-ISSUE #7258] [Bug]: Daily automatic transactions only occur every 3rd day #52147

Closed
opened 2026-04-30 20:19:55 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @anoff on GitHub (Mar 22, 2026).
Original GitHub issue: https://github.com/actualbudget/actual/issues/7258

What happened?

I set daily, automatically added scheduled transactions to mirror a scheduled transaction on my bank account.
In the schedule view the upcoming days shows correctly each day, also in my accounts overview they are showing up as upcoming on each day.

🐞 However the actual transactions get created only every 3rd (sometimes it's off) day.
Instead of having a transaction on every day I end up with having them on 2026-03-21, 2026-03-17, 2026-03-14 only.

Initially I suspected it only updates when I'm accessing the app but I'm adding transactions every day so must be another reason.

Screenshot of the schedule view showing the correct future schedules, but wrong past schedules:

Image

How can we reproduce the issue?

  1. Go to schedules
  2. Create a schedule set to every day
  3. Activate Automatically add transaction
  4. Wait 3 days, see only 1 transaction being added

Where are you hosting Actual?

Docker

What browsers are you seeing the problem on?

Other, Firefox

Operating System

Mac OSX

Originally created by @anoff on GitHub (Mar 22, 2026). Original GitHub issue: https://github.com/actualbudget/actual/issues/7258 ### What happened? I set daily, automatically added scheduled transactions to mirror a scheduled transaction on my bank account. ✅ In the schedule view the upcoming days shows correctly each day, also in my accounts overview they are showing up as upcoming on each day. 🐞 However the actual transactions get created only every 3rd (sometimes it's off) day. Instead of having a transaction on every day I end up with having them on 2026-03-21, 2026-03-17, 2026-03-14 only. Initially I suspected it only updates when I'm accessing the app but I'm adding transactions every day so must be another reason. Screenshot of the schedule view showing the correct future schedules, but wrong past schedules: <img width="593" height="881" alt="Image" src="https://github.com/user-attachments/assets/691a41f1-6100-40de-8631-e55e7de00f86" /> ### How can we reproduce the issue? 1. Go to schedules 2. Create a schedule set to _every day_ 3. Activate _Automatically add transaction_ 4. Wait 3 days, see only 1 transaction being added ### Where are you hosting Actual? Docker ### What browsers are you seeing the problem on? Other, Firefox ### Operating System Mac OSX
GiteaMirror added the schedulesbug labels 2026-04-30 20:19:56 -05:00
Author
Owner

@anoff commented on GitHub (Mar 22, 2026):

I took a stab at analyzing the potential root cause.
Disclaimer: Initially I asked Claude to point me in the right direction, but then did my own digging through the typescript code. So this is not AI-assisted but not vibed.

Potential root cause

The getHasTransactionsQuery(schedules) allows past transactions to be off by 2 days, probably to accomodate for slight deviations in manual transactions and bank transfers.
However when there are daily transactions that means it always finds a existing transaction within the last 2 days as well.
See next chapter for what I believe is happening overall.

        date: {
          $gte:
            dateCond && dateCond.op === 'is'
              ? schedule.next_date
              : monthUtils.subDays(schedule.next_date, 2),
        },

Assumptions

Based on what I understand from the code, I think this is what's going wrong:

  1. app.ts:advanceSchedulesService() is in charge of checking schedules and proceeding them
  2. the path to adding a new transaction automatically await postTransactionForSchedule({ id: schedule.id **}); can only be reached if the status != 'paid'
  3. in case of recurring payments the dateCond.op != 'is' (only used for schedules with 1 fixed date)
  4. the 2 day lookback described above kicks in and inside advanceSchedulesService() the schedule is considered as 'paid' because there was a transaction yesterday
  5. since it's already paid, no new transaction is created
  6. on day3, the 2 day lookback finds no transaction, hence it creates one
  7. that's how I end up with a transaction every 3 days even though I set it as daily

Potential fix

There are a few ways to fix, this, please advise which you prefer and I'll push a PR :)

A: Add a case for automatic transactions

As automatically created transactions should always be on time, we just disable the 2 day lookback for those.

$gte:
  dateCond && dateCond.op === 'is'
    ? schedule.next_date
    : schedule.posts_transaction
      ? schedule.next_date                          // auto-posted: exact date, no lookback
      : monthUtils.subDays(schedule.next_date, 2),  // manual transaction matching: keep current behavior

B: Add a case for daily transactions

Similar as above but assuming the special scenario is daily schedules.
For any daily schedule - automatically added or not - it does not make sense to check for existing transactions other than on the current day.

$gte:
  dateCond && dateCond.op === 'is'
    ? schedule.next_date
    : dateCond?.value?.frequency === 'daily'
      ? schedule.next_date                          // no lookback for daily
      : monthUtils.subDays(schedule.next_date, 2),  // keep 2-day lookback for weekly+ schedules

C: Combine both conditions

Would specifically target daily + automatically created transaction schedules only.

Personally I would go with A because the 2 day lookback is a feature only relevant for manual (or synced) transactions, not for those created by AB directly.

Test Case

I asked [AI] for a test case for this and it ended up deserializing the query and checking for that.
I would not add that kind of implementation-specific test cases into my codebase but if you feel it might help with future regression testing I can add something along these lines to the PR as well.

it('should not match a previous day transaction for a daily schedule', () => {
      // Scenario: daily schedule, next occurrence is 2025-03-05.
      // A transaction was posted yesterday (2025-03-04) for the prior
      // occurrence. The query should NOT look back far enough to catch it.
      const schedule = {
        id: 'daily-schedule-1',
        next_date: '2025-03-05',
        _conditions: [
          {
            field: 'date',
            op: 'isapprox',
            value: { start: '2025-03-01', frequency: 'daily' },
          },
        ],
      };

      const query = getHasTransactionsQuery([schedule]);
      const serialized = query.serialize();

      // Extract the date filter from the generated query.
      // Structure: filterExpressions[0] = { $or: [{ $and: { schedule, date: { $gte: ... } } }] }
      const orFilter = serialized.filterExpressions[0] as {
        $or: Array<{ $and: { date: { $gte: string } } }>;
      };
      const dateGte = orFilter.$or[0].$and.date.$gte;

      // For a daily schedule whose next_date is '2025-03-05', the query
      // should only look for transactions on or after '2025-03-05' (the
      // next_date itself) — not 2 days earlier. A 2-day lookback would
      // catch yesterday's transaction and falsely mark this as 'paid'.
      //
      // BUG: The current code uses subDays(next_date, 2) = '2025-03-03',
      // which is too early and causes the false positive.
      expect(dateGte).toBe('2025-03-05');
    });
<!-- gh-comment-id:4105167516 --> @anoff commented on GitHub (Mar 22, 2026): I took a stab at analyzing the potential root cause. Disclaimer: Initially I asked Claude to point me in the right direction, but then did my own digging through the typescript code. So this is not AI-assisted but not vibed. ## Potential root cause The [getHasTransactionsQuery(schedules)](https://github.com/actualbudget/actual/blob/master/packages/loot-core/src/shared/schedules.ts#L58) allows past transactions to be off by 2 days, probably to accomodate for slight deviations in manual transactions and bank transfers. However when there are daily transactions that means it always finds a existing transaction within the last 2 days as well. See next chapter for what I believe is happening overall. ```ts date: { $gte: dateCond && dateCond.op === 'is' ? schedule.next_date : monthUtils.subDays(schedule.next_date, 2), }, ``` ## Assumptions Based on what I understand from the code, I think this is what's going wrong: 1. `app.ts:advanceSchedulesService()` is in charge of checking schedules and proceeding them 2. the path to adding a new transaction automatically `await postTransactionForSchedule({ id: schedule.id **});` can only be reached if the `status != 'paid'` 3. in case of recurring payments the `dateCond.op != 'is'` (only used for schedules with 1 fixed date) 4. the 2 day lookback described above kicks in and inside `advanceSchedulesService()` the schedule is considered as `'paid'` because there was a transaction yesterday 5. since it's already paid, no new transaction is created 6. on day3, the 2 day lookback finds no transaction, hence it creates one 7. that's how I end up with a transaction every 3 days even though I set it as daily ## Potential fix There are a few ways to fix, this, please advise which you prefer and I'll push a PR :) ### A: Add a case for automatic transactions As automatically created transactions should always be on time, we just disable the 2 day lookback for those. ```ts $gte: dateCond && dateCond.op === 'is' ? schedule.next_date : schedule.posts_transaction ? schedule.next_date // auto-posted: exact date, no lookback : monthUtils.subDays(schedule.next_date, 2), // manual transaction matching: keep current behavior ``` ### B: Add a case for daily transactions Similar as above but assuming the special scenario is _daily schedules_. For any daily schedule - automatically added or not - it does not make sense to check for existing transactions other than on the current day. ```ts $gte: dateCond && dateCond.op === 'is' ? schedule.next_date : dateCond?.value?.frequency === 'daily' ? schedule.next_date // no lookback for daily : monthUtils.subDays(schedule.next_date, 2), // keep 2-day lookback for weekly+ schedules ``` ### C: Combine both conditions Would specifically target _daily + automatically created_ transaction schedules only. Personally I would go with A because the 2 day lookback is a feature only relevant for manual (or synced) transactions, not for those created by AB directly. ## Test Case I asked [AI] for a test case for this and it ended up deserializing the query and checking for that. I would not add that kind of implementation-specific test cases into my codebase but if you feel it might help with future regression testing I can add something along these lines to the PR as well. ```ts it('should not match a previous day transaction for a daily schedule', () => { // Scenario: daily schedule, next occurrence is 2025-03-05. // A transaction was posted yesterday (2025-03-04) for the prior // occurrence. The query should NOT look back far enough to catch it. const schedule = { id: 'daily-schedule-1', next_date: '2025-03-05', _conditions: [ { field: 'date', op: 'isapprox', value: { start: '2025-03-01', frequency: 'daily' }, }, ], }; const query = getHasTransactionsQuery([schedule]); const serialized = query.serialize(); // Extract the date filter from the generated query. // Structure: filterExpressions[0] = { $or: [{ $and: { schedule, date: { $gte: ... } } }] } const orFilter = serialized.filterExpressions[0] as { $or: Array<{ $and: { date: { $gte: string } } }>; }; const dateGte = orFilter.$or[0].$and.date.$gte; // For a daily schedule whose next_date is '2025-03-05', the query // should only look for transactions on or after '2025-03-05' (the // next_date itself) — not 2 days earlier. A 2-day lookback would // catch yesterday's transaction and falsely mark this as 'paid'. // // BUG: The current code uses subDays(next_date, 2) = '2025-03-03', // which is too early and causes the false positive. expect(dateGte).toBe('2025-03-05'); }); ```
Author
Owner

@matt-fidd commented on GitHub (Mar 23, 2026):

@anoff thanks for doing the digging here, of your options "A" seems the most sensible

<!-- gh-comment-id:4111080026 --> @matt-fidd commented on GitHub (Mar 23, 2026): @anoff thanks for doing the digging here, of your options "A" seems the most sensible
Author
Owner

@matt-fidd commented on GitHub (Mar 23, 2026):

This is a duplicate of https://github.com/actualbudget/actual/issues/1847, I'll close this off in favour of the older one, but the debugging here is valuable.

<!-- gh-comment-id:4111091602 --> @matt-fidd commented on GitHub (Mar 23, 2026): This is a duplicate of https://github.com/actualbudget/actual/issues/1847, I'll close this off in favour of the older one, but the debugging here is valuable.
Author
Owner

@anoff commented on GitHub (Mar 27, 2026):

This is a duplicate of #1847

Whoops, sorry I searched but didn't find that one.
I'll create a PR for #1847

<!-- gh-comment-id:4139388563 --> @anoff commented on GitHub (Mar 27, 2026): > This is a duplicate of [#1847](https://github.com/actualbudget/actual/issues/1847) Whoops, sorry I searched but didn't find that one. I'll create a PR for #1847
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/actual#52147