apskel-pos-backend/internal/service/report_service.go

193 lines
6.2 KiB
Go
Raw Normal View History

2025-08-10 20:41:34 +07:00
package service
import (
"apskel-pos-be/internal/repository"
"context"
"fmt"
"path/filepath"
"strings"
"time"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/processor"
"github.com/google/uuid"
)
type ReportService interface {
// Returns (publicURL, fileName, error)
GenerateDailyTransactionPDF(ctx context.Context, organizationID string, outletID string, reportDate *time.Time, generatedBy string) (string, string, error)
}
type ReportServiceImpl struct {
analyticsService AnalyticsService
organizationRepo *repository.OrganizationRepositoryImpl
outletRepo *repository.OutletRepositoryImpl
fileClient processor.FileClient
}
func NewReportService(analyticsService *AnalyticsServiceImpl, organizationRepo *repository.OrganizationRepositoryImpl, outletRepo *repository.OutletRepositoryImpl, fileClient processor.FileClient) *ReportServiceImpl {
return &ReportServiceImpl{
analyticsService: analyticsService,
organizationRepo: organizationRepo,
outletRepo: outletRepo,
fileClient: fileClient,
}
}
// reportTemplateData holds the data passed to the HTML template
type reportTemplateData struct {
OrganizationName string
OutletName string
ReportDate string
StartDate string
EndDate string
GeneratedBy string
PrintTime string
Summary reportSummary
Items []reportItem
}
type reportSummary struct {
TotalTransactions int64
TotalItems int64
GrossSales string
Discount string
Tax string
NetSales string
COGS string
GrossProfit string
GrossMarginPercent string
}
type reportItem struct {
Name string
Quantity int64
GrossSales string
Discount string
NetSales string
COGS string
GrossProfit string
}
func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, organizationID string, outletID string, reportDate *time.Time, generatedBy string) (string, string, error) {
// Parse IDs
orgID, err := uuid.Parse(organizationID)
if err != nil {
return "", "", fmt.Errorf("invalid organization id: %w", err)
}
outID, err := uuid.Parse(outletID)
if err != nil {
return "", "", fmt.Errorf("invalid outlet id: %w", err)
}
org, err := s.organizationRepo.GetByID(ctx, orgID)
if err != nil {
return "", "", fmt.Errorf("organization not found: %w", err)
}
2025-08-10 21:46:44 +07:00
2025-08-10 20:41:34 +07:00
outlet, err := s.outletRepo.GetByID(ctx, outID)
if err != nil {
return "", "", fmt.Errorf("outlet not found: %w", err)
}
tzName := "Asia/Jakarta"
if outlet.Timezone != nil && *outlet.Timezone != "" {
tzName = *outlet.Timezone
}
2025-08-10 21:46:44 +07:00
2025-08-10 20:41:34 +07:00
loc, locErr := time.LoadLocation(tzName)
if locErr != nil || loc == nil {
loc = time.Local
}
var day time.Time
if reportDate != nil {
t := reportDate.UTC()
day = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
} else {
now := time.Now().In(loc)
day = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
}
start := day
end := day.Add(24*time.Hour - time.Nanosecond)
salesReq := &models.SalesAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
plReq := &models.ProfitLossAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
sales, err := s.analyticsService.GetSalesAnalytics(ctx, salesReq)
if err != nil {
return "", "", fmt.Errorf("get sales analytics: %w", err)
}
pl, err := s.analyticsService.GetProfitLossAnalytics(ctx, plReq)
if err != nil {
return "", "", fmt.Errorf("get profit/loss analytics: %w", err)
}
data := reportTemplateData{
OrganizationName: org.Name,
OutletName: outlet.Name,
ReportDate: day.Format("02/01/2006"),
StartDate: start.Format("02/01/2006 15:04"),
EndDate: end.Format("02/01/2006 15:04"),
GeneratedBy: generatedBy,
PrintTime: time.Now().Format("02/01/2006 15:04:05"),
Summary: reportSummary{
TotalTransactions: pl.Summary.TotalOrders,
TotalItems: sales.Summary.TotalItems,
GrossSales: formatCurrency(pl.Summary.TotalRevenue),
Discount: formatCurrency(pl.Summary.TotalDiscount),
Tax: formatCurrency(pl.Summary.TotalTax),
NetSales: formatCurrency(sales.Summary.NetSales),
COGS: formatCurrency(pl.Summary.TotalCost),
GrossProfit: formatCurrency(pl.Summary.GrossProfit),
GrossMarginPercent: fmt.Sprintf("%.2f", pl.Summary.GrossProfitMargin),
},
}
items := make([]reportItem, 0, len(pl.ProductData))
for _, p := range pl.ProductData {
items = append(items, reportItem{
Name: p.ProductName,
Quantity: p.QuantitySold,
GrossSales: formatCurrency(p.Revenue),
Discount: formatCurrency(0),
NetSales: formatCurrency(p.Revenue),
COGS: formatCurrency(p.Cost),
GrossProfit: formatCurrency(p.GrossProfit),
})
}
data.Items = items
templatePath := filepath.Join("templates", "daily_transaction.html")
pdfBytes, err := renderTemplateToPDF(templatePath, data)
if err != nil {
return "", "", fmt.Errorf("render pdf: %w", err)
}
// Upload to bucket
safeOutlet := outID.String()
safeOrg := orgID.String()
// Clean outlet name for filename (remove spaces and special characters)
cleanOutletName := strings.ReplaceAll(outlet.Name, " ", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, "/", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, "\\", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, ":", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, "*", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, "?", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, "\"", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, "<", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, ">", "-")
cleanOutletName = strings.ReplaceAll(cleanOutletName, "|", "-")
fileName := fmt.Sprintf("laporan-transaksi-harian-%s-%s-%s.pdf", cleanOutletName, day.Format("2006-01-02"), time.Now().Format("20060102-150405"))
objectKey := fmt.Sprintf("/reports/%s/%s/%s", safeOrg, safeOutlet, fileName)
publicURL, err := s.fileClient.UploadFile(ctx, objectKey, pdfBytes)
if err != nil {
return "", "", fmt.Errorf("upload pdf: %w", err)
}
return publicURL, fileName, nil
}