Update
This commit is contained in:
@@ -74,6 +74,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
services.paymentMethodService,
|
||||
validators.paymentMethodValidator,
|
||||
services.analyticsService,
|
||||
services.reportService,
|
||||
services.tableService,
|
||||
validators.tableValidator,
|
||||
services.unitService,
|
||||
@@ -187,6 +188,7 @@ type processors struct {
|
||||
tableProcessor *processor.TableProcessor
|
||||
unitProcessor *processor.UnitProcessorImpl
|
||||
ingredientProcessor *processor.IngredientProcessorImpl
|
||||
fileClient processor.FileClient
|
||||
}
|
||||
|
||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||
@@ -209,6 +211,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
|
||||
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
|
||||
fileClient: fileClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +230,7 @@ type services struct {
|
||||
fileService service.FileService
|
||||
customerService service.CustomerService
|
||||
analyticsService *service.AnalyticsServiceImpl
|
||||
reportService service.ReportService
|
||||
tableService *service.TableServiceImpl
|
||||
unitService *service.UnitServiceImpl
|
||||
ingredientService *service.IngredientServiceImpl
|
||||
@@ -248,6 +252,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
||||
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
||||
analyticsService := service.NewAnalyticsServiceImpl(processors.analyticsProcessor)
|
||||
reportService := service.NewReportService(analyticsService, repos.organizationRepo, repos.outletRepo, processors.fileClient)
|
||||
tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer())
|
||||
unitService := service.NewUnitService(processors.unitProcessor)
|
||||
ingredientService := service.NewIngredientService(processors.ingredientProcessor)
|
||||
@@ -267,6 +272,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
fileService: fileService,
|
||||
customerService: customerService,
|
||||
analyticsService: analyticsService,
|
||||
reportService: reportService,
|
||||
tableService: tableService,
|
||||
unitService: unitService,
|
||||
ingredientService: ingredientService,
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type CreateUserRequest struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id" validate:"required"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ReportHandler struct {
|
||||
reportService service.ReportService
|
||||
userService UserService
|
||||
}
|
||||
|
||||
func NewReportHandler(reportService service.ReportService, userService UserService) *ReportHandler {
|
||||
return &ReportHandler{reportService: reportService, userService: userService}
|
||||
}
|
||||
|
||||
// GET /api/v1/outlets/:outlet_id/reports/daily-transaction.pdf?date=YYYY-MM-DD
|
||||
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
ci := appcontext.FromGinContext(ctx)
|
||||
|
||||
outletID := c.Param("outlet_id")
|
||||
var dayPtr *time.Time
|
||||
if d := c.Query("date"); d != "" {
|
||||
if t, err := time.Parse("2006-01-02", d); err == nil {
|
||||
dayPtr = &t
|
||||
}
|
||||
}
|
||||
|
||||
// Get user name for "Dicetak Oleh"
|
||||
user, err := h.userService.GetUserByID(ctx, ci.UserID)
|
||||
var genBy string
|
||||
if err != nil {
|
||||
// Fallback to user ID if name cannot be retrieved
|
||||
genBy = ci.UserID.String()
|
||||
} else {
|
||||
genBy = user.Name
|
||||
}
|
||||
|
||||
publicURL, fileName, err := h.reportService.GenerateDailyTransactionPDF(ctx, ci.OrganizationID.String(), outletID, dayPtr, genBy)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "ReportHandler::GetDailyTransactionReportPDF", err.Error())}), "ReportHandler::GetDailyTransactionReportPDF")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]string{
|
||||
"url": publicURL,
|
||||
"file_name": fileName,
|
||||
}), "ReportHandler::GetDailyTransactionReportPDF")
|
||||
}
|
||||
@@ -27,12 +27,16 @@ func NewUserHandler(userService UserService, userValidator UserValidator) *UserH
|
||||
}
|
||||
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c).WithError(err).Error("UserHandler::CreateUser -> request binding failed")
|
||||
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
||||
return
|
||||
}
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
|
||||
validationError, validationErrorCode := h.userValidator.ValidateCreateUserRequest(&req)
|
||||
if validationError != nil {
|
||||
|
||||
@@ -44,7 +44,10 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||||
setKeyInContext(c, appcontext.UserRoleKey, userResponse.Role)
|
||||
setKeyInContext(c, appcontext.OrganizationIDKey, userResponse.OrganizationID.String())
|
||||
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
||||
setKeyInContext(c, appcontext.OutletIDKey, userResponse.OutletID.String())
|
||||
|
||||
if (userResponse.Role != "superadmin") {
|
||||
setKeyInContext(c, appcontext.OutletIDKey, userResponse.OutletID.String())
|
||||
}
|
||||
|
||||
logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireAuth -> User authenticated: %s", userResponse.Email)
|
||||
c.Next()
|
||||
|
||||
@@ -28,6 +28,7 @@ type Router struct {
|
||||
customerHandler *handler.CustomerHandler
|
||||
paymentMethodHandler *handler.PaymentMethodHandler
|
||||
analyticsHandler *handler.AnalyticsHandler
|
||||
reportHandler *handler.ReportHandler
|
||||
tableHandler *handler.TableHandler
|
||||
unitHandler *handler.UnitHandler
|
||||
ingredientHandler *handler.IngredientHandler
|
||||
@@ -62,6 +63,7 @@ func NewRouter(cfg *config.Config,
|
||||
paymentMethodService service.PaymentMethodService,
|
||||
paymentMethodValidator validator.PaymentMethodValidator,
|
||||
analyticsService *service.AnalyticsServiceImpl,
|
||||
reportService service.ReportService,
|
||||
tableService *service.TableServiceImpl,
|
||||
tableValidator *validator.TableValidator,
|
||||
unitService handler.UnitService,
|
||||
@@ -83,6 +85,7 @@ func NewRouter(cfg *config.Config,
|
||||
customerHandler: handler.NewCustomerHandler(customerService, customerValidator),
|
||||
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
|
||||
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
|
||||
reportHandler: handler.NewReportHandler(reportService, userService),
|
||||
tableHandler: handler.NewTableHandler(tableService, tableValidator),
|
||||
unitHandler: handler.NewUnitHandler(unitService),
|
||||
ingredientHandler: handler.NewIngredientHandler(ingredientService),
|
||||
@@ -296,6 +299,8 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
outlets.PUT("/printer-setting/:outlet_id", r.outletSettingHandler.UpdatePrinterSettings)
|
||||
outlets.GET("/:outlet_id/tables/available", r.tableHandler.GetAvailableTables)
|
||||
outlets.GET("/:outlet_id/tables/occupied", r.tableHandler.GetOccupiedTables)
|
||||
// Reports
|
||||
outlets.GET("/:outlet_id/reports/daily-transaction.pdf", r.reportHandler.GetDailyTransactionReportPDF)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func renderTemplateToPDF(templatePath string, data interface{}) ([]byte, error) {
|
||||
// Create template with custom functions
|
||||
funcMap := template.FuncMap{
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
}
|
||||
|
||||
// Parse and execute HTML template
|
||||
tmpl, err := template.New(filepath.Base(templatePath)).Funcs(funcMap).ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse template: %w", err)
|
||||
}
|
||||
var htmlBuf bytes.Buffer
|
||||
if err := tmpl.Execute(&htmlBuf, data); err != nil {
|
||||
return nil, fmt.Errorf("execute template: %w", err)
|
||||
}
|
||||
|
||||
// Write HTML to a temp file
|
||||
tmpDir, err := os.MkdirTemp("", "daily_report_")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tmp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
htmlPath := filepath.Join(tmpDir, "report.html")
|
||||
pdfPath := filepath.Join(tmpDir, "report.pdf")
|
||||
|
||||
if err := os.WriteFile(htmlPath, htmlBuf.Bytes(), 0644); err != nil {
|
||||
return nil, fmt.Errorf("write html: %w", err)
|
||||
}
|
||||
|
||||
// Use Chrome headless for better CSS rendering
|
||||
chromeArgs := []string{
|
||||
"--headless",
|
||||
"--disable-gpu",
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--print-to-pdf=" + pdfPath,
|
||||
"--print-to-pdf-no-header",
|
||||
"--print-to-pdf-no-footer",
|
||||
"--print-to-pdf-margin-top=0",
|
||||
"--print-to-pdf-margin-bottom=0",
|
||||
"--print-to-pdf-margin-left=0",
|
||||
"--print-to-pdf-margin-right=0",
|
||||
"--print-to-pdf-paper-width=210mm",
|
||||
"--print-to-pdf-paper-height=297mm",
|
||||
htmlPath,
|
||||
}
|
||||
|
||||
// Try Google Chrome first, then Chromium, then wkhtmltopdf as fallback
|
||||
chromeCmd := exec.Command("google-chrome", chromeArgs...)
|
||||
if out, err := chromeCmd.CombinedOutput(); err != nil {
|
||||
// Fallback to Chromium
|
||||
chromiumCmd := exec.Command("chromium", chromeArgs...)
|
||||
if chromiumOut, err := chromiumCmd.CombinedOutput(); err != nil {
|
||||
// Final fallback to wkhtmltopdf
|
||||
wkhtmlArgs := []string{
|
||||
"--enable-local-file-access",
|
||||
"--dpi", "300",
|
||||
"--page-size", "A4",
|
||||
"--orientation", "Portrait",
|
||||
"--margin-top", "0",
|
||||
"--margin-bottom", "0",
|
||||
"--margin-left", "0",
|
||||
"--margin-right", "0",
|
||||
htmlPath,
|
||||
pdfPath,
|
||||
}
|
||||
wkhtmlCmd := exec.Command("wkhtmltopdf", wkhtmlArgs...)
|
||||
if wkhtmlOut, err := wkhtmlCmd.CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("all PDF generators failed. Chrome error: %v, Chromium error: %v, wkhtmltopdf error: %v, chrome output: %s, chromium output: %s, wkhtmltopdf output: %s", chromeCmd.Err, chromiumCmd.Err, err, string(out), string(chromiumOut), string(wkhtmlOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read PDF bytes
|
||||
pdfBytes, err := os.ReadFile(pdfPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read pdf: %w", err)
|
||||
}
|
||||
return pdfBytes, nil
|
||||
}
|
||||
|
||||
func formatCurrency(amount float64) string {
|
||||
// Simple currency formatting: Rp with thousands separators
|
||||
// Note: For exact locale formatting, integrate a locale library later
|
||||
s := fmt.Sprintf("%.0f", amount) // Remove decimal places for cleaner display
|
||||
intPart := addThousandsSep(s)
|
||||
return "Rp " + intPart
|
||||
}
|
||||
|
||||
func splitAmount(s string) (string, string) {
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == '.' {
|
||||
return s[:i], s[i+1:]
|
||||
}
|
||||
}
|
||||
return s, "00"
|
||||
}
|
||||
|
||||
func addThousandsSep(s string) string {
|
||||
n := len(s)
|
||||
if n <= 3 {
|
||||
return s
|
||||
}
|
||||
var out bytes.Buffer
|
||||
pre := n % 3
|
||||
if pre > 0 {
|
||||
out.WriteString(s[:pre])
|
||||
if n > pre {
|
||||
out.WriteByte('.')
|
||||
}
|
||||
}
|
||||
for i := pre; i < n; i += 3 {
|
||||
out.WriteString(s[i : i+3])
|
||||
if i+3 < n {
|
||||
out.WriteByte('.')
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
@@ -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