Added structured logging across services and repositories. Updated SQL queries to use parameterized placeholders for better readability and security. Enhanced error handling for external service communication.

This commit is contained in:
2026-05-15 22:07:03 +03:00
parent c3f90b57c2
commit 8462b16305
11 changed files with 296 additions and 63 deletions
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"FamilyHub/src/domain"
"errors"
"io"
"log"
"net/http"
"strconv"
"strings"
@@ -73,13 +74,13 @@ func (router *TransactionsRouter) Create(c *gin.Context) {
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
log.Printf("%+v\n", input)
transaction, err := router.creationService.Create(c.Request.Context(), input)
if err != nil {
handleTransactionError(c, err)
return
}
log.Printf("%+v\n", transaction)
c.JSON(http.StatusCreated, dto.TransactionToResponse(transaction))
}
+12 -1
View File
@@ -4,8 +4,10 @@ import (
"FamilyHub/src/domain"
"FamilyHub/src/integrations/receiptProvider"
"FamilyHub/src/repositories"
"FamilyHub/src/utils"
"context"
"fmt"
"log"
"strings"
)
@@ -35,18 +37,23 @@ func (s *receiptService) AddReceipt(
ctx context.Context,
req domain.AddReceiptRequest,
) (*domain.Receipt, error) {
log.Printf("receipt add request: payload=%s", utils.ToLogJSON(req))
receipt, err := s.provider.GetReceipt(ctx, req.Date, req.Number)
if err != nil {
log.Printf("receipt add failed: err=%v payload=%s", err, utils.ToLogJSON(req))
return nil, err
}
receiptID, err := s.repo.Create(ctx, receipt)
if err != nil {
log.Printf("receipt persist failed: err=%v receipt=%s", err, utils.ToLogJSON(receipt))
return nil, err
}
receipt.ID = int(receiptID)
if !s.shouldCreateTransaction(req) {
log.Printf("receipt add response: payload=%s", utils.ToLogJSON(receipt))
return receipt, nil
}
@@ -59,6 +66,7 @@ func (s *receiptService) AddReceipt(
}
receipt.TransactionID = &transaction.ID
log.Printf("receipt add response: payload=%s", utils.ToLogJSON(receipt))
return receipt, nil
}
@@ -94,11 +102,14 @@ func (s *receiptService) createTransactionForReceipt(
CreatedBy: *req.CreatedBy,
ReceiptID: &receiptID,
}
log.Printf("%+v\n", transaction)
log.Printf("receipt transaction create request: payload=%s", utils.ToLogJSON(transaction))
if err := s.transactionRepo.Create(ctx, transaction); err != nil {
log.Printf("receipt transaction create failed: err=%v payload=%s", err, utils.ToLogJSON(transaction))
return nil, err
}
log.Printf("receipt transaction create response: payload=%s", utils.ToLogJSON(transaction))
return transaction, nil
}
+8
View File
@@ -3,10 +3,12 @@ package services
import (
"FamilyHub/src/domain"
"FamilyHub/src/repositories"
"FamilyHub/src/utils"
"context"
"database/sql"
"errors"
"fmt"
"log"
"strings"
)
@@ -38,7 +40,10 @@ var (
)
func (s *transactionService) Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error) {
log.Printf("transaction create request: payload=%s", utils.ToLogJSON(req))
if strings.TrimSpace(req.Type) == "" || strings.TrimSpace(req.Category) == "" {
log.Printf("transaction create failed: err=%v payload=%s", ErrInvalidTransaction, utils.ToLogJSON(req))
return nil, ErrInvalidTransaction
}
@@ -55,8 +60,10 @@ func (s *transactionService) Create(ctx context.Context, req domain.CreateTransa
if err := s.repo.Create(ctx, transaction); err != nil {
if errors.Is(err, repositories.ErrReceiptNotFound) {
log.Printf("transaction create failed: err=%v payload=%s", ErrReceiptNotFound, utils.ToLogJSON(req))
return nil, ErrReceiptNotFound
}
log.Printf("transaction create failed: err=%v payload=%s", err, utils.ToLogJSON(req))
return nil, err
}
@@ -72,6 +79,7 @@ func (s *transactionService) Create(ctx context.Context, req domain.CreateTransa
})
}
log.Printf("transaction create response: payload=%s", utils.ToLogJSON(transaction))
return transaction, nil
}
+51 -18
View File
@@ -3,11 +3,14 @@ package familyHub
import (
"FamilyHub/src/config"
"FamilyHub/src/domain"
"FamilyHub/src/utils"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"time"
@@ -63,14 +66,13 @@ func (c *HTTPClient) SendReceipt(ctx context.Context, payload domain.AddReceiptR
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
responseBody, statusCode, err := c.doRequest(req, "familyhub_api.transactions.create", body)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("api error: status %d", resp.StatusCode)
if statusCode >= 300 {
return fmt.Errorf("api error: status %d body %s", statusCode, utils.TruncateForLog(string(responseBody), 512))
}
return nil
@@ -120,14 +122,13 @@ func (c *HTTPClient) RegisterUser(ctx context.Context, payload domain.CreateUser
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
responseBody, statusCode, err := c.doRequest(req, "familyhub_api.users.create", body)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("api error: status %d", resp.StatusCode)
if statusCode >= 300 {
return fmt.Errorf("api error: status %d body %s", statusCode, utils.TruncateForLog(string(responseBody), 512))
}
return nil
@@ -144,22 +145,21 @@ func (c *HTTPClient) GetUserByTelegramID(ctx context.Context, telegramID int64)
return nil, err
}
resp, err := c.client.Do(req)
responseBody, statusCode, err := c.doRequest(req, "familyhub_api.users.by_telegram", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
if statusCode == http.StatusNotFound {
return nil, errUserNotFound
}
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("api error: status %d", resp.StatusCode)
if statusCode >= 300 {
return nil, fmt.Errorf("api error: status %d body %s", statusCode, utils.TruncateForLog(string(responseBody), 512))
}
var user domain.UserResponse
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
if err := json.Unmarshal(responseBody, &user); err != nil {
return nil, err
}
@@ -184,15 +184,48 @@ func (c *HTTPClient) CreateFamily(ctx context.Context, payload domain.CreateFami
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
responseBody, statusCode, err := c.doRequest(req, "familyhub_api.families.create", body)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("api error: status %d", resp.StatusCode)
if statusCode >= 300 {
return fmt.Errorf("api error: status %d body %s", statusCode, utils.TruncateForLog(string(responseBody), 512))
}
return nil
}
func (c *HTTPClient) doRequest(req *http.Request, service string, requestBody []byte) ([]byte, int, error) {
log.Printf(
"external request: service=%s method=%s url=%s body=%q",
service,
req.Method,
req.URL.String(),
utils.TruncateForLog(string(requestBody), utils.DefaultLogValueLimit),
)
resp, err := c.client.Do(req)
if err != nil {
log.Printf("external response: service=%s method=%s url=%s err=%v", service, req.Method, req.URL.String(), err)
return nil, 0, err
}
defer resp.Body.Close()
responseBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
log.Printf("external response: service=%s method=%s url=%s status=%d read_err=%v", service, req.Method, req.URL.String(), resp.StatusCode, readErr)
return nil, resp.StatusCode, readErr
}
log.Printf(
"external response: service=%s method=%s url=%s status=%d body=%q",
service,
req.Method,
req.URL.String(),
resp.StatusCode,
utils.TruncateForLog(string(responseBody), utils.DefaultLogValueLimit),
)
return responseBody, resp.StatusCode, nil
}
@@ -2,9 +2,14 @@ package familyHub
import (
"FamilyHub/src/config"
"FamilyHub/src/utils"
"context"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
@@ -18,19 +23,42 @@ func NewBotClient(config config.Config) (*HTTPClient, error) {
}
func (c *HTTPClient) SendMessage(ctx context.Context, chatId int64, message string) error {
url := c.config.TelegramApi + "/bot" + c.config.BotToken + "/sendMessage?chat_id=" + strconv.FormatInt(chatId, 10) + "&text=" + message
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
c.config.TelegramApi+"/bot"+c.config.BotToken+"/sendMessage?chat_id="+strconv.FormatInt(chatId, 10)+"&text="+message,
url,
nil,
)
if err != nil {
return err
}
logURL := strings.ReplaceAll(req.URL.String(), c.config.BotToken, "***")
log.Printf(
"external request: service=telegram_bot.send_message method=%s url=%s body=%q",
http.MethodGet,
logURL,
utils.TruncateForLog(fmt.Sprintf("chat_id=%d&text=%s", chatId, message), utils.DefaultLogValueLimit),
)
resp, err := c.client.Do(req)
if err != nil {
log.Printf("external response: service=telegram_bot.send_message method=%s url=%s err=%v", http.MethodGet, logURL, err)
return err
}
defer resp.Body.Close()
responseBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
log.Printf("external response: service=telegram_bot.send_message method=%s url=%s status=%d read_err=%v", http.MethodGet, logURL, resp.StatusCode, readErr)
return readErr
}
log.Printf(
"external response: service=telegram_bot.send_message method=%s url=%s status=%d body=%q",
http.MethodGet,
logURL,
resp.StatusCode,
utils.TruncateForLog(string(responseBody), utils.DefaultLogValueLimit),
)
return nil
}
+12
View File
@@ -1,9 +1,11 @@
package ocr
import (
"FamilyHub/src/utils"
"bytes"
"context"
"fmt"
"log"
vision "cloud.google.com/go/vision/apiv1"
)
@@ -30,19 +32,29 @@ func (g *GoogleOCR) Close() error {
}
func (g *GoogleOCR) Recognize(ctx context.Context, image []byte) (string, error) {
log.Printf("external request: service=google_ocr.detect_text image_size_bytes=%d", len(image))
img, err := vision.NewImageFromReader(bytes.NewReader(image))
if err != nil {
log.Printf("external response: service=google_ocr.detect_text err=%v", err)
return "", fmt.Errorf("load image: %w", err)
}
annotations, err := g.client.DetectTexts(ctx, img, nil, 1)
if err != nil {
log.Printf("external response: service=google_ocr.detect_text err=%v", err)
return "", fmt.Errorf("detect text: %w", err)
}
if len(annotations) == 0 {
log.Printf("external response: service=google_ocr.detect_text result=%q", "")
return "", nil
}
log.Printf(
"external response: service=google_ocr.detect_text result=%q annotations=%d",
utils.TruncateForLog(annotations[0].Description, utils.DefaultLogValueLimit),
len(annotations),
)
return annotations[0].Description, nil
}
@@ -64,6 +64,14 @@ func (s *receiptProvider) GetReceipt(
var receipt domain.Receipt
body, contentType := buildMultipartBody(date, number)
requestBody := body.String()
log.Printf(
"external request: service=receipt_provider method=%s url=%s content_type=%s body=%q",
http.MethodPost,
url,
contentType,
utils.TruncateForLog(requestBody, utils.DefaultLogValueLimit),
)
httpReq, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
@@ -79,18 +87,27 @@ func (s *receiptProvider) GetReceipt(
resp, err := s.client.Do(httpReq)
if err != nil {
log.Println(err.Error())
log.Printf("external response: service=receipt_provider method=%s url=%s err=%v", http.MethodPost, url, err)
return nil, err
}
defer resp.Body.Close()
responseBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
log.Printf("failed to read external service response body: %v", readErr)
return nil, readErr
}
bodyText := strings.TrimSpace(string(responseBody))
log.Printf(
"external response: service=receipt_provider method=%s url=%s status=%d body=%q",
http.MethodPost,
url,
resp.StatusCode,
utils.TruncateForLog(bodyText, utils.DefaultLogValueLimit),
)
if resp.StatusCode != http.StatusOK {
responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 4096))
if readErr != nil {
log.Printf("failed to read external service error body: %v", readErr)
}
bodyText := strings.TrimSpace(string(responseBody))
log.Printf("external service returned %d body=%q", resp.StatusCode, bodyText)
return nil, &ExternalServiceError{
StatusCode: resp.StatusCode,
Body: bodyText,
@@ -100,8 +117,8 @@ func (s *receiptProvider) GetReceipt(
var raw struct {
Message map[string]interface{} `json:"message"`
}
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
log.Printf("external service returned %s\n", err.Error())
if err := json.Unmarshal(responseBody, &raw); err != nil {
log.Printf("external service returned invalid json: %v", err)
return nil, err
}
@@ -112,6 +129,7 @@ func (s *receiptProvider) GetReceipt(
}
if receipt.IssuedAtRaw == "" {
log.Printf("external response parse failed: service=receipt_provider err=%v date=%s number=%s", ErrReceiptNotFound, date, number)
return nil, ErrReceiptNotFound
}
+91 -32
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"log"
"FamilyHub/src/domain"
)
@@ -25,29 +26,63 @@ func NewReceiptsSQLRepository(db *sql.DB) *ReceiptsSQLRepository {
}
func (r *ReceiptsSQLRepository) Create(ctx context.Context, receipt *domain.Receipt) (int64, error) {
log.Printf("%+v\n", receipt)
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return 0, err
}
defer tx.Rollback()
if receipt.ReceiptNumber != receipt.UI {
receipt.ReceiptNumber = receipt.UI
}
res, err := tx.ExecContext(ctx, `
log.Println("First query")
query := `
INSERT INTO receipts (
transaction_id, receipt_number, ui, status, issued_at,
total_amount, payment_amount, cash_amount,
another_amount, clearing_amount, margin,
currency, payment_type,
cashbox_number, cashier,
name_spd, name_to, name_np, type_np,
street_to, house_to,
kod_soato, oblast_soato, rayon_soato, selsovet_soato,
doc_num, skno_number, unp,
transaction_id,
receipt_number,
ui,
status,
issued_at,
total_amount,
payment_amount,
cash_amount,
another_amount,
clearing_amount,
margin,
currency,
payment_type,
cashbox_number,
cashier,
name_spd,
name_to,
name_np,
type_np,
street_to,
house_to,
kod_soato,
oblast_soato,
rayon_soato,
selsovet_soato,
doc_num,
skno_number,
unp,
success
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9, $10,
$11, $12, $13, $14, $15,
$16, $17, $18, $19, $20,
$21, $22, $23, $24, $25,
$26, $27, $28, $29
)
RETURNING id;
`
args := []any{
receipt.TransactionID,
receipt.ReceiptNumber,
receipt.UI,
@@ -85,16 +120,19 @@ func (r *ReceiptsSQLRepository) Create(ctx context.Context, receipt *domain.Rece
receipt.UNP,
receipt.Success,
)
}
log.Printf("SQL: %s", query)
log.Printf("ARGS: %+v", args)
var receiptID int64
err = tx.QueryRowContext(ctx, query, args...).Scan(&receiptID)
if err != nil {
return 0, err
}
receiptID, err := res.LastInsertId()
if err != nil {
return 0, err
}
log.Println("Second query")
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO positions (
@@ -109,7 +147,11 @@ func (r *ReceiptsSQLRepository) Create(ctx context.Context, receipt *domain.Rece
tag,
marking_code,
ukz_code
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9, $10, $11
)
`)
if err != nil {
return 0, err
@@ -117,7 +159,8 @@ func (r *ReceiptsSQLRepository) Create(ctx context.Context, receipt *domain.Rece
defer stmt.Close()
for _, p := range receipt.Positions {
_, err = stmt.ExecContext(ctx,
_, err = stmt.ExecContext(
ctx,
receiptID,
p.SectionNumber,
p.GTINCode,
@@ -135,7 +178,11 @@ func (r *ReceiptsSQLRepository) Create(ctx context.Context, receipt *domain.Rece
}
}
return receiptID, tx.Commit()
if err = tx.Commit(); err != nil {
return 0, err
}
return receiptID, nil
}
func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.Receipt, error) {
@@ -157,7 +204,7 @@ func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.
doc_num, skno_number, unp,
success
FROM receipts
WHERE id = ?
WHERE id = $1
`, id).Scan(
&receipt.ID,
&receipt.TransactionID,
@@ -213,7 +260,7 @@ func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.
product_count, amount,
discount, surcharge,
tag, marking_code, ukz_code
FROM positions WHERE receipt_id = ?
FROM positions WHERE receipt_id = $1
`, id)
if err != nil {
return nil, err
@@ -247,10 +294,16 @@ func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.
func (r *ReceiptsSQLRepository) GetAll(ctx context.Context, limit, offset int) ([]*domain.Receipt, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, transaction_id, receipt_number, issued_at, total_amount, currency
SELECT
id,
transaction_id,
receipt_number,
issued_at,
total_amount,
currency
FROM receipts
ORDER BY issued_at DESC
LIMIT ? OFFSET ?
LIMIT $1 OFFSET $2
`, limit, offset)
if err != nil {
return nil, err
@@ -261,6 +314,7 @@ func (r *ReceiptsSQLRepository) GetAll(ctx context.Context, limit, offset int) (
for rows.Next() {
var rct domain.Receipt
if err := rows.Scan(
&rct.ID,
&rct.TransactionID,
@@ -271,9 +325,14 @@ func (r *ReceiptsSQLRepository) GetAll(ctx context.Context, limit, offset int) (
); err != nil {
return nil, err
}
receipts = append(receipts, &rct)
}
if err := rows.Err(); err != nil {
return nil, err
}
return receipts, nil
}
@@ -287,11 +346,11 @@ func (r *ReceiptsSQLRepository) Update(ctx context.Context, receipt *domain.Rece
_, err = tx.ExecContext(ctx, `
UPDATE receipts SET
transaction_id = ?,
issued_at = ?,
total_amount = ?,
currency = ?
WHERE id = ?
transaction_id = $1,
issued_at = $2,
total_amount = $3,
currency = $4
WHERE id = $5
`,
receipt.TransactionID,
receipt.IssuedAt,
@@ -303,7 +362,7 @@ func (r *ReceiptsSQLRepository) Update(ctx context.Context, receipt *domain.Rece
return err
}
_, err = tx.ExecContext(ctx, `DELETE FROM positions WHERE receipt_id = ?`, receipt.ID)
_, err = tx.ExecContext(ctx, `DELETE FROM positions WHERE receipt_id = $1`, receipt.ID)
if err != nil {
return err
}
@@ -312,7 +371,7 @@ func (r *ReceiptsSQLRepository) Update(ctx context.Context, receipt *domain.Rece
_, err = tx.ExecContext(ctx, `
INSERT INTO positions (
receipt_id, product_name, product_count, amount
) VALUES (?, ?, ?, ?)
) VALUES ($1, $2, $3, $4)
`, receipt.ID, p.ProductName, p.ProductCount, p.Amount)
if err != nil {
return err
@@ -324,7 +383,7 @@ func (r *ReceiptsSQLRepository) Update(ctx context.Context, receipt *domain.Rece
func (r *ReceiptsSQLRepository) Delete(ctx context.Context, id int64) error {
_, err := r.db.ExecContext(ctx,
`DELETE FROM receipts WHERE id = ?`,
`DELETE FROM receipts WHERE id = $1`,
id,
)
return err
+29
View File
@@ -0,0 +1,29 @@
package utils
import (
"encoding/json"
"fmt"
)
const DefaultLogValueLimit = 4096
func ToLogJSON(value any) string {
if value == nil {
return "null"
}
data, err := json.Marshal(value)
if err != nil {
return TruncateForLog(fmt.Sprintf("%+v", value), DefaultLogValueLimit)
}
return TruncateForLog(string(data), DefaultLogValueLimit)
}
func TruncateForLog(value string, limit int) string {
if limit <= 0 || len(value) <= limit {
return value
}
return value[:limit] + "...(truncated)"
}