Update
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// Resolve organization and outlet names
|
||||
org, err := s.organizationRepo.GetByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("organization not found: %w", err)
|
||||
}
|
||||
outlet, err := s.outletRepo.GetByID(ctx, outID)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Determine timezone (fallback to system local if not available)
|
||||
tzName := "Asia/Jakarta"
|
||||
if outlet.Timezone != nil && *outlet.Timezone != "" {
|
||||
tzName = *outlet.Timezone
|
||||
}
|
||||
loc, locErr := time.LoadLocation(tzName)
|
||||
if locErr != nil || loc == nil {
|
||||
loc = time.Local
|
||||
}
|
||||
|
||||
// Compute day range in the chosen location
|
||||
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)
|
||||
|
||||
// Build requests
|
||||
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"}
|
||||
|
||||
// Call services
|
||||
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)
|
||||
}
|
||||
|
||||
// Compose template data
|
||||
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 by product
|
||||
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
|
||||
|
||||
// Render to PDF
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user