feat: #44 fatura de cartão com data futura fica pendente até confirmação
- Nova tabela pending_bill_imports (migration 016) - ImportHandler.Confirm: quando is_credit_card=true e payment_date > hoje, salva como pending em vez de inserir transações - Novos endpoints: GET /pending-bills, POST /pending-bills/:id/confirm, DELETE /pending-bills/:id - Dashboard inclui pending_bill_imports no payload - Frontend: resultado "fatura salva como pendente" no ImportView - AccountsView exibe widget de faturas pendentes com ações de confirmar/descartar - dashboard_test: mock de PendingBillRepo + TransactionRepository Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -65,7 +65,10 @@ func main() {
|
||||
|
||||
transactionRepo := repository.NewTransactionRepository(pool)
|
||||
importSvc := service.NewImportService(transactionRepo)
|
||||
importHandler := handler.NewImportHandler(importSvc, gameSvc)
|
||||
pendingBillRepo := repository.NewPendingBillRepository(pool)
|
||||
pendingBillSvc := service.NewPendingBillService(pendingBillRepo, transactionRepo)
|
||||
importHandler := handler.NewImportHandler(importSvc, gameSvc, pendingBillSvc)
|
||||
pendingBillHandler := handler.NewPendingBillHandler(pendingBillSvc, gameSvc)
|
||||
|
||||
manualTxRepo := repository.NewManualTransactionRepository(pool)
|
||||
txSvc := service.NewTransactionService(manualTxRepo)
|
||||
@@ -84,7 +87,7 @@ func main() {
|
||||
creditBillHandler := handler.NewCreditBillHandler(creditBillSvc)
|
||||
|
||||
dashboardRepo := repository.NewDashboardRepository(pool)
|
||||
dashboardSvc := service.NewDashboardService(dashboardRepo, recurringSvc, accountRepo, creditBillSvc)
|
||||
dashboardSvc := service.NewDashboardService(dashboardRepo, recurringSvc, accountRepo, creditBillSvc, pendingBillSvc)
|
||||
dashboardHandler := handler.NewDashboardHandler(dashboardSvc)
|
||||
|
||||
r.Get("/health", handler.Health)
|
||||
@@ -108,6 +111,10 @@ func main() {
|
||||
r.Post("/imports/preview", importHandler.Preview)
|
||||
r.Post("/imports/confirm", importHandler.Confirm)
|
||||
|
||||
r.Get("/pending-bills", pendingBillHandler.List)
|
||||
r.Post("/pending-bills/{id}/confirm", pendingBillHandler.Confirm)
|
||||
r.Delete("/pending-bills/{id}", pendingBillHandler.Discard)
|
||||
|
||||
r.Get("/transactions", txHandler.List)
|
||||
r.Post("/transactions", txHandler.Create)
|
||||
r.Put("/transactions/{id}", txHandler.Update)
|
||||
|
||||
@@ -11,12 +11,13 @@ import (
|
||||
const maxUploadSize = 10 << 20 // 10 MB
|
||||
|
||||
type ImportHandler struct {
|
||||
svc *service.ImportService
|
||||
gameSvc *service.GameService
|
||||
svc *service.ImportService
|
||||
gameSvc *service.GameService
|
||||
pendingSvc *service.PendingBillService
|
||||
}
|
||||
|
||||
func NewImportHandler(svc *service.ImportService, gameSvc *service.GameService) *ImportHandler {
|
||||
return &ImportHandler{svc: svc, gameSvc: gameSvc}
|
||||
func NewImportHandler(svc *service.ImportService, gameSvc *service.GameService, pendingSvc *service.PendingBillService) *ImportHandler {
|
||||
return &ImportHandler{svc: svc, gameSvc: gameSvc, pendingSvc: pendingSvc}
|
||||
}
|
||||
|
||||
// Preview parses the uploaded file and returns rows with duplicate flags.
|
||||
@@ -59,11 +60,14 @@ func (h *ImportHandler) Preview(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Confirm saves the rows provided in the request body (after user review).
|
||||
// POST /api/imports/confirm
|
||||
// When is_credit_card=true and payment_date is in the future, saves as pending instead of transactions.
|
||||
func (h *ImportHandler) Confirm(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Filename string `json:"filename"`
|
||||
Filename string `json:"filename"`
|
||||
Rows []model.ImportRow `json:"rows"`
|
||||
ParseErrCount int `json:"parse_error_count"`
|
||||
ParseErrCount int `json:"parse_error_count"`
|
||||
IsCreditCard bool `json:"is_credit_card"`
|
||||
PaymentDate string `json:"payment_date"` // YYYY-MM-DD
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid JSON")
|
||||
@@ -74,6 +78,24 @@ func (h *ImportHandler) Confirm(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if body.IsCreditCard && body.PaymentDate != "" {
|
||||
saved, pending, err := h.pendingSvc.MaybeSaveAsPending(r.Context(), body.Filename, body.PaymentDate, body.Rows)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to save pending bill")
|
||||
return
|
||||
}
|
||||
if saved {
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"pending": true,
|
||||
"pending_bill": pending,
|
||||
"imported": 0,
|
||||
"duplicates": len(body.Rows) - len(pending.Rows),
|
||||
"errors": body.ParseErrCount,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.svc.Confirm(r.Context(), body.Filename, body.Rows, body.ParseErrCount)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to save transactions")
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"financeiro-carvalho/internal/service"
|
||||
)
|
||||
|
||||
type PendingBillHandler struct {
|
||||
svc *service.PendingBillService
|
||||
gameSvc *service.GameService
|
||||
}
|
||||
|
||||
func NewPendingBillHandler(svc *service.PendingBillService, gameSvc *service.GameService) *PendingBillHandler {
|
||||
return &PendingBillHandler{svc: svc, gameSvc: gameSvc}
|
||||
}
|
||||
|
||||
func (h *PendingBillHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := h.svc.List(r.Context())
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to list pending bills")
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, items)
|
||||
}
|
||||
|
||||
func (h *PendingBillHandler) Confirm(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
if err := h.svc.Confirm(r.Context(), id); err != nil {
|
||||
if err == service.ErrPendingBillNotFound {
|
||||
respondError(w, http.StatusNotFound, "pending bill not found")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "failed to confirm pending bill")
|
||||
return
|
||||
}
|
||||
h.gameSvc.NotifyAction(r.Context(), "import_confirmed", map[string]any{"count": 1})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *PendingBillHandler) Discard(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
if err := h.svc.Discard(r.Context(), id); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to discard pending bill")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -53,6 +53,9 @@ var m014 string
|
||||
//go:embed sql/015_accounts_cdi_percentage.sql
|
||||
var m015 string
|
||||
|
||||
//go:embed sql/016_pending_bill_imports.sql
|
||||
var m016 string
|
||||
|
||||
func Run(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
// Bootstrap: ensure schema_migrations table exists before checking versions.
|
||||
if _, err := pool.Exec(ctx, `
|
||||
@@ -64,7 +67,7 @@ func Run(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
return fmt.Errorf("bootstrap schema_migrations: %w", err)
|
||||
}
|
||||
|
||||
migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012, m013, m014, m015}
|
||||
migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012, m013, m014, m015, m016}
|
||||
for i, sql := range migrations {
|
||||
version := i + 1
|
||||
var applied bool
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS pending_bill_imports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
profile_id INTEGER NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
payment_date DATE NOT NULL,
|
||||
total NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
rows JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -24,15 +24,16 @@ type RecentTransaction struct {
|
||||
}
|
||||
|
||||
type DashboardData struct {
|
||||
Month string `json:"month"`
|
||||
TotalIncome float64 `json:"total_income"`
|
||||
TotalExpenses float64 `json:"total_expenses"`
|
||||
SavingsPct float64 `json:"savings_pct"`
|
||||
TotalPatrimony float64 `json:"total_patrimony"`
|
||||
ByCategory []CategoryTotal `json:"by_category"`
|
||||
MonthlyEvolution []MonthEvolution `json:"monthly_evolution"`
|
||||
RecentTransactions []RecentTransaction `json:"recent_transactions"`
|
||||
PendingRecurring int `json:"pending_recurring"`
|
||||
PendingIncomeRecurrings []PendingIncome `json:"pending_income_recurrings"`
|
||||
CurrentBills []CreditBill `json:"current_bills"`
|
||||
Month string `json:"month"`
|
||||
TotalIncome float64 `json:"total_income"`
|
||||
TotalExpenses float64 `json:"total_expenses"`
|
||||
SavingsPct float64 `json:"savings_pct"`
|
||||
TotalPatrimony float64 `json:"total_patrimony"`
|
||||
ByCategory []CategoryTotal `json:"by_category"`
|
||||
MonthlyEvolution []MonthEvolution `json:"monthly_evolution"`
|
||||
RecentTransactions []RecentTransaction `json:"recent_transactions"`
|
||||
PendingRecurring int `json:"pending_recurring"`
|
||||
PendingIncomeRecurrings []PendingIncome `json:"pending_income_recurrings"`
|
||||
CurrentBills []CreditBill `json:"current_bills"`
|
||||
PendingBillImports []PendingBillImport `json:"pending_bill_imports"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
type PendingBillImport struct {
|
||||
ID int `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
PaymentDate string `json:"payment_date"`
|
||||
Total float64 `json:"total"`
|
||||
Rows []ImportRow `json:"rows"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"financeiro-carvalho/internal/middleware"
|
||||
"financeiro-carvalho/internal/model"
|
||||
)
|
||||
|
||||
type PendingBillRepository interface {
|
||||
List(ctx context.Context) ([]model.PendingBillImport, error)
|
||||
Create(ctx context.Context, filename, paymentDate string, total float64, rows []model.ImportRow) (*model.PendingBillImport, error)
|
||||
GetByID(ctx context.Context, id int) (*model.PendingBillImport, error)
|
||||
Delete(ctx context.Context, id int) error
|
||||
}
|
||||
|
||||
type pendingBillRepo struct{ pool *pgxpool.Pool }
|
||||
|
||||
func NewPendingBillRepository(pool *pgxpool.Pool) PendingBillRepository {
|
||||
return &pendingBillRepo{pool: pool}
|
||||
}
|
||||
|
||||
func (r *pendingBillRepo) List(ctx context.Context) ([]model.PendingBillImport, error) {
|
||||
pid := middleware.ProfileIDFromCtx(ctx)
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id, filename, payment_date::text, total, rows, created_at::text
|
||||
FROM pending_bill_imports
|
||||
WHERE profile_id = $1
|
||||
ORDER BY payment_date
|
||||
`, pid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []model.PendingBillImport
|
||||
for rows.Next() {
|
||||
var p model.PendingBillImport
|
||||
var rowsJSON []byte
|
||||
if err := rows.Scan(&p.ID, &p.Filename, &p.PaymentDate, &p.Total, &rowsJSON, &p.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal(rowsJSON, &p.Rows)
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (r *pendingBillRepo) Create(ctx context.Context, filename, paymentDate string, total float64, rows []model.ImportRow) (*model.PendingBillImport, error) {
|
||||
pid := middleware.ProfileIDFromCtx(ctx)
|
||||
rowsJSON, err := json.Marshal(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p model.PendingBillImport
|
||||
var rowsBack []byte
|
||||
err = r.pool.QueryRow(ctx, `
|
||||
INSERT INTO pending_bill_imports (profile_id, filename, payment_date, total, rows)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, filename, payment_date::text, total, rows, created_at::text
|
||||
`, pid, filename, paymentDate, total, rowsJSON).
|
||||
Scan(&p.ID, &p.Filename, &p.PaymentDate, &p.Total, &rowsBack, &p.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal(rowsBack, &p.Rows)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (r *pendingBillRepo) GetByID(ctx context.Context, id int) (*model.PendingBillImport, error) {
|
||||
pid := middleware.ProfileIDFromCtx(ctx)
|
||||
var p model.PendingBillImport
|
||||
var rowsJSON []byte
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT id, filename, payment_date::text, total, rows, created_at::text
|
||||
FROM pending_bill_imports
|
||||
WHERE id = $1 AND profile_id = $2
|
||||
`, id, pid).Scan(&p.ID, &p.Filename, &p.PaymentDate, &p.Total, &rowsJSON, &p.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal(rowsJSON, &p.Rows)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (r *pendingBillRepo) Delete(ctx context.Context, id int) error {
|
||||
pid := middleware.ProfileIDFromCtx(ctx)
|
||||
_, err := r.pool.Exec(ctx, `DELETE FROM pending_bill_imports WHERE id = $1 AND profile_id = $2`, id, pid)
|
||||
return err
|
||||
}
|
||||
@@ -19,14 +19,15 @@ type PatrimonySource interface {
|
||||
}
|
||||
|
||||
type DashboardService struct {
|
||||
repo DashboardRepo
|
||||
recurrSvc *RecurringService
|
||||
patrimony PatrimonySource
|
||||
billSvc *CreditBillService
|
||||
repo DashboardRepo
|
||||
recurrSvc *RecurringService
|
||||
patrimony PatrimonySource
|
||||
billSvc *CreditBillService
|
||||
pendingBillSvc *PendingBillService
|
||||
}
|
||||
|
||||
func NewDashboardService(repo DashboardRepo, recurrSvc *RecurringService, patrimony PatrimonySource, billSvc *CreditBillService) *DashboardService {
|
||||
return &DashboardService{repo: repo, recurrSvc: recurrSvc, patrimony: patrimony, billSvc: billSvc}
|
||||
func NewDashboardService(repo DashboardRepo, recurrSvc *RecurringService, patrimony PatrimonySource, billSvc *CreditBillService, pendingBillSvc *PendingBillService) *DashboardService {
|
||||
return &DashboardService{repo: repo, recurrSvc: recurrSvc, patrimony: patrimony, billSvc: billSvc, pendingBillSvc: pendingBillSvc}
|
||||
}
|
||||
|
||||
func (s *DashboardService) Get(ctx context.Context, month string) (*model.DashboardData, error) {
|
||||
@@ -109,6 +110,11 @@ func (s *DashboardService) Get(ctx context.Context, month string) (*model.Dashbo
|
||||
currentBills = []model.CreditBill{}
|
||||
}
|
||||
|
||||
pendingBillImports, _ := s.pendingBillSvc.List(ctx)
|
||||
if pendingBillImports == nil {
|
||||
pendingBillImports = []model.PendingBillImport{}
|
||||
}
|
||||
|
||||
return &model.DashboardData{
|
||||
Month: month,
|
||||
TotalIncome: income,
|
||||
@@ -121,5 +127,6 @@ func (s *DashboardService) Get(ctx context.Context, month string) (*model.Dashbo
|
||||
PendingRecurring: pending,
|
||||
PendingIncomeRecurrings: pendingIncome,
|
||||
CurrentBills: currentBills,
|
||||
PendingBillImports: pendingBillImports,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,21 @@ import (
|
||||
"financeiro-carvalho/internal/service"
|
||||
)
|
||||
|
||||
type mockImportTxRepo struct{}
|
||||
|
||||
func (m *mockImportTxRepo) IsDuplicate(_ context.Context, _, _ string, _ float64) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (m *mockImportTxRepo) IsExternalIDKnown(_ context.Context, _ string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (m *mockImportTxRepo) BulkInsert(_ context.Context, rows []model.ImportRow) (int, error) {
|
||||
return len(rows), nil
|
||||
}
|
||||
func (m *mockImportTxRepo) SaveImportLog(_ context.Context, _, _ string, _, _, _ int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockDashboardRepo struct {
|
||||
income float64
|
||||
expenses float64
|
||||
@@ -33,11 +48,25 @@ type mockPatrimony struct{}
|
||||
|
||||
func (m *mockPatrimony) TotalPatrimony(_ context.Context) (float64, error) { return 0, nil }
|
||||
|
||||
type mockPendingBillRepo struct{}
|
||||
|
||||
func (m *mockPendingBillRepo) List(_ context.Context) ([]model.PendingBillImport, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockPendingBillRepo) Create(_ context.Context, _, _ string, _ float64, _ []model.ImportRow) (*model.PendingBillImport, error) {
|
||||
return &model.PendingBillImport{}, nil
|
||||
}
|
||||
func (m *mockPendingBillRepo) GetByID(_ context.Context, _ int) (*model.PendingBillImport, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockPendingBillRepo) Delete(_ context.Context, _ int) error { return nil }
|
||||
|
||||
func newDashboardSvc(income, expenses float64) *service.DashboardService {
|
||||
repo := &mockAccountRepo{}
|
||||
recurrSvc := service.NewRecurringService(newMockRecurring(nil), &mockTxRepo{})
|
||||
billSvc := service.NewCreditBillService(&mockCreditBillRepo{}, repo)
|
||||
return service.NewDashboardService(&mockDashboardRepo{income: income, expenses: expenses}, recurrSvc, &mockPatrimony{}, billSvc)
|
||||
pendingBillSvc := service.NewPendingBillService(&mockPendingBillRepo{}, &mockImportTxRepo{})
|
||||
return service.NewDashboardService(&mockDashboardRepo{income: income, expenses: expenses}, recurrSvc, &mockPatrimony{}, billSvc, pendingBillSvc)
|
||||
}
|
||||
|
||||
func TestDashboard_SavingsPct_40(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"financeiro-carvalho/internal/model"
|
||||
"financeiro-carvalho/internal/repository"
|
||||
)
|
||||
|
||||
var ErrPendingBillNotFound = errors.New("pending bill not found")
|
||||
|
||||
type PendingBillService struct {
|
||||
repo repository.PendingBillRepository
|
||||
txRepo repository.TransactionRepository
|
||||
}
|
||||
|
||||
func NewPendingBillService(repo repository.PendingBillRepository, txRepo repository.TransactionRepository) *PendingBillService {
|
||||
return &PendingBillService{repo: repo, txRepo: txRepo}
|
||||
}
|
||||
|
||||
func (s *PendingBillService) List(ctx context.Context) ([]model.PendingBillImport, error) {
|
||||
items, err := s.repo.List(ctx)
|
||||
if items == nil {
|
||||
return []model.PendingBillImport{}, err
|
||||
}
|
||||
return items, err
|
||||
}
|
||||
|
||||
// Save stores a credit-card import as pending when payment_date is in the future.
|
||||
// Returns (true, pendingBill, nil) if saved as pending; (false, nil, nil) if not applicable.
|
||||
func (s *PendingBillService) MaybeSaveAsPending(ctx context.Context, filename, paymentDate string, rows []model.ImportRow) (bool, *model.PendingBillImport, error) {
|
||||
if paymentDate == "" {
|
||||
return false, nil, nil
|
||||
}
|
||||
t, err := time.Parse("2006-01-02", paymentDate)
|
||||
if err != nil || !t.After(time.Now().Truncate(24*time.Hour)) {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
var total float64
|
||||
newRows := make([]model.ImportRow, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
if !r.IsDuplicate {
|
||||
total += r.Amount
|
||||
newRows = append(newRows, r)
|
||||
}
|
||||
}
|
||||
|
||||
p, err := s.repo.Create(ctx, filename, paymentDate, total, newRows)
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
return true, p, nil
|
||||
}
|
||||
|
||||
// Confirm inserts the pending bill's rows as actual transactions and removes the pending record.
|
||||
func (s *PendingBillService) Confirm(ctx context.Context, id int) error {
|
||||
p, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return ErrPendingBillNotFound
|
||||
}
|
||||
if _, err := s.txRepo.BulkInsert(ctx, p.Rows); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// Discard removes a pending bill import without creating transactions.
|
||||
func (s *PendingBillService) Discard(ctx context.Context, id int) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
Reference in New Issue
Block a user