449 lines
19 KiB
Go
449 lines
19 KiB
Go
package purchase
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"strings"
|
||
"time"
|
||
"unicode"
|
||
"unicode/utf8"
|
||
|
||
"go-admin/app/goauto/models"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type AdminListRequest struct {
|
||
Page int
|
||
PageSize int
|
||
TaskID uint64
|
||
Status string
|
||
ExecutionMode string
|
||
TaskType string
|
||
SYBProductID uint64
|
||
ShopeeOrderNo string
|
||
ShopeeOrderNos []string
|
||
PDDOrderNo string
|
||
}
|
||
|
||
type AdminTaskItem struct {
|
||
OrderWriteback OrderWritebackView `json:"orderWriteback"`
|
||
ID uint64 `json:"id"`
|
||
TaskType string `json:"taskType"`
|
||
ExecutionMode string `json:"executionMode"`
|
||
Status string `json:"status"`
|
||
SYBProductID *uint64 `json:"sybProductId,omitempty"`
|
||
ShopeeProductID *uint64 `json:"shopeeProductId,omitempty"`
|
||
PDDProductID uint64 `json:"pddProductId"`
|
||
DeviceID *uint64 `json:"deviceId,omitempty"`
|
||
DeviceName string `json:"deviceName,omitempty"`
|
||
ShopeeItemIDSnapshot string `json:"shopeeItemIdSnapshot"`
|
||
ShopeeOrderNoSnapshot string `json:"shopeeOrderNoSnapshot"`
|
||
ShopeeTitleSnapshot string `json:"shopeeTitleSnapshot"`
|
||
ShopeeShopNameSnapshot string `json:"shopeeShopNameSnapshot"`
|
||
PDDGoodsIDSnapshot string `json:"pddGoodsIdSnapshot"`
|
||
PDDTitleSnapshot string `json:"pddTitleSnapshot"`
|
||
TargetColorSnapshot string `json:"targetColorSnapshot"`
|
||
TargetSizeSnapshot string `json:"targetSizeSnapshot"`
|
||
MappedColorSnapshot string `json:"mappedColorSnapshot"`
|
||
MappedSizeSnapshot string `json:"mappedSizeSnapshot"`
|
||
SpecSource string `json:"specSource"`
|
||
Quantity int64 `json:"quantity"`
|
||
ReferenceUnitPriceCent int64 `json:"referenceUnitPriceCent"`
|
||
MinUnitPriceCent int64 `json:"minUnitPriceCent"`
|
||
MaxUnitPriceCent int64 `json:"maxUnitPriceCent"`
|
||
Currency string `json:"currency"`
|
||
PDDAccountRefSnapshot string `json:"pddAccountRefSnapshot"`
|
||
AddressSuffix string `json:"addressSuffix"`
|
||
PDDOrderNo *string `json:"pddOrderNo,omitempty"`
|
||
OrderSubmittedAt *time.Time `json:"orderSubmittedAt,omitempty"`
|
||
PDDOrderAmountCent *int64 `json:"pddOrderAmountCent,omitempty"`
|
||
IrreversibleAt *time.Time `json:"irreversibleAt,omitempty"`
|
||
PaymentReviewStatus string `json:"paymentReviewStatus"`
|
||
PaymentReviewedAt *time.Time `json:"paymentReviewedAt,omitempty"`
|
||
TrackingNo *string `json:"trackingNo,omitempty"`
|
||
TrackingCollectedAt *time.Time `json:"trackingCollectedAt,omitempty"`
|
||
LogisticsStatus string `json:"logisticsStatus"`
|
||
WritebackStatus string `json:"writebackStatus"`
|
||
WritebackAt *time.Time `json:"writebackAt,omitempty"`
|
||
RePurchaseAuthorizedAt *time.Time `json:"rePurchaseAuthorizedAt,omitempty"`
|
||
RePurchaseConsumedAt *time.Time `json:"rePurchaseConsumedAt,omitempty"`
|
||
CancelledAt *time.Time `json:"cancelledAt,omitempty"`
|
||
CancelReason *string `json:"cancelReason,omitempty"`
|
||
ErrorCode *string `json:"errorCode,omitempty"`
|
||
ErrorMessage *string `json:"errorMessage,omitempty"`
|
||
Retryable bool `json:"retryable"`
|
||
RetryDisabledCode string `json:"retryDisabledCode,omitempty"`
|
||
RetryDisabledReason string `json:"retryDisabledReason,omitempty"`
|
||
StatusVersion uint64 `json:"statusVersion"`
|
||
StatusChangedAt time.Time `json:"statusChangedAt"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
UpdatedAt time.Time `json:"updatedAt"`
|
||
Matching MatchingView `json:"matching"`
|
||
ImageSearchLinked bool `json:"imageSearchLinked,omitempty"`
|
||
}
|
||
|
||
type AdminAttemptItem struct {
|
||
AttemptID string `json:"attemptId"`
|
||
AttemptNumber int `json:"attemptNumber"`
|
||
Phase string `json:"phase"`
|
||
Status string `json:"status"`
|
||
DeviceID *uint64 `json:"deviceId,omitempty"`
|
||
RuleSnapshotHash string `json:"ruleSnapshotHash"`
|
||
ResultType *string `json:"resultType,omitempty"`
|
||
ErrorCode *string `json:"errorCode,omitempty"`
|
||
ErrorMessage *string `json:"errorMessage,omitempty"`
|
||
StartedAt *time.Time `json:"startedAt,omitempty"`
|
||
FinishedAt *time.Time `json:"finishedAt,omitempty"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
}
|
||
|
||
type AdminListResponse struct {
|
||
Items []AdminTaskItem `json:"items"`
|
||
Total int64 `json:"total"`
|
||
Page int `json:"page"`
|
||
PageSize int `json:"pageSize"`
|
||
}
|
||
|
||
type AdminDetailResponse struct {
|
||
Task AdminTaskItem `json:"task"`
|
||
Attempts []AdminAttemptItem `json:"attempts"`
|
||
Matching MatchingView `json:"matching"`
|
||
}
|
||
|
||
// Query-only value: never a PurchaseTask status or a logistics writeback state.
|
||
const adminStatusSYBWritebackSucceeded = "syb_writeback_succeeded"
|
||
|
||
// A nil list means the optional filter was absent; an explicitly empty list
|
||
// must not silently turn a restricted lookup into an unfiltered query.
|
||
func normalizeAdminOrderNos(single string, raw []string) ([]string, error) {
|
||
single = strings.TrimSpace(single)
|
||
if utf8.RuneCountInString(single) > 64 {
|
||
return nil, fail(CodeInvalidRequest, "每个蝦皮订单号最多 64 个字符")
|
||
}
|
||
if raw == nil {
|
||
return nil, nil
|
||
}
|
||
if single != "" {
|
||
return nil, fail(CodeInvalidRequest, "蝦皮订单号单值与批量参数不能同时使用")
|
||
}
|
||
values := make([]string, 0, len(raw))
|
||
seen := make(map[string]bool)
|
||
for _, entry := range raw {
|
||
for _, value := range strings.FieldsFunc(entry, func(r rune) bool {
|
||
return r == ',' || r == ',' || unicode.IsSpace(r) || r == '\uFEFF'
|
||
}) {
|
||
if utf8.RuneCountInString(value) > 64 {
|
||
return nil, fail(CodeInvalidRequest, "每个蝦皮订单号最多 64 个字符")
|
||
}
|
||
if !seen[value] {
|
||
seen[value] = true
|
||
values = append(values, value)
|
||
if len(values) > 100 {
|
||
return nil, fail(CodeInvalidRequest, "最多查询 100 个蝦皮订单号,请分批查询")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if len(values) == 0 {
|
||
return nil, fail(CodeInvalidRequest, "批量蝦皮订单号不能为空")
|
||
}
|
||
return values, nil
|
||
}
|
||
|
||
func (s *Service) AdminList(ctx context.Context, req AdminListRequest) (AdminListResponse, error) {
|
||
orderNos, err := normalizeAdminOrderNos(req.ShopeeOrderNo, req.ShopeeOrderNos)
|
||
if err != nil {
|
||
return AdminListResponse{}, err
|
||
}
|
||
if req.Page < 1 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize < 1 {
|
||
req.PageSize = 20
|
||
}
|
||
if req.PageSize > 100 {
|
||
req.PageSize = 100
|
||
}
|
||
if req.Status != "" && req.Status != adminStatusSYBWritebackSucceeded && !validPurchaseStatus(req.Status) {
|
||
return AdminListResponse{}, fail(CodeInvalidRequest, "status 无效")
|
||
}
|
||
if req.ExecutionMode != "" && req.ExecutionMode != models.PurchaseExecutionModeRehearsal && req.ExecutionMode != models.PurchaseExecutionModeLive {
|
||
return AdminListResponse{}, fail(CodeInvalidRequest, "executionMode 无效")
|
||
}
|
||
if req.TaskType != "" && req.TaskType != models.PurchaseTaskTypeSYBOrder && req.TaskType != models.PurchaseTaskTypeStock {
|
||
return AdminListResponse{}, fail(CodeInvalidRequest, "taskType 无效")
|
||
}
|
||
query := s.DB.WithContext(ctx).Model(&models.PurchaseTask{})
|
||
if req.TaskID > 0 {
|
||
query = query.Where("id = ?", req.TaskID)
|
||
}
|
||
if req.Status == adminStatusSYBWritebackSucceeded {
|
||
query = query.Where("id IN (?)", s.DB.WithContext(ctx).Model(&models.PurchaseOrderWriteback{}).Select("purchase_task_id").Where("status = ?", "succeeded"))
|
||
} else if req.Status != "" {
|
||
query = query.Where("status = ?", req.Status)
|
||
}
|
||
if req.ExecutionMode != "" {
|
||
query = query.Where("execution_mode = ?", req.ExecutionMode)
|
||
}
|
||
if req.TaskType != "" {
|
||
query = query.Where("task_type = ?", req.TaskType)
|
||
}
|
||
if req.SYBProductID > 0 {
|
||
query = query.Where("syb_product_id = ?", req.SYBProductID)
|
||
}
|
||
if orderNo := strings.TrimSpace(req.ShopeeOrderNo); orderNo != "" {
|
||
query = query.Where("shopee_order_no_snapshot LIKE ?", "%"+orderNo+"%")
|
||
}
|
||
if len(orderNos) > 0 {
|
||
query = query.Where("shopee_order_no_snapshot IN ?", orderNos)
|
||
}
|
||
if orderNo := strings.TrimSpace(req.PDDOrderNo); orderNo != "" {
|
||
query = query.Where("pdd_order_no LIKE ?", "%"+orderNo+"%")
|
||
}
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return AdminListResponse{}, internal(err)
|
||
}
|
||
var tasks []models.PurchaseTask
|
||
if err := query.Order("created_at DESC, id DESC").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&tasks).Error; err != nil {
|
||
return AdminListResponse{}, internal(err)
|
||
}
|
||
deviceNames, err := loadDeviceNames(s.DB.WithContext(ctx), tasks)
|
||
if err != nil {
|
||
return AdminListResponse{}, err
|
||
}
|
||
matching, err := s.matchingViews(ctx, tasks)
|
||
if err != nil {
|
||
return AdminListResponse{}, err
|
||
}
|
||
imageSearchLinked, err := s.loadImageSearchLinked(ctx, tasks)
|
||
if err != nil {
|
||
return AdminListResponse{}, err
|
||
}
|
||
items := make([]AdminTaskItem, 0, len(tasks))
|
||
writebacks, err := s.OrderWritebackViews(ctx, tasks)
|
||
if err != nil {
|
||
return AdminListResponse{}, err
|
||
}
|
||
for _, task := range tasks {
|
||
item := adminTaskItem(task, deviceNames, s.retryQueryEligibility(ctx, task, true))
|
||
item.OrderWriteback = writebacks[task.ID]
|
||
item.Matching = matching[task.ID]
|
||
if task.ShopeeProductID != nil {
|
||
item.ImageSearchLinked = imageSearchLinked[*task.ShopeeProductID]
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
return AdminListResponse{Items: items, Total: total, Page: req.Page, PageSize: req.PageSize}, nil
|
||
}
|
||
|
||
// loadImageSearchLinked bulk-loads the read-only "linked via image search, not
|
||
// manually verified" flag for purchase-task list/detail views (#279 item 5).
|
||
// It is display-only and must never influence eligibility or matching.
|
||
func (s *Service) loadImageSearchLinked(ctx context.Context, tasks []models.PurchaseTask) (map[uint64]bool, error) {
|
||
result := make(map[uint64]bool)
|
||
ids := make([]uint64, 0, len(tasks))
|
||
seen := make(map[uint64]bool)
|
||
for _, task := range tasks {
|
||
if task.ShopeeProductID != nil && !seen[*task.ShopeeProductID] {
|
||
ids = append(ids, *task.ShopeeProductID)
|
||
seen[*task.ShopeeProductID] = true
|
||
}
|
||
}
|
||
if len(ids) == 0 {
|
||
return result, nil
|
||
}
|
||
var shopeeProducts []models.ShopeeProduct
|
||
if err := s.DB.WithContext(ctx).Select("id, image_search_linked").Where("id IN ?", ids).Find(&shopeeProducts).Error; err != nil {
|
||
return nil, internal(err)
|
||
}
|
||
for _, shopee := range shopeeProducts {
|
||
result[shopee.ID] = shopee.ImageSearchLinked
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (s *Service) AdminDetail(ctx context.Context, taskID uint64) (AdminDetailResponse, error) {
|
||
var task models.PurchaseTask
|
||
if err := s.DB.WithContext(ctx).First(&task, taskID).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return AdminDetailResponse{}, fail(CodeTaskNotFound, "采购任务不存在")
|
||
}
|
||
return AdminDetailResponse{}, internal(err)
|
||
}
|
||
deviceNames, err := loadDeviceNames(s.DB.WithContext(ctx), []models.PurchaseTask{task})
|
||
if err != nil {
|
||
return AdminDetailResponse{}, err
|
||
}
|
||
var attempts []models.PurchaseTaskAttempt
|
||
if err := s.DB.WithContext(ctx).Where("task_id = ?", task.ID).Order("attempt_number ASC, id ASC").Find(&attempts).Error; err != nil {
|
||
return AdminDetailResponse{}, internal(err)
|
||
}
|
||
items := make([]AdminAttemptItem, 0, len(attempts))
|
||
for _, attempt := range attempts {
|
||
items = append(items, AdminAttemptItem{
|
||
AttemptID: attempt.AttemptID, AttemptNumber: attempt.AttemptNumber, Phase: attempt.Phase,
|
||
Status: attempt.Status, DeviceID: attempt.DeviceID, RuleSnapshotHash: attempt.RuleSnapshotHash,
|
||
ResultType: attempt.ResultType, ErrorCode: attempt.ErrorCode, ErrorMessage: attempt.ErrorMessage,
|
||
StartedAt: attempt.StartedAt, FinishedAt: attempt.FinishedAt, CreatedAt: attempt.CreatedAt,
|
||
})
|
||
}
|
||
matching, matchErr := s.MatchingDetail(ctx, task.ID)
|
||
if matchErr != nil {
|
||
return AdminDetailResponse{}, matchErr
|
||
}
|
||
item := adminTaskItem(task, deviceNames, s.retryQueryEligibility(ctx, task, true))
|
||
writebacks, wbErr := s.OrderWritebackViews(ctx, []models.PurchaseTask{task})
|
||
if wbErr != nil {
|
||
return AdminDetailResponse{}, wbErr
|
||
}
|
||
item.OrderWriteback = writebacks[task.ID]
|
||
item.Matching = matching
|
||
imageSearchLinked, err := s.loadImageSearchLinked(ctx, []models.PurchaseTask{task})
|
||
if err != nil {
|
||
return AdminDetailResponse{}, err
|
||
}
|
||
if task.ShopeeProductID != nil {
|
||
item.ImageSearchLinked = imageSearchLinked[*task.ShopeeProductID]
|
||
}
|
||
return AdminDetailResponse{Task: item, Attempts: items, Matching: matching}, nil
|
||
}
|
||
|
||
func loadDeviceNames(db *gorm.DB, tasks []models.PurchaseTask) (map[uint64]string, error) {
|
||
ids := make([]uint64, 0, len(tasks))
|
||
seen := make(map[uint64]bool)
|
||
for _, task := range tasks {
|
||
if task.DeviceID != nil && !seen[*task.DeviceID] {
|
||
ids = append(ids, *task.DeviceID)
|
||
seen[*task.DeviceID] = true
|
||
}
|
||
}
|
||
names := make(map[uint64]string, len(ids))
|
||
if len(ids) == 0 {
|
||
return names, nil
|
||
}
|
||
var devices []models.AgentDevice
|
||
if err := db.Select("id, name").Where("id IN ?", ids).Find(&devices).Error; err != nil {
|
||
return nil, internal(err)
|
||
}
|
||
for _, device := range devices {
|
||
names[device.ID] = device.Name
|
||
}
|
||
return names, nil
|
||
}
|
||
|
||
func adminTaskItem(task models.PurchaseTask, deviceNames map[uint64]string, retry retryDecision) AdminTaskItem {
|
||
item := AdminTaskItem{
|
||
ID: task.ID, TaskType: task.TaskType, ExecutionMode: task.ExecutionMode, Status: task.Status,
|
||
SYBProductID: task.SYBProductID, ShopeeProductID: task.ShopeeProductID, PDDProductID: task.PDDProductID,
|
||
DeviceID: task.DeviceID, ShopeeItemIDSnapshot: task.ShopeeItemIDSnapshot, ShopeeOrderNoSnapshot: task.ShopeeOrderNoSnapshot,
|
||
ShopeeTitleSnapshot: task.ShopeeTitleSnapshot, ShopeeShopNameSnapshot: task.ShopeeShopNameSnapshot,
|
||
PDDGoodsIDSnapshot: task.PDDGoodsIDSnapshot, PDDTitleSnapshot: task.PDDTitleSnapshot,
|
||
TargetColorSnapshot: task.TargetColorSnapshot, TargetSizeSnapshot: task.TargetSizeSnapshot,
|
||
MappedColorSnapshot: task.MappedColorSnapshot, MappedSizeSnapshot: task.MappedSizeSnapshot,
|
||
SpecSource: task.SpecSource, Quantity: task.Quantity, ReferenceUnitPriceCent: task.ReferenceUnitPriceCent,
|
||
MinUnitPriceCent: task.MinUnitPriceCent, MaxUnitPriceCent: task.MaxUnitPriceCent, Currency: task.Currency,
|
||
PDDAccountRefSnapshot: task.PDDAccountRefSnapshot, AddressSuffix: task.AddressSuffix,
|
||
PDDOrderNo: task.PDDOrderNo, OrderSubmittedAt: task.OrderSubmittedAt, PDDOrderAmountCent: task.PDDOrderAmountCent, IrreversibleAt: task.IrreversibleAt,
|
||
PaymentReviewStatus: task.PaymentReviewStatus, PaymentReviewedAt: task.PaymentReviewedAt,
|
||
TrackingNo: task.TrackingNo, TrackingCollectedAt: task.TrackingCollectedAt,
|
||
LogisticsStatus: task.LogisticsStatus, WritebackStatus: task.WritebackStatus, WritebackAt: task.WritebackAt,
|
||
RePurchaseAuthorizedAt: task.RePurchaseAuthorizedAt, RePurchaseConsumedAt: task.RePurchaseConsumedAt,
|
||
CancelledAt: task.CancelledAt, CancelReason: task.CancelReason, ErrorCode: task.ErrorCode,
|
||
ErrorMessage: task.ErrorMessage, Retryable: retry.Allowed,
|
||
RetryDisabledCode: retry.ReasonCode, RetryDisabledReason: retry.Reason,
|
||
StatusVersion: task.StatusVersion, StatusChangedAt: task.StatusChangedAt,
|
||
CreatedAt: task.CreatedAt, UpdatedAt: task.UpdatedAt,
|
||
}
|
||
if task.DeviceID != nil {
|
||
item.DeviceName = deviceNames[*task.DeviceID]
|
||
}
|
||
return item
|
||
}
|
||
|
||
func validPurchaseStatus(status string) bool {
|
||
switch status {
|
||
case models.PurchaseTaskStatusPending, models.PurchaseTaskStatusSpecProbePending,
|
||
models.PurchaseTaskStatusRunning, models.PurchaseTaskStatusRehearsalCompleted,
|
||
models.PurchaseTaskStatusOrderSubmitStarted, models.PurchaseTaskStatusOrderCreated,
|
||
models.PurchaseTaskStatusOrderResultUnknown, models.PurchaseTaskStatusFailed,
|
||
models.PurchaseTaskStatusCancelled:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func (s *Service) matchingViews(ctx context.Context, tasks []models.PurchaseTask) (map[uint64]MatchingView, error) {
|
||
result := make(map[uint64]MatchingView, len(tasks))
|
||
ids := make([]uint64, 0, len(tasks))
|
||
for _, task := range tasks {
|
||
ids = append(ids, task.ID)
|
||
result[task.ID] = resolvedMatchingView(task)
|
||
}
|
||
if len(ids) == 0 {
|
||
return result, nil
|
||
}
|
||
var works []models.PurchaseSpecMatchWorkItem
|
||
if err := s.DB.WithContext(ctx).Where("purchase_task_id IN ?", ids).Find(&works).Error; err != nil {
|
||
return nil, internal(err)
|
||
}
|
||
for _, work := range works {
|
||
view, err := matchingViewFromWork(work)
|
||
if err != nil {
|
||
return nil, internal(err)
|
||
}
|
||
result[work.PurchaseTaskID] = view
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (s *Service) MatchingDetail(ctx context.Context, taskID uint64) (MatchingView, error) {
|
||
var task models.PurchaseTask
|
||
if err := s.DB.WithContext(ctx).First(&task, taskID).Error; err != nil {
|
||
return MatchingView{}, purchaseNotFound(err)
|
||
}
|
||
var work models.PurchaseSpecMatchWorkItem
|
||
if err := s.DB.WithContext(ctx).Where("purchase_task_id = ?", taskID).First(&work).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return resolvedMatchingView(task), nil
|
||
} else if err != nil {
|
||
return MatchingView{}, internal(err)
|
||
}
|
||
view, err := matchingViewFromWork(work)
|
||
if err != nil {
|
||
return MatchingView{}, internal(err)
|
||
}
|
||
return view, nil
|
||
}
|
||
|
||
func resolvedMatchingView(task models.PurchaseTask) MatchingView {
|
||
if task.SpecSource == "unresolved" {
|
||
return MatchingView{Status: "unresolved", Executable: true, Reason: "任务执行时需要规格探测"}
|
||
}
|
||
return MatchingView{Status: models.PurchaseMatchMatched, Executable: true, Reason: "规格已匹配"}
|
||
}
|
||
|
||
func matchingViewFromWork(work models.PurchaseSpecMatchWorkItem) (MatchingView, error) {
|
||
var snapshot matchFingerprint
|
||
if err := json.Unmarshal([]byte(work.InputSnapshotJSON), &snapshot); err != nil {
|
||
return MatchingView{}, err
|
||
}
|
||
view := MatchingView{Status: work.Status, Executable: work.Status == models.PurchaseMatchMatched, Reason: work.Reason, NextAttemptAt: work.NextAttemptAt, TargetColor: snapshot.TargetColor, TargetSize: snapshot.TargetSize, Colors: snapshot.Colors, Sizes: snapshot.Sizes}
|
||
if work.LastErrorCode != nil {
|
||
view.ReasonCode = *work.LastErrorCode
|
||
}
|
||
switch work.Status {
|
||
case models.PurchaseMatchPending, models.PurchaseMatchRunning:
|
||
view.NextAction = "wait"
|
||
case models.PurchaseMatchRetryWait:
|
||
view.NextAction = "wait_retry"
|
||
case models.PurchaseMatchManualRequired:
|
||
view.NextAction = "manual_or_retry"
|
||
case models.PurchaseMatchCancelled:
|
||
view.NextAction = "requeue"
|
||
}
|
||
return view, nil
|
||
}
|