292 lines
7.6 KiB
Go
292 lines
7.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"cmroubao/backend-api/internal/domain"
|
|
"cmroubao/backend-api/internal/usecase"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func (h *adminHandlers) createFreightSync(ctx *gin.Context) {
|
|
if !hasMediaType(ctx, "application/json") {
|
|
writePublicError(
|
|
ctx,
|
|
http.StatusUnsupportedMediaType,
|
|
"UNSUPPORTED_MEDIA_TYPE",
|
|
"application/json is required",
|
|
false,
|
|
gin.H{},
|
|
)
|
|
return
|
|
}
|
|
var request struct {
|
|
Mode string `json:"mode"`
|
|
OrderNumber string `json:"order_number"`
|
|
CreatedFrom string `json:"created_from"`
|
|
CreatedTo string `json:"created_to"`
|
|
SyncToNow bool `json:"sync_to_now"`
|
|
}
|
|
if err := decodeJSON(ctx, &request); err != nil {
|
|
writePublicError(
|
|
ctx,
|
|
http.StatusBadRequest,
|
|
"INVALID_JSON",
|
|
"request body must be valid JSON",
|
|
false,
|
|
gin.H{},
|
|
)
|
|
return
|
|
}
|
|
var result usecase.CreateFreightSyncResult
|
|
var err error
|
|
switch request.Mode {
|
|
case domain.FreightSyncOrderNumber:
|
|
if strings.TrimSpace(request.CreatedFrom) != "" ||
|
|
strings.TrimSpace(request.CreatedTo) != "" ||
|
|
request.SyncToNow {
|
|
writePublicError(
|
|
ctx,
|
|
http.StatusUnprocessableEntity,
|
|
"FREIGHT_SYNC_INVALID",
|
|
"freight sync request is invalid",
|
|
false,
|
|
fieldDetails("mode", "ORDER_NUMBER cannot include date parameters"),
|
|
)
|
|
return
|
|
}
|
|
result, err = h.services.Freight.CreateOrderSync(
|
|
ctx.Request.Context(),
|
|
usecase.CreateFreightSyncCommand{
|
|
CreatorSubject: localAdminSubject,
|
|
ActorUserID: adminActorUserID(ctx),
|
|
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
|
|
OrderNumber: request.OrderNumber,
|
|
},
|
|
)
|
|
case domain.FreightSyncCreatedRange:
|
|
if strings.TrimSpace(request.OrderNumber) != "" {
|
|
writePublicError(
|
|
ctx,
|
|
http.StatusUnprocessableEntity,
|
|
"FREIGHT_SYNC_INVALID",
|
|
"freight sync request is invalid",
|
|
false,
|
|
fieldDetails("mode", "CREATED_RANGE cannot include order_number"),
|
|
)
|
|
return
|
|
}
|
|
result, err = h.services.Freight.CreateDateSync(
|
|
ctx.Request.Context(),
|
|
usecase.CreateFreightDateSyncCommand{
|
|
CreatorSubject: localAdminSubject,
|
|
ActorUserID: adminActorUserID(ctx),
|
|
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
|
|
CreatedFrom: request.CreatedFrom,
|
|
CreatedTo: request.CreatedTo,
|
|
SyncToNow: request.SyncToNow,
|
|
},
|
|
)
|
|
default:
|
|
writePublicError(
|
|
ctx,
|
|
http.StatusUnprocessableEntity,
|
|
"FREIGHT_SYNC_MODE_INVALID",
|
|
"freight sync mode is not supported",
|
|
false,
|
|
fieldDetails("mode", "must be ORDER_NUMBER or CREATED_RANGE"),
|
|
)
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeUsecaseError(ctx, err)
|
|
return
|
|
}
|
|
ctx.Header("Cache-Control", "no-store")
|
|
status := http.StatusAccepted
|
|
if request.Mode == domain.FreightSyncOrderNumber {
|
|
status = http.StatusCreated
|
|
if result.Replayed {
|
|
status = http.StatusOK
|
|
}
|
|
ctx.Header("Location", "/api/v1/freight-orders")
|
|
}
|
|
ctx.JSON(status, gin.H{
|
|
"sync": freightSyncResponse(result.Run),
|
|
"replayed": result.Replayed,
|
|
})
|
|
}
|
|
|
|
func (h *adminHandlers) freightSyncWatermark(ctx *gin.Context) {
|
|
watermark, err := h.services.Freight.GetWatermark(
|
|
ctx.Request.Context(),
|
|
localAdminSubject,
|
|
)
|
|
if err != nil {
|
|
writeUsecaseError(ctx, err)
|
|
return
|
|
}
|
|
ctx.Header("Cache-Control", "no-store")
|
|
if watermark == nil {
|
|
ctx.JSON(http.StatusOK, gin.H{"watermark": nil})
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"watermark": gin.H{
|
|
"source_system": watermark.SourceSystem,
|
|
"last_successful_to": formatTime(watermark.LastSuccessfulTo),
|
|
"last_successful_run_id": watermark.LastSuccessfulRunID,
|
|
"updated_at": formatTime(watermark.UpdatedAt),
|
|
}})
|
|
}
|
|
|
|
func (h *adminHandlers) freightSyncDetail(ctx *gin.Context) {
|
|
run, err := h.services.Freight.GetSync(
|
|
ctx.Request.Context(),
|
|
localAdminSubject,
|
|
ctx.Param("id"),
|
|
)
|
|
if err != nil {
|
|
writeUsecaseError(ctx, err)
|
|
return
|
|
}
|
|
ctx.Header("Cache-Control", "no-store")
|
|
ctx.JSON(http.StatusOK, gin.H{"sync": freightSyncResponse(run)})
|
|
}
|
|
|
|
func (h *adminHandlers) listFreightOrders(ctx *gin.Context) {
|
|
limit := 0
|
|
if value := strings.TrimSpace(ctx.Query("limit")); value != "" {
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
writePublicError(
|
|
ctx,
|
|
http.StatusBadRequest,
|
|
"FREIGHT_LIST_INVALID",
|
|
"freight list filter is invalid",
|
|
false,
|
|
fieldDetails("limit", "must be an integer"),
|
|
)
|
|
return
|
|
}
|
|
limit = parsed
|
|
}
|
|
orders, err := h.services.Freight.ListOrders(
|
|
ctx.Request.Context(),
|
|
localAdminSubject,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
writeUsecaseError(ctx, err)
|
|
return
|
|
}
|
|
items := make([]gin.H, 0, len(orders))
|
|
for _, order := range orders {
|
|
items = append(items, freightOrderResponse(order))
|
|
}
|
|
ctx.Header("Cache-Control", "no-store")
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"items": items,
|
|
"next_cursor": nil,
|
|
})
|
|
}
|
|
|
|
func (h *adminHandlers) freightOrderDetail(ctx *gin.Context) {
|
|
detail, err := h.services.Freight.GetOrder(
|
|
ctx.Request.Context(),
|
|
localAdminSubject,
|
|
ctx.Param("id"),
|
|
)
|
|
if err != nil {
|
|
writeUsecaseError(ctx, err)
|
|
return
|
|
}
|
|
items := make([]gin.H, 0, len(detail.Items))
|
|
for _, item := range detail.Items {
|
|
items = append(items, gin.H{
|
|
"id": item.ID,
|
|
"external_item_id": item.ExternalItemID,
|
|
"title": item.Title,
|
|
"product_spec": item.ProductSpec,
|
|
"sku": item.SKU,
|
|
"quantity": item.Quantity,
|
|
"product_thumb_ref": item.ProductThumbRef,
|
|
"purchase_status": item.PurchaseStatus,
|
|
"revision": item.Revision,
|
|
"canonical_sha256": item.CanonicalSHA256,
|
|
"updated_at": formatTime(item.UpdatedAt),
|
|
})
|
|
}
|
|
ctx.Header("Cache-Control", "no-store")
|
|
response := gin.H{
|
|
"order": freightOrderResponse(detail.Order),
|
|
"items": items,
|
|
}
|
|
if h.services.Procurement != nil {
|
|
requests, requestErr := h.services.Procurement.ListForOrder(
|
|
ctx.Request.Context(),
|
|
localAdminSubject,
|
|
detail.Order.ID,
|
|
)
|
|
if requestErr != nil {
|
|
writeUsecaseError(ctx, requestErr)
|
|
return
|
|
}
|
|
requestItems := make([]gin.H, 0, len(requests))
|
|
for _, request := range requests {
|
|
requestItems = append(
|
|
requestItems,
|
|
procurementRequestResponse(request),
|
|
)
|
|
}
|
|
response["procurement_requests"] = requestItems
|
|
}
|
|
ctx.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
func freightSyncResponse(run domain.FreightSyncRun) gin.H {
|
|
return gin.H{
|
|
"id": run.ID,
|
|
"mode": run.Mode,
|
|
"created_from": nullableResponseString(run.CreatedFrom),
|
|
"created_to": nullableResponseString(run.CreatedTo),
|
|
"watermark_through": formatOptionalTime(run.WatermarkThrough),
|
|
"query_sha256": run.QuerySHA256,
|
|
"status": run.Status,
|
|
"error_code": run.ErrorCode,
|
|
"order_count": run.OrderCount,
|
|
"item_count": run.ItemCount,
|
|
"created_at": formatTime(run.CreatedAt),
|
|
"started_at": formatOptionalTime(run.StartedAt),
|
|
"finished_at": formatOptionalTime(run.FinishedAt),
|
|
}
|
|
}
|
|
|
|
func nullableResponseString(value string) any {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func freightOrderResponse(order domain.FreightOrder) gin.H {
|
|
return gin.H{
|
|
"id": order.ID,
|
|
"source_system": order.SourceSystem,
|
|
"external_stock_id": order.ExternalStockID,
|
|
"source_code": order.SourceCode,
|
|
"platform_order_no": order.PlatformOrderNo,
|
|
"shop_name": order.ShopName,
|
|
"source_created_at": formatOptionalTime(order.SourceCreatedAt),
|
|
"order_status": order.OrderStatus,
|
|
"purchase_status": order.PurchaseStatus,
|
|
"is_canceled": order.IsCanceled,
|
|
"revision": order.Revision,
|
|
"canonical_sha256": order.CanonicalSHA256,
|
|
"item_count": order.ItemCount,
|
|
"updated_at": formatTime(order.UpdatedAt),
|
|
}
|
|
}
|