diff --git a/111 b/111 new file mode 100644 index 0000000..fca3d07 --- /dev/null +++ b/111 @@ -0,0 +1,2 @@ +Portainer +admin - 4c#;=H36$s^J \ No newline at end of file diff --git a/backend/src/api/routers/transactions.go b/backend/src/api/routers/transactions.go index 9d3025e..47e8b41 100644 --- a/backend/src/api/routers/transactions.go +++ b/backend/src/api/routers/transactions.go @@ -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)) } diff --git a/backend/src/api/services/receipts.go b/backend/src/api/services/receipts.go index 462763a..7128591 100644 --- a/backend/src/api/services/receipts.go +++ b/backend/src/api/services/receipts.go @@ -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 } diff --git a/backend/src/api/services/transactions.go b/backend/src/api/services/transactions.go index cb99c8f..d3c00bd 100644 --- a/backend/src/api/services/transactions.go +++ b/backend/src/api/services/transactions.go @@ -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 } diff --git a/backend/src/integrations/familyHub/apiClient.go b/backend/src/integrations/familyHub/apiClient.go index 71a250e..164d531 100644 --- a/backend/src/integrations/familyHub/apiClient.go +++ b/backend/src/integrations/familyHub/apiClient.go @@ -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 +} diff --git a/backend/src/integrations/familyHub/botClient.go b/backend/src/integrations/familyHub/botClient.go index 335459b..30360a3 100644 --- a/backend/src/integrations/familyHub/botClient.go +++ b/backend/src/integrations/familyHub/botClient.go @@ -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 } diff --git a/backend/src/integrations/ocr/google.go b/backend/src/integrations/ocr/google.go index dda7888..3b034d1 100644 --- a/backend/src/integrations/ocr/google.go +++ b/backend/src/integrations/ocr/google.go @@ -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 } diff --git a/backend/src/integrations/receiptProvider/receipt_provider.go b/backend/src/integrations/receiptProvider/receipt_provider.go index 61215b9..9c00be7 100644 --- a/backend/src/integrations/receiptProvider/receipt_provider.go +++ b/backend/src/integrations/receiptProvider/receipt_provider.go @@ -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 } diff --git a/backend/src/repositories/receipts.go b/backend/src/repositories/receipts.go index 34f1094..ae52a1b 100644 --- a/backend/src/repositories/receipts.go +++ b/backend/src/repositories/receipts.go @@ -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 diff --git a/backend/src/utils/logging.go b/backend/src/utils/logging.go new file mode 100644 index 0000000..cbe4961 --- /dev/null +++ b/backend/src/utils/logging.go @@ -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)" +} diff --git a/resp.json b/resp.json new file mode 100644 index 0000000..1c1b79a --- /dev/null +++ b/resp.json @@ -0,0 +1,32 @@ +{ + "id": 0, + "transaction_id": null, + "STATUS": 1, + "another_amount": 0, + "cash_amount": 0, + "cashbox_number": 119091676, + "cashier": "Старший кассир КСО", + "clearing_amount": 190.06, + "currency": "BYN", + "doc_num": "39972", + "house_to": "11", + "kod_soato": "5000000000", + "margin": 0, + "name_np": "Минск", + "name_spd": "Общество с ограниченной ответственностью \"ГРИНрозница\"", + "name_to": "Магазин \"ГРИН-5\"", + "oblast_soato": null, + "rayon_soato": null, + "selsovet_soato": null, + "payment_amount": 190.06, + "payment_type": 1, + "receipt_number": "CBB580D6268681CB071931DC", + "skno_number": "AVQ24170126963", + "street_to": "УЛ. ПЕТРА МСТИСЛАВЦА", + "success": "A check is correct", + "total_amount": 190.06, + "type_np": "г.", + "ui": "CBB580D6268681CB071931DC", + "unp": "191634233", + "issued_at": "09/11/2025, 18:08:31" +} \ No newline at end of file