package receiptService import ( "FamilyHub/src/domain" "FamilyHub/src/repositories" "FamilyHub/src/utils" "bytes" "context" "crypto/tls" "encoding/json" "errors" "fmt" "log" "mime/multipart" "net/http" "strings" "time" ) type ReceiptService struct { client *http.Client repo repositories.ReceiptsRepository transactionRepo repositories.TransactionRepository } func NewReceiptService( repo repositories.ReceiptsRepository, transactionRepo repositories.TransactionRepository, ) *ReceiptService { return &ReceiptService{ client: &http.Client{ Timeout: 60 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, }, repo: repo, transactionRepo: transactionRepo, } } func (s *ReceiptService) GetReceipt( ctx context.Context, req domain.AddReceiptRequest, ) (*domain.Receipt, error) { url := "https://ch.info-center.by/ajax/check1.php" var receipt domain.Receipt body, contentType := buildMultipartBody(req.Date, req.Number) httpReq, err := http.NewRequestWithContext( ctx, http.MethodPost, url, body, ) if err != nil { log.Println(err.Error()) return nil, err } httpReq.Header.Set("Content-Type", contentType) resp, err := s.client.Do(httpReq) if err != nil { log.Println(err.Error()) return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("external service returned %d\n", resp.StatusCode) return nil, fmt.Errorf("external service returned %d", resp.StatusCode) } 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()) return nil, err } bytes_, _ := json.Marshal(raw.Message) if err := json.Unmarshal(bytes_, &receipt); err != nil { return nil, err } if receipt.IssuedAtRaw == "" { return nil, errors.New("receipt not found") } positions, err := parsePositions(receipt.PositionsRaw) if err != nil { log.Printf("failed to parse positions: %s", err.Error()) return nil, err } receipt.IssuedAt, err = utils.ParseIssuedAt(receipt.IssuedAtRaw) if err != nil { log.Printf("failed to parse issued at: %s", err.Error()) return nil, err } receipt.Positions = positions for i := range receipt.Positions { p := &receipt.Positions[i] p.ProductCount, err = utils.ParseFloat(p.ProductCountRaw) if err != nil { log.Printf("failed to parse product count: %s", err.Error()) return nil, err } p.Amount, err = utils.ParseFloat(p.AmountRaw) if err != nil { log.Printf("failed to parse amount: %s", err.Error()) return nil, err } p.Discount, _ = utils.ParseFloat(p.DiscountRaw) p.Surcharge, _ = utils.ParseFloat(p.SurchargeRaw) } receiptID, err := s.repo.Create(ctx, &receipt) if err != nil { return nil, err } receipt.ID = int(receiptID) if s.shouldCreateTransaction(req) { transaction, err := s.createTransactionForReceipt(ctx, &receipt, req, receiptID) if err != nil { if rollbackErr := s.repo.Delete(ctx, receiptID); rollbackErr != nil { log.Printf("failed to rollback receipt %d after transaction error: %v", receiptID, rollbackErr) } return nil, err } receipt.TransactionID = &transaction.ID } return &receipt, nil } func buildMultipartBody(date, number string) (*bytes.Buffer, string) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) _ = writer.WriteField("orig_date", date) _ = writer.WriteField("orig_ui", number) _ = writer.Close() return body, writer.FormDataContentType() } func parsePositions(raw string) ([]domain.Position, error) { var positions []domain.Position if raw == "" { return positions, nil } if err := json.Unmarshal([]byte(raw), &positions); err != nil { return nil, err } return positions, nil } func (s *ReceiptService) shouldCreateTransaction(req domain.AddReceiptRequest) bool { return s.transactionRepo != nil && req.FamilyID != nil && req.CreatedBy != nil } func (s *ReceiptService) createTransactionForReceipt( ctx context.Context, receipt *domain.Receipt, req domain.AddReceiptRequest, receiptID int64, ) (*domain.Transaction, error) { transactionType := "expense" if req.Type != nil && strings.TrimSpace(*req.Type) != "" { transactionType = strings.TrimSpace(*req.Type) } category := "receipt" if req.Category != nil && strings.TrimSpace(*req.Category) != "" { category = strings.TrimSpace(*req.Category) } description := buildReceiptTransactionDescription(receipt, req.Description) transaction := &domain.Transaction{ FamilyID: *req.FamilyID, Description: description, Type: transactionType, DateTime: receipt.IssuedAt, Category: category, Amount: receipt.TotalAmount, CreatedBy: *req.CreatedBy, ReceiptID: &receiptID, } if err := s.transactionRepo.Create(ctx, transaction); err != nil { return nil, err } return transaction, nil } func buildReceiptTransactionDescription(receipt *domain.Receipt, explicit *string) *string { if explicit != nil && strings.TrimSpace(*explicit) != "" { value := strings.TrimSpace(*explicit) return &value } if name := strings.TrimSpace(receipt.NameSPD); name != "" { return &name } if number := strings.TrimSpace(receipt.ReceiptNumber); number != "" { value := fmt.Sprintf("Receipt %s", number) return &value } return nil }