Updated transaction routers, removed receipts router
This commit is contained in:
@@ -25,7 +25,28 @@ func NewApiClient(config config.Config) (*HTTPClient, error) {
|
||||
}
|
||||
|
||||
func (c *HTTPClient) SendReceipt(ctx context.Context, payload domain.AddReceiptRequest) error {
|
||||
body, err := json.Marshal(payload)
|
||||
requestBody := map[string]any{
|
||||
"receipt_number": payload.Number,
|
||||
"receipt_date": payload.Date,
|
||||
}
|
||||
|
||||
if payload.FamilyID != nil {
|
||||
requestBody["family_id"] = *payload.FamilyID
|
||||
}
|
||||
if payload.CreatedBy != nil {
|
||||
requestBody["created_by"] = *payload.CreatedBy
|
||||
}
|
||||
if payload.Type != nil {
|
||||
requestBody["type"] = *payload.Type
|
||||
}
|
||||
if payload.Category != nil {
|
||||
requestBody["category"] = *payload.Category
|
||||
}
|
||||
if payload.Description != nil {
|
||||
requestBody["description"] = *payload.Description
|
||||
}
|
||||
|
||||
body, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -33,7 +54,7 @@ func (c *HTTPClient) SendReceipt(ctx context.Context, payload domain.AddReceiptR
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
c.config.APIHost+c.config.APIPort+"/receipts",
|
||||
c.config.APIHost+c.config.APIPort+"/api/v1/transactions",
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,6 +21,39 @@ func testConfig(baseURL string) config.Config {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClient_SendReceipt_UsesTransactionsEndpoint(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/v1/transactions" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("failed to decode body: %v", err)
|
||||
}
|
||||
|
||||
if payload["receipt_number"] != "123" || payload["receipt_date"] != "21.01.2026" {
|
||||
t.Fatalf("unexpected payload: %+v", payload)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := NewApiClient(testConfig(ts.URL))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
err = client.SendReceipt(context.Background(), domain.AddReceiptRequest{
|
||||
Number: "123",
|
||||
Date: "21.01.2026",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendReceipt returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClient_EnsureUser_AlreadyExists(t *testing.T) {
|
||||
var postCalls int32
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package receiptProvider
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"FamilyHub/src/utils"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ReceiptProvider interface {
|
||||
GetReceipt(ctx context.Context, date, number string) (*domain.Receipt, error)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrReceiptNotFound = errors.New("receipt not found")
|
||||
ErrExternalService = errors.New("external receipt service failure")
|
||||
)
|
||||
|
||||
type ExternalServiceError struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
func (e *ExternalServiceError) Error() string {
|
||||
return fmt.Sprintf("%s: status %d", ErrExternalService, e.StatusCode)
|
||||
}
|
||||
|
||||
func (e *ExternalServiceError) Unwrap() error {
|
||||
return ErrExternalService
|
||||
}
|
||||
|
||||
type receiptProvider struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewReceiptProvider() *receiptProvider {
|
||||
return &receiptProvider{
|
||||
client: &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *receiptProvider) GetReceipt(
|
||||
ctx context.Context,
|
||||
date, number string,
|
||||
) (*domain.Receipt, error) {
|
||||
url := "https://ch.info-center.by/ajax/check1.php"
|
||||
var receipt domain.Receipt
|
||||
|
||||
body, contentType := buildMultipartBody(date, 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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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, ErrReceiptNotFound
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user