package repository import ( "enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/repository/models" "time" "github.com/pkg/errors" "go.uber.org/zap" "gorm.io/gorm" ) type OrderRepository interface { Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error) GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error) GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error) GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, error) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error) UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error } type orderRepository struct { db *gorm.DB } func NeworderRepository(db *gorm.DB) *orderRepository { return &orderRepository{db: db} } func (r *orderRepository) Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) { orderDB := r.toOrderDBModel(order) tx := r.db.Begin() if tx.Error != nil { return nil, errors.Wrap(tx.Error, "failed to begin transaction") } defer func() { if r := recover(); r != nil { tx.Rollback() } }() if order.InProgressOrderID != 0 { orderDB.ID = order.InProgressOrderID if err := tx.Omit("customer_id", "partner_id", "customer_name", "created_by").Save(&orderDB).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to update in-progress order") } order.ID = order.InProgressOrderID if err := tx.Where("order_id = ?", order.ID).Delete(&models.OrderItemDB{}).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to delete existing order items") } } else { if err := tx.Create(&orderDB).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to insert order") } order.ID = orderDB.ID } for i := range order.OrderItems { item := &order.OrderItems[i] item.OrderID = order.ID itemDB := r.toOrderItemDBModel(item) if err := tx.Create(&itemDB).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to insert order item") } item.ID = itemDB.ID } if err := tx.Commit().Error; err != nil { return nil, errors.Wrap(err, "failed to commit transaction") } var updatedOrderDB models.OrderDB if err := r.db.Preload("OrderItems").First(&updatedOrderDB, order.ID).Error; err != nil { return nil, errors.Wrap(err, "failed to fetch updated order") } updatedOrder := r.toDomainOrderModel(&updatedOrderDB) return updatedOrder, nil } func (r *orderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) { var orderDB models.OrderDB if err := r.db.Preload("OrderItems").First(&orderDB, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("order not found") } return nil, errors.Wrap(err, "failed to find order") } order := r.toDomainOrderModel(&orderDB) return order, nil } func (r *orderRepository) CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) { inquiryDB := r.toOrderInquiryDBModel(inquiry) inquiryItems := make([]models.InquiryItemDB, 0, len(inquiry.OrderItems)) for _, item := range inquiry.OrderItems { inquiryItems = append(inquiryItems, models.InquiryItemDB{ InquiryID: inquiryDB.ID, ItemID: item.ItemID, ItemType: item.ItemType, ItemName: item.ItemName, Price: item.Price, Quantity: item.Quantity, CreatedBy: item.CreatedBy, CreatedAt: time.Now(), Notes: item.Notes, }) } tx := r.db.Begin() if tx.Error != nil { return nil, errors.Wrap(tx.Error, "failed to begin transaction") } defer func() { if r := recover(); r != nil { tx.Rollback() } }() if err := tx.Create(&inquiryDB).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to insert order inquiry") } if len(inquiryItems) > 0 { if err := tx.CreateInBatches(inquiryItems, 100).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to insert inquiry items") } } if err := tx.Commit().Error; err != nil { return nil, errors.Wrap(err, "failed to commit transaction") } return inquiry, nil } func (r *orderRepository) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) { var inquiryDB models.OrderInquiryDB if err := r.db.Preload("InquiryItems").First(&inquiryDB, "id = ?", id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("inquiry not found") } return nil, errors.Wrap(err, "failed to find inquiry") } inquiry := r.toDomainOrderInquiryModel(&inquiryDB) orderItems := make([]entity.OrderItem, 0, len(inquiryDB.InquiryItems)) for _, itemDB := range inquiryDB.InquiryItems { orderItems = append(orderItems, entity.OrderItem{ ItemID: itemDB.ItemID, ItemType: itemDB.ItemType, ItemName: itemDB.ItemName, Price: itemDB.Price, Quantity: itemDB.Quantity, CreatedBy: itemDB.CreatedBy, CreatedAt: itemDB.CreatedAt, Notes: itemDB.Notes, }) } inquiry.OrderItems = orderItems return inquiry, nil } func (r *orderRepository) UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error { now := time.Now() result := r.db.Model(&models.OrderInquiryDB{}). Where("id = ?", id). Updates(map[string]interface{}{ "status": status, "updated_at": now, }) if result.Error != nil { return errors.Wrap(result.Error, "failed to update inquiry status") } if result.RowsAffected == 0 { logger.ContextLogger(ctx).Warn("no inquiry updated", zap.String("id", id)) } return nil } func (r *orderRepository) UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error { now := time.Now() result := r.db.Model(&models.OrderDB{}). Where("id = ?", id). Updates(map[string]interface{}{ "status": status, "updated_at": now, "description": description, }) if result.Error != nil { return errors.Wrap(result.Error, "failed to update order status") } if result.RowsAffected == 0 { logger.ContextLogger(ctx).Warn("no order updated") } return nil } func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB { return models.OrderDB{ ID: order.ID, PartnerID: order.PartnerID, CustomerID: order.CustomerID, InquiryID: order.InquiryID, Status: order.Status, Amount: order.Amount, Tax: order.Tax, Total: order.Total, PaymentType: order.PaymentType, Source: order.Source, CreatedBy: order.CreatedBy, CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt, OrderType: order.OrderType, TableNumber: order.TableNumber, PaymentProvider: order.PaymentProvider, CustomerName: order.CustomerName, CashierSessionID: order.CashierSessionID, } } func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order { orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems)) for _, itemDB := range dbModel.OrderItems { orderItems = append(orderItems, entity.OrderItem{ ID: itemDB.ID, ItemID: itemDB.ItemID, ItemType: itemDB.ItemType, ItemName: itemDB.ItemName, Price: itemDB.Price, Quantity: itemDB.Quantity, Status: itemDB.Status, CreatedBy: itemDB.CreatedBy, CreatedAt: itemDB.CreatedAt, Notes: itemDB.Notes, }) } return &entity.Order{ ID: dbModel.ID, PartnerID: dbModel.PartnerID, CustomerID: dbModel.CustomerID, InquiryID: dbModel.InquiryID, Status: dbModel.Status, Amount: dbModel.Amount, Tax: dbModel.Tax, Total: dbModel.Total, PaymentType: dbModel.PaymentType, Source: dbModel.Source, CreatedBy: dbModel.CreatedBy, CreatedAt: dbModel.CreatedAt, UpdatedAt: dbModel.UpdatedAt, OrderItems: orderItems, CustomerName: dbModel.CustomerName, TableNumber: dbModel.TableNumber, OrderType: dbModel.OrderType, PaymentProvider: dbModel.PaymentProvider, } } func (r *orderRepository) toOrderItemDBModel(item *entity.OrderItem) models.OrderItemDB { return models.OrderItemDB{ ID: item.ID, OrderID: item.OrderID, ItemID: item.ItemID, ItemType: item.ItemType, ItemName: item.ItemName, Price: item.Price, Quantity: item.Quantity, Status: item.Status, CreatedBy: item.CreatedBy, CreatedAt: item.CreatedAt, Notes: item.Notes, } } func (r *orderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *entity.OrderItem { return &entity.OrderItem{ ID: dbModel.ID, OrderID: dbModel.OrderID, ItemID: dbModel.ItemID, ItemType: dbModel.ItemType, Price: dbModel.Price, Quantity: dbModel.Quantity, Status: dbModel.Status, CreatedBy: dbModel.CreatedBy, CreatedAt: dbModel.CreatedAt, ItemName: dbModel.ItemName, Notes: dbModel.Notes, Product: &entity.Product{ ID: dbModel.ItemID, Name: dbModel.ItemName, }, } } func (r *orderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) models.OrderInquiryDB { return models.OrderInquiryDB{ ID: inquiry.ID, PartnerID: inquiry.PartnerID, CustomerID: &inquiry.CustomerID, Status: inquiry.Status, Amount: inquiry.Amount, Tax: inquiry.Tax, Total: inquiry.Total, PaymentType: inquiry.PaymentType, Source: inquiry.Source, CreatedBy: inquiry.CreatedBy, CreatedAt: inquiry.CreatedAt, UpdatedAt: inquiry.UpdatedAt, ExpiresAt: inquiry.ExpiresAt, CustomerName: inquiry.CustomerName, CustomerPhoneNumber: inquiry.CustomerPhoneNumber, CustomerEmail: inquiry.CustomerEmail, PaymentProvider: inquiry.PaymentProvider, OrderType: inquiry.OrderType, TableNumber: inquiry.TableNumber, CashierSessionID: inquiry.CashierSessionID, } } func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiryDB) *entity.OrderInquiry { inquiry := &entity.OrderInquiry{ ID: dbModel.ID, PartnerID: dbModel.PartnerID, Status: dbModel.Status, Amount: dbModel.Amount, Tax: dbModel.Tax, Total: dbModel.Total, PaymentType: dbModel.PaymentType, Source: dbModel.Source, CreatedBy: dbModel.CreatedBy, CreatedAt: dbModel.CreatedAt, ExpiresAt: dbModel.ExpiresAt, OrderItems: []entity.OrderItem{}, OrderType: dbModel.OrderType, CustomerName: dbModel.CustomerName, PaymentProvider: dbModel.PaymentProvider, TableNumber: dbModel.TableNumber, CashierSessionID: dbModel.CashierSessionID, } if dbModel.CustomerID != nil { inquiry.CustomerID = *dbModel.CustomerID } inquiry.UpdatedAt = dbModel.UpdatedAt return inquiry } func (r *orderRepository) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error) { queryBuilder := NewQueryBuilder[models.OrderDB](r.db) filters := []Filter{ Equal("partner_id", partnerID), } if req.Status != "" { filters = append(filters, Equal("status", req.Status)) } if !req.Start.IsZero() { filters = append(filters, GreaterEqual("created_at", req.Start)) } if !req.End.IsZero() { filters = append(filters, LessEqual("created_at", req.End)) } options := QueryOptions{ Filters: filters, Limit: req.Limit, Offset: req.Offset, OrderBy: []string{"created_at DESC"}, Preloads: []string{"OrderItems"}, } baseQuery := queryBuilder.BuildQuery(options) totalCount, err := queryBuilder.Count(baseQuery) if err != nil { return nil, 0, err } query := queryBuilder.ExecuteQuery(baseQuery, options) ordersDB, err := queryBuilder.Find(query) if err != nil { return nil, 0, err } orders := r.convertOrdersToEntity(ordersDB) return orders, totalCount, nil } func (r *orderRepository) convertOrdersToEntity(ordersDB []models.OrderDB) []*entity.Order { orders := make([]*entity.Order, 0, len(ordersDB)) for _, orderDB := range ordersDB { order := r.toDomainOrderModel(&orderDB) order.OrderItems = r.convertOrderItemsToEntity(orderDB.OrderItems) orders = append(orders, order) } return orders } func (r *orderRepository) convertOrderItemsToEntity(itemsDB []models.OrderItemDB) []entity.OrderItem { items := make([]entity.OrderItem, 0, len(itemsDB)) for _, itemDB := range itemsDB { item := r.toDomainOrderItemModel(&itemDB) items = append(items, *item) } return items } func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) { var ordersDB []models.OrderDB var totalCount int64 baseQuery := r.db.Model(&models.OrderDB{}).Where("customer_id = ?", userID) if req.Status != "" { baseQuery = baseQuery.Where("status = ?", req.Status) } if !req.Start.IsZero() { baseQuery = baseQuery.Where("created_at >= ?", req.Start) } if !req.End.IsZero() { baseQuery = baseQuery.Where("created_at <= ?", req.End) } if err := baseQuery.Count(&totalCount).Error; err != nil { return nil, 0, errors.Wrap(err, "failed to count total orders") } query := baseQuery.Session(&gorm.Session{}) query = query.Order("created_at DESC") if req.Limit > 0 { query = query.Limit(req.Limit) } if req.Offset > 0 { query = query.Offset(req.Offset) } if err := query.Preload("OrderItems").Find(&ordersDB).Error; err != nil { return nil, 0, errors.Wrap(err, "failed to find order history by partner ID") } orders := make([]*entity.Order, 0, len(ordersDB)) for _, orderDB := range ordersDB { order := r.toDomainOrderModel(&orderDB) order.OrderItems = make([]entity.OrderItem, 0, len(orderDB.OrderItems)) for _, itemDB := range orderDB.OrderItems { item := r.toDomainOrderItemModel(&itemDB) order.OrderItems = append(order.OrderItems, *item) } orders = append(orders, order) } return orders, totalCount, nil } func (r *orderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) { orderDB := r.toOrderDBModel(order) // Use provided transaction or create new one var dbTx *gorm.DB if tx != nil { dbTx = tx } else { dbTx = r.db.Begin() if dbTx.Error != nil { return nil, errors.Wrap(dbTx.Error, "failed to begin transaction") } defer func() { if r := recover(); r != nil { dbTx.Rollback() } }() } if order.InProgressOrderID != 0 { // Update existing order orderDB.ID = order.InProgressOrderID if err := dbTx.Omit("customer_id", "partner_id", "customer_name", "created_by").Save(&orderDB).Error; err != nil { if tx == nil { dbTx.Rollback() } return nil, errors.Wrap(err, "failed to update in-progress order") } order.ID = order.InProgressOrderID } else { // Create new order if err := dbTx.Create(&orderDB).Error; err != nil { if tx == nil { dbTx.Rollback() } return nil, errors.Wrap(err, "failed to insert order") } order.ID = orderDB.ID } // Only commit if we created the transaction if tx == nil { if err := dbTx.Commit().Error; err != nil { return nil, errors.Wrap(err, "failed to commit transaction") } } // Return the order with the ID set, but without items (items will be added separately) return order, nil } func (r *orderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error { // Use provided transaction or create new one var dbTx *gorm.DB if tx != nil { dbTx = tx } else { dbTx = r.db.Begin() if dbTx.Error != nil { return errors.Wrap(dbTx.Error, "failed to begin transaction") } defer func() { if r := recover(); r != nil { dbTx.Rollback() } }() } for _, item := range items { itemDB := r.toOrderItemDBModel(&item) itemDB.OrderID = orderID if err := dbTx.Create(&itemDB).Error; err != nil { if tx == nil { dbTx.Rollback() } return errors.Wrap(err, "failed to insert order item") } item.ID = itemDB.ID } // Only commit if we created the transaction if tx == nil { if err := dbTx.Commit().Error; err != nil { return errors.Wrap(err, "failed to commit transaction") } } return nil } func (r *orderRepository) CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error { itemDB := r.toOrderItemDBModel(item) itemDB.OrderID = orderID if err := r.db.Create(&itemDB).Error; err != nil { return errors.Wrap(err, "failed to insert order item") } item.ID = itemDB.ID return nil } func (r *orderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) { var ordersDB []models.OrderDB query := r.db.Where("partner_id = ?", partnerID) if status != "" { query = query.Where("status = ?", status) } query = query.Order("created_at DESC") if limit > 0 { query = query.Limit(limit) } if offset > 0 { query = query.Offset(offset) } if err := query.Find(&ordersDB).Error; err != nil { return nil, errors.Wrap(err, "failed to find orders by partner ID") } orders := make([]*entity.Order, 0, len(ordersDB)) for _, orderDB := range ordersDB { order := r.toDomainOrderModel(&orderDB) var orderItems []models.OrderItemDB if err := r.db.Where("order_id = ?", orderDB.ID).Find(&orderItems).Error; err != nil { return nil, errors.Wrap(err, "failed to find order items") } order.OrderItems = make([]entity.OrderItem, 0, len(orderItems)) for _, itemDB := range orderItems { item := r.toDomainOrderItemModel(&itemDB) orderItem := entity.OrderItem{ ID: item.ID, ItemID: item.ItemID, Quantity: item.Quantity, ItemName: item.ItemName, } if itemDB.ItemID > 0 { var product models.ProductDB err := r.db.First(&product, itemDB.ItemID).Error if err == nil { productDomain := r.toDomainProductModel(&product) orderItem.Product = productDomain } } order.OrderItems = append(order.OrderItems, orderItem) } orders = append(orders, order) } return orders, nil } func (r *orderRepository) GetOrderPaymentMethodBreakdown( ctx mycontext.Context, partnerID int64, req entity.SearchRequest, ) ([]entity.PaymentMethodBreakdown, error) { var breakdown []entity.PaymentMethodBreakdown baseQuery := r.db.Model(&models.OrderDB{}).Where("partner_id = ?", partnerID) if !req.Start.IsZero() { baseQuery = baseQuery.Where("created_at >= ?", req.Start) } if !req.End.IsZero() { baseQuery = baseQuery.Where("created_at <= ?", req.End) } if req.Status != "" { baseQuery = baseQuery.Where("status = ?", req.Status) } err := baseQuery.Select( "payment_type, " + "payment_provider, " + "COUNT(*) as total_transactions, " + "SUM(total) as total_amount", ).Group( "payment_type, payment_provider", ).Order("total_amount DESC").Scan(&breakdown).Error if err != nil { return nil, errors.Wrap(err, "failed to get payment method breakdown") } return breakdown, nil } func (r *orderRepository) GetRevenueOverview( ctx mycontext.Context, req entity.RevenueOverviewRequest, ) ([]entity.RevenueOverviewItem, error) { overview := []entity.RevenueOverviewItem{} baseQuery := r.db.Model(&models.OrderDB{}). Where("partner_id = ?", req.PartnerID). Where("EXTRACT(YEAR FROM created_at) = ?", req.Year) if req.Status != "" { baseQuery = baseQuery.Where("status = ?", req.Status) } switch req.Granularity { case "m": // Monthly err := baseQuery.Select( "TO_CHAR(created_at, 'YYYY-MM') as period, " + "SUM(total) as total_amount, " + "COUNT(*) as order_count", ).Group("period"). Order("period"). Scan(&overview).Error if err != nil { return nil, errors.Wrap(err, "failed to get monthly revenue overview") } case "w": // Weekly err := baseQuery.Select( "CONCAT(EXTRACT(YEAR FROM created_at), '-W', " + "LPAD(EXTRACT(WEEK FROM created_at)::text, 2, '0')) as period, " + "SUM(total) as total_amount, " + "COUNT(*) as order_count", ).Group("period"). Order("period"). Scan(&overview).Error if err != nil { return nil, errors.Wrap(err, "failed to get weekly revenue overview") } case "d": // Daily err := baseQuery.Select( "TO_CHAR(created_at, 'YYYY-MM-DD') as period, " + "SUM(total) as total_amount, " + "COUNT(*) as order_count", ).Group("period"). Order("period"). Scan(&overview).Error if err != nil { return nil, errors.Wrap(err, "failed to get daily revenue overview") } default: return nil, errors.New("invalid granularity. Use 'm' (monthly), 'w' (weekly), or 'd' (daily)") } return overview, nil } func (r *orderRepository) GetSalesByCategory( ctx mycontext.Context, req entity.SalesByCategoryRequest, ) ([]entity.SalesByCategoryItem, error) { salesByCategory := []entity.SalesByCategoryItem{} baseQuery := r.db.Model(&models.OrderItemDB{}). Joins("JOIN orders ON order_items.order_id = orders.id"). Where("orders.partner_id = ?", req.PartnerID) if req.Status != "" { baseQuery = baseQuery.Where("orders.status = ?", req.Status) } switch req.Period { case "d": // Daily baseQuery = baseQuery.Where("DATE(orders.created_at) = CURRENT_DATE") case "w": // Weekly baseQuery = baseQuery.Where("DATE_TRUNC('week', orders.created_at) = DATE_TRUNC('week', CURRENT_DATE)") case "m": // Monthly baseQuery = baseQuery.Where("DATE_TRUNC('month', orders.created_at) = DATE_TRUNC('month', CURRENT_DATE)") default: return nil, errors.New("invalid period. Use 'd' (daily), 'w' (weekly), or 'm' (monthly)") } var totalSales float64 err := r.db.Model(&models.OrderItemDB{}). Joins("JOIN orders ON order_items.order_id = orders.id"). Where("orders.partner_id = ?", req.PartnerID). Select("COALESCE(SUM(order_items.price * order_items.quantity), 0)"). Scan(&totalSales).Error if err != nil { return nil, errors.Wrap(err, "failed to calculate total sales") } err = baseQuery.Select( "order_items.item_type AS category, " + "COALESCE(SUM(order_items.price * order_items.quantity), 0) AS total_amount, " + "COALESCE(SUM(order_items.quantity), 0) AS total_quantity", ). Group("order_items.item_type"). Order("total_amount DESC"). Scan(&salesByCategory).Error if err != nil { return nil, errors.Wrap(err, "failed to get sales by category") } for i := range salesByCategory { if totalSales > 0 { salesByCategory[i].Percentage = (salesByCategory[i].TotalAmount / totalSales) * 100 } } return salesByCategory, nil } func (r *orderRepository) GetPopularProducts( ctx mycontext.Context, req entity.PopularProductsRequest, ) ([]entity.PopularProductItem, error) { if req.Limit == 0 { req.Limit = 10 } if req.SortBy != "sales" && req.SortBy != "revenue" { req.SortBy = "sales" // default to sales } // Base query baseQuery := r.db.Model(&models.OrderItemDB{}). Joins("JOIN orders ON order_items.order_id = orders.id"). Where("orders.partner_id = ?", req.PartnerID) if req.Status != "" { baseQuery = baseQuery.Where("orders.status = ?", req.Status) } switch req.Period { case "d": // Daily baseQuery = baseQuery.Where("DATE(orders.created_at) = CURRENT_DATE") case "w": // Weekly baseQuery = baseQuery.Where("DATE_TRUNC('week', orders.created_at) = DATE_TRUNC('week', CURRENT_DATE)") case "m": // Monthly baseQuery = baseQuery.Where("DATE_TRUNC('month', orders.created_at) = DATE_TRUNC('month', CURRENT_DATE)") default: return nil, errors.New("invalid period. Use 'd' (daily), 'w' (weekly), or 'm' (monthly)") } // Calculate total sales/revenue for percentage calculation var totalSales struct { TotalAmount float64 TotalQuantity int64 } err := baseQuery. Select("COALESCE(SUM(order_items.price * order_items.quantity), 0) as total_amount, " + "COALESCE(SUM(order_items.quantity), 0) as total_quantity"). Scan(&totalSales).Error if err != nil { return nil, errors.Wrap(err, "failed to calculate total sales") } popularProducts := []entity.PopularProductItem{} orderClause := "total_sales DESC" if req.SortBy == "revenue" { orderClause = "total_revenue DESC" } err = baseQuery. Select( "order_items.item_id AS product_id, " + "order_items.item_name AS product_name, " + "order_items.item_type AS category, " + "COALESCE(SUM(order_items.quantity), 0) AS total_sales, " + "COALESCE(SUM(order_items.price * order_items.quantity), 0) AS total_revenue, " + "COALESCE(AVG(order_items.price), 0) AS average_price", ). Group("order_items.item_id, order_items.item_name, order_items.item_type"). Order(orderClause). Limit(req.Limit). Scan(&popularProducts).Error if err != nil { return nil, errors.Wrap(err, "failed to get popular products") } for i := range popularProducts { popularProducts[i].Percentage = (float64(popularProducts[i].TotalSales) / float64(totalSales.TotalQuantity)) * 100 } return popularProducts, nil } func (r *orderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) { var orderDB models.OrderDB if err := r.db.Preload("OrderItems").Where("id = ? AND partner_id = ?", id, partnerID).First(&orderDB).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("order not found") } return nil, errors.Wrap(err, "failed to find order") } order := r.toDomainOrderModel(&orderDB) return order, nil } func (r *orderRepository) FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error) { var orderDB models.OrderDB if err := r.db.Preload("OrderItems").Where("id = ? AND customer_id = ?", id, customerID).First(&orderDB).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("order not found") } return nil, errors.Wrap(err, "failed to find order") } order := r.toDomainOrderModel(&orderDB) for _, itemDB := range orderDB.OrderItems { item := r.toDomainOrderItemModel(&itemDB) order.OrderItems = append(order.OrderItems, *item) } return order, nil } func (r *orderRepository) UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error { now := time.Now() result := r.db.Model(&models.OrderItemDB{}). Where("order_item_id = ?", orderItemID). Updates(map[string]interface{}{ "quantity": quantity, "updated_at": now, }) if result.Error != nil { return errors.Wrap(result.Error, "failed to update order item") } if result.RowsAffected == 0 { logger.ContextLogger(ctx).Warn("no order item updated") } return nil } func (r *orderRepository) UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error { now := time.Now() result := r.db.Model(&models.OrderDB{}). Where("id = ?", orderID). Updates(map[string]interface{}{ "amount": amount, "tax": tax, "total": total, "updated_at": now, }) if result.Error != nil { return errors.Wrap(result.Error, "failed to update order totals") } if result.RowsAffected == 0 { logger.ContextLogger(ctx).Warn("no order updated") } return nil } func (r *orderRepository) UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error { now := time.Now() result := trx.Model(&models.OrderDB{}). Where("id = ?", orderID). Updates(map[string]interface{}{ "amount": amount, "tax": tax, "total": total, "updated_at": now, }) if result.Error != nil { return errors.Wrap(result.Error, "failed to update order totals") } if result.RowsAffected == 0 { logger.ContextLogger(ctx).Warn("no order updated") } return nil } func (r *orderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product { if productDB == nil { return nil } return &entity.Product{ ID: productDB.ID, Name: productDB.Name, Description: productDB.Description, Price: productDB.Price, CreatedAt: productDB.CreatedAt, UpdatedAt: productDB.UpdatedAt, Type: productDB.Type, Image: productDB.Image, } }