Persist last_reconciled instead of using last transactions date #41

Merged
jacob1123 merged 4 commits from reconciled-as-field into master 2022-03-14 21:01:08 +01:00
9 changed files with 95 additions and 35 deletions

View File

@ -5,7 +5,7 @@ package postgres
import ( import (
"context" "context"
"time" "database/sql"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric" "git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid" "github.com/google/uuid"
@ -15,7 +15,7 @@ const createAccount = `-- name: CreateAccount :one
INSERT INTO accounts INSERT INTO accounts
(name, budget_id) (name, budget_id)
VALUES ($1, $2) VALUES ($1, $2)
RETURNING id, budget_id, name, on_budget, is_open RETURNING id, budget_id, name, on_budget, is_open, last_reconciled
` `
type CreateAccountParams struct { type CreateAccountParams struct {
@ -32,12 +32,13 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A
&i.Name, &i.Name,
&i.OnBudget, &i.OnBudget,
&i.IsOpen, &i.IsOpen,
&i.LastReconciled,
) )
return i, err return i, err
} }
const getAccount = `-- name: GetAccount :one const getAccount = `-- name: GetAccount :one
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open FROM accounts SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled FROM accounts
WHERE accounts.id = $1 WHERE accounts.id = $1
` `
@ -50,12 +51,13 @@ func (q *Queries) GetAccount(ctx context.Context, id uuid.UUID) (Account, error)
&i.Name, &i.Name,
&i.OnBudget, &i.OnBudget,
&i.IsOpen, &i.IsOpen,
&i.LastReconciled,
) )
return i, err return i, err
} }
const getAccounts = `-- name: GetAccounts :many const getAccounts = `-- name: GetAccounts :many
SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open FROM accounts SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled FROM accounts
WHERE accounts.budget_id = $1 WHERE accounts.budget_id = $1
AND accounts.is_open = TRUE AND accounts.is_open = TRUE
ORDER BY accounts.name ORDER BY accounts.name
@ -76,6 +78,7 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
&i.Name, &i.Name,
&i.OnBudget, &i.OnBudget,
&i.IsOpen, &i.IsOpen,
&i.LastReconciled,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -91,8 +94,7 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
} }
const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many
SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled,
(SELECT MAX(transactions.date) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.status = 'Reconciled')::date as last_reconciled,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance
@ -107,7 +109,7 @@ type GetAccountsWithBalanceRow struct {
Name string Name string
OnBudget bool OnBudget bool
IsOpen bool IsOpen bool
LastReconciled time.Time LastReconciled sql.NullTime
WorkingBalance numeric.Numeric WorkingBalance numeric.Numeric
ClearedBalance numeric.Numeric ClearedBalance numeric.Numeric
ReconciledBalance numeric.Numeric ReconciledBalance numeric.Numeric
@ -193,13 +195,24 @@ func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams)
return items, nil return items, nil
} }
const setLastReconciled = `-- name: SetLastReconciled :exec
UPDATE accounts
SET last_reconciled = NOW()
WHERE accounts.id = $1
`
func (q *Queries) SetLastReconciled(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, setLastReconciled, id)
return err
}
const updateAccount = `-- name: UpdateAccount :one const updateAccount = `-- name: UpdateAccount :one
UPDATE accounts UPDATE accounts
SET name = $1, SET name = $1,
on_budget = $2, on_budget = $2,
is_open = $3 is_open = $3
WHERE accounts.id = $4 WHERE accounts.id = $4
RETURNING id, budget_id, name, on_budget, is_open RETURNING id, budget_id, name, on_budget, is_open, last_reconciled
` `
type UpdateAccountParams struct { type UpdateAccountParams struct {
@ -223,6 +236,7 @@ func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (A
&i.Name, &i.Name,
&i.OnBudget, &i.OnBudget,
&i.IsOpen, &i.IsOpen,
&i.LastReconciled,
) )
return i, err return i, err
} }

View File

@ -37,6 +37,7 @@ type Account struct {
Name string Name string
OnBudget bool OnBudget bool
IsOpen bool IsOpen bool
LastReconciled sql.NullTime
} }
type Assignment struct { type Assignment struct {

View File

@ -15,8 +15,7 @@ AND accounts.is_open = TRUE
ORDER BY accounts.name; ORDER BY accounts.name;
-- name: GetAccountsWithBalance :many -- name: GetAccountsWithBalance :many
SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled,
(SELECT MAX(transactions.date) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.status = 'Reconciled')::date as last_reconciled,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance
@ -39,3 +38,8 @@ SET name = $1,
is_open = $3 is_open = $3
WHERE accounts.id = $4 WHERE accounts.id = $4
RETURNING *; RETURNING *;
-- name: SetLastReconciled :exec
UPDATE accounts
SET last_reconciled = NOW()
WHERE accounts.id = $1;

View File

@ -0,0 +1,12 @@
-- +goose Up
ALTER TABLE accounts ADD COLUMN last_reconciled date NULL;
UPDATE accounts
SET last_reconciled = (
SELECT MAX(transactions.date)
FROM transactions
WHERE transactions.account_id = accounts.id
AND transactions.status = 'Reconciled'
);
-- +goose Down
ALTER TABLE accounts DROP COLUMN last_reconciled;

View File

@ -65,6 +65,12 @@ func (h *Handler) reconcileTransactions(c *gin.Context) {
return return
} }
err = h.Service.SetLastReconciled(c.Request.Context(), accountUUID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("set last reconciled: %w", err))
return
}
err = tx.Commit() err = tx.Commit()
if err != nil { if err != nil {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("commit: %w", err)) c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("commit: %w", err))

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useBudgetsStore } from '../stores/budget';
import { Account } from '../stores/budget-account';
const props = defineProps<{
account: Account
}>();
const budgetStore = useBudgetsStore();
const CurrentBudgetID = computed(() => budgetStore.CurrentBudgetID);
const days = 24 * 60 * 60 * 1000;
function daysSinceLastReconciled() {
const now = new Date().getTime();
const diff = new Date(now).getTime() - props.account.LastReconciled.Time.getTime();
return Math.floor(diff / days);
}
</script>
<template>
<span>
<router-link
:to="'/budget/' + CurrentBudgetID + '/account/' + account.ID">
{{account.Name}}
</router-link>
<span v-if="props.account.LastReconciled.Valid && daysSinceLastReconciled() > 7" class="font-bold bg-gray-500 rounded-md text-sm px-2 mx-2 py-1 no-underline">
{{daysSinceLastReconciled()}}
</span>
</span>
</template>

View File

@ -4,6 +4,7 @@ import Currency from "../components/Currency.vue"
import { useBudgetsStore } from "../stores/budget" import { useBudgetsStore } from "../stores/budget"
import { Account, useAccountStore } from "../stores/budget-account" import { Account, useAccountStore } from "../stores/budget-account"
import { useSettingsStore } from "../stores/settings" import { useSettingsStore } from "../stores/settings"
import AccountWithReconciled from "../components/AccountWithReconciled.vue";
const settings = useSettingsStore(); const settings = useSettingsStore();
const ExpandMenu = computed(() => settings.Menu.Expand); const ExpandMenu = computed(() => settings.Menu.Expand);
@ -18,17 +19,6 @@ const OnBudgetAccounts = computed(() => accountStore.OnBudgetAccounts);
const OffBudgetAccounts = computed(() => accountStore.OffBudgetAccounts); const OffBudgetAccounts = computed(() => accountStore.OffBudgetAccounts);
const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBalance); const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBalance);
const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBalance); const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBalance);
function isRecentlyReconciled(account: Account) {
const now = new Date().getTime();
const recently = 7 * 24 * 60 * 60 * 1000;
return new Date(now - recently).getTime() < account.LastReconciled.getTime();
}
function getAccountName(account: Account) {
const reconciledMarker = isRecentlyReconciled(account) ? "" : " *";
return account.Name + reconciledMarker;
}
</script> </script>
<template> <template>
@ -56,9 +46,7 @@ function getAccountName(account: Account) {
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OnBudgetAccountsBalance" /> <Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OnBudgetAccountsBalance" />
</div> </div>
<div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between"> <div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between">
<router-link <AccountWithReconciled :account="account" />
:to="'/budget/' + CurrentBudgetID + '/account/' + account.ID"
>{{ getAccountName(account) }}</router-link>
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" /> <Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
</div> </div>
</li> </li>
@ -68,9 +56,7 @@ function getAccountName(account: Account) {
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OffBudgetAccountsBalance" /> <Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OffBudgetAccountsBalance" />
</div> </div>
<div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between"> <div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between">
<router-link <AccountWithReconciled :account="account" />
:to="'/budget/' + CurrentBudgetID + '/account/' + account.ID"
>{{ getAccountName(account) }}</router-link>
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" /> <Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
</div> </div>
</li> </li>

View File

@ -21,7 +21,12 @@ export interface Account {
WorkingBalance: number WorkingBalance: number
ReconciledBalance: number ReconciledBalance: number
Transactions: string[] Transactions: string[]
LastReconciled: Date LastReconciled: NullDate
}
interface NullDate {
Valid: boolean
Time: Date
} }
export interface Category { export interface Category {

View File

@ -58,7 +58,8 @@ export const useBudgetsStore = defineStore('budget', {
for (const account of response.Accounts || []) { for (const account of response.Accounts || []) {
const existingAccount = accounts.Accounts.get(account.ID); const existingAccount = accounts.Accounts.get(account.ID);
account.Transactions = existingAccount?.Transactions ?? []; account.Transactions = existingAccount?.Transactions ?? [];
account.LastReconciled = new Date(account.LastReconciled); if(account.LastReconciled.Valid)
account.LastReconciled.Time = new Date(account.LastReconciled.Time);
accounts.Accounts.set(account.ID, account); accounts.Accounts.set(account.ID, account);
} }
for (const category of response.Categories || []) { for (const category of response.Categories || []) {