193 lines
6.2 KiB
Go
193 lines
6.2 KiB
Go
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|