account/internal/store/transaction.go
GnomeZworc 0f2d7126eb
ajoute snapshot
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-06-13 22:52:13 +02:00

223 lines
7.5 KiB
Go

package store
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
type Transaction struct {
ID int64 `json:"id"`
Date string `json:"date"`
AccountSourceID *int32 `json:"account_source_id,omitempty"`
InstrumentSourceID *int32 `json:"instrument_source_id,omitempty"`
QuantiteSource *float64 `json:"quantite_source,omitempty"`
AccountDestID *int32 `json:"account_dest_id,omitempty"`
InstrumentDestID *int32 `json:"instrument_dest_id,omitempty"`
QuantiteDest *float64 `json:"quantite_dest,omitempty"`
Tiers *string `json:"tiers,omitempty"`
Label string `json:"label"`
Categorie *string `json:"categorie,omitempty"`
Validated bool `json:"validated"`
RecurringRuleID *int32 `json:"recurring_rule_id,omitempty"`
}
// AccountTransaction est la vue centrée sur un compte :
// montant signé (+ crédit, - débit), instrument du côté concerné.
type AccountTransaction struct {
ID int64 `json:"id"`
Date string `json:"date"`
Montant float64 `json:"montant"`
InstrumentID int32 `json:"instrument_id"`
Tiers *string `json:"tiers,omitempty"`
Label string `json:"label"`
Categorie *string `json:"categorie,omitempty"`
Validated bool `json:"validated"`
ContrepartieAccountID *int32 `json:"contrepartie_account_id,omitempty"`
}
type CreateTransactionParams struct {
Date string `json:"date"`
AccountSourceID *int32 `json:"account_source_id,omitempty"`
InstrumentSourceID *int32 `json:"instrument_source_id,omitempty"`
QuantiteSource *float64 `json:"quantite_source,omitempty"`
AccountDestID *int32 `json:"account_dest_id,omitempty"`
InstrumentDestID *int32 `json:"instrument_dest_id,omitempty"`
QuantiteDest *float64 `json:"quantite_dest,omitempty"`
Tiers *string `json:"tiers,omitempty"`
Label string `json:"label"`
Categorie *string `json:"categorie,omitempty"`
Validated bool `json:"validated"`
}
type AccountTransactionFilters struct {
Validated *bool
Pending *bool
From *string
To *string
}
const txCols = `id, date::text, account_source_id, instrument_source_id, quantite_source,
account_dest_id, instrument_dest_id, quantite_dest,
tiers, label, categorie, validated, recurring_rule_id`
// ListAccountTransactions retourne les transactions d'un compte avec montant signé.
func (s *Store) ListAccountTransactions(ctx context.Context, accountID int32, f AccountTransactionFilters) ([]AccountTransaction, error) {
q := `
SELECT
id,
date::text,
CASE WHEN account_dest_id = $1 THEN quantite_dest ELSE -quantite_source END AS montant,
CASE WHEN account_dest_id = $1 THEN instrument_dest_id ELSE instrument_source_id END AS instrument_id,
tiers, label, categorie, validated,
CASE WHEN account_dest_id = $1 THEN account_source_id ELSE account_dest_id END AS contrepartie_account_id
FROM transaction
WHERE (account_source_id = $1 OR account_dest_id = $1)`
args := []any{accountID}
i := 2
if f.Pending != nil && *f.Pending {
q += ` AND validated = false AND date <= CURRENT_DATE`
} else if f.Validated != nil {
q += fmt.Sprintf(` AND validated = $%d`, i)
args = append(args, *f.Validated)
i++
}
if f.From != nil {
q += fmt.Sprintf(` AND date >= $%d::date`, i)
args = append(args, *f.From)
i++
}
if f.To != nil {
q += fmt.Sprintf(` AND date <= $%d::date`, i)
args = append(args, *f.To)
i++
}
q += ` ORDER BY date DESC, id DESC`
rows, err := s.pool.Query(ctx, q, args...)
if err != nil {
return nil, err
}
return pgx.CollectRows(rows, pgx.RowToStructByName[AccountTransaction])
}
func (s *Store) GetTransaction(ctx context.Context, id int64) (Transaction, error) {
rows, err := s.pool.Query(ctx,
`SELECT `+txCols+` FROM transaction WHERE id = $1`, id)
if err != nil {
return Transaction{}, err
}
return pgx.CollectOneRow(rows, pgx.RowToStructByName[Transaction])
}
func (s *Store) CreateTransaction(ctx context.Context, p CreateTransactionParams) (Transaction, error) {
rows, err := s.pool.Query(ctx,
`INSERT INTO transaction (
date, account_source_id, instrument_source_id, quantite_source,
account_dest_id, instrument_dest_id, quantite_dest,
tiers, label, categorie, validated
) VALUES (
$1::date, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
) RETURNING `+txCols,
p.Date, p.AccountSourceID, p.InstrumentSourceID, p.QuantiteSource,
p.AccountDestID, p.InstrumentDestID, p.QuantiteDest,
p.Tiers, p.Label, p.Categorie, p.Validated)
if err != nil {
return Transaction{}, err
}
tx, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[Transaction])
if err != nil {
return tx, err
}
s.invalidateAccounts(ctx, tx.Date, tx.AccountSourceID, tx.AccountDestID)
return tx, nil
}
func (s *Store) UpdateTransaction(ctx context.Context, id int64, p CreateTransactionParams) (Transaction, error) {
// Récupérer l'ancienne date pour invalider à partir du MIN(ancienne, nouvelle).
old, err := s.GetTransaction(ctx, id)
if err != nil {
return Transaction{}, err
}
rows, err := s.pool.Query(ctx,
`UPDATE transaction SET
date = $2::date,
account_source_id = $3, instrument_source_id = $4, quantite_source = $5,
account_dest_id = $6, instrument_dest_id = $7, quantite_dest = $8,
tiers = $9, label = $10, categorie = $11, validated = $12
WHERE id = $1
RETURNING `+txCols,
id, p.Date,
p.AccountSourceID, p.InstrumentSourceID, p.QuantiteSource,
p.AccountDestID, p.InstrumentDestID, p.QuantiteDest,
p.Tiers, p.Label, p.Categorie, p.Validated)
if err != nil {
return Transaction{}, err
}
tx, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[Transaction])
if err != nil {
return tx, err
}
// Invalider depuis la date la plus ancienne (ancienne ou nouvelle).
earliest := minDateStr(old.Date, tx.Date)
s.invalidateAccounts(ctx, earliest, tx.AccountSourceID, tx.AccountDestID)
// Si les comptes ont changé, invalider aussi les anciens.
s.invalidateAccounts(ctx, earliest, old.AccountSourceID, old.AccountDestID)
return tx, nil
}
func (s *Store) SetValidated(ctx context.Context, id int64, validated bool) (Transaction, error) {
rows, err := s.pool.Query(ctx,
`UPDATE transaction SET validated = $2 WHERE id = $1 RETURNING `+txCols,
id, validated)
if err != nil {
return Transaction{}, err
}
tx, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[Transaction])
if err != nil {
return tx, err
}
s.invalidateAccounts(ctx, tx.Date, tx.AccountSourceID, tx.AccountDestID)
return tx, nil
}
func (s *Store) DeleteTransaction(ctx context.Context, id int64) error {
tx, err := s.GetTransaction(ctx, id)
if err != nil {
return err
}
if _, err := s.pool.Exec(ctx, `DELETE FROM transaction WHERE id = $1`, id); err != nil {
return err
}
s.invalidateAccounts(ctx, tx.Date, tx.AccountSourceID, tx.AccountDestID)
return nil
}
// invalidateAccounts marque les comptes non-nil comme devant être recalculés depuis dateStr.
func (s *Store) invalidateAccounts(ctx context.Context, dateStr string, accountIDs ...*int32) {
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return
}
seen := map[int32]bool{}
for _, id := range accountIDs {
if id != nil && !seen[*id] {
seen[*id] = true
s.InvalidateSnapshot(ctx, *id, date) //nolint:errcheck
}
}
}
// minDateStr retourne la plus petite des deux dates au format YYYY-MM-DD.
func minDateStr(a, b string) string {
if a <= b {
return a
}
return b
}