fix(t243): allow deleting unreferenced procurement drafts
This commit is contained in:
@@ -773,26 +773,6 @@ func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) {
|
||||
createRequest.Body,
|
||||
)
|
||||
}
|
||||
deleteInUse := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodDelete,
|
||||
"/api/v1/freight-orders/"+orderBody.Items[0].ID,
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
)
|
||||
if deleteInUse.Code != http.StatusConflict ||
|
||||
!strings.Contains(
|
||||
deleteInUse.Body.String(),
|
||||
`"code":"FREIGHT_ORDER_IN_USE"`,
|
||||
) {
|
||||
t.Fatalf(
|
||||
"delete in-use status/body = %d / %s",
|
||||
deleteInUse.Code,
|
||||
deleteInUse.Body,
|
||||
)
|
||||
}
|
||||
var requestBody struct {
|
||||
Request struct {
|
||||
ID string `json:"id"`
|
||||
@@ -829,6 +809,26 @@ func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) {
|
||||
!strings.Contains(bind.Body.String(), `"status":"READY"`) {
|
||||
t.Fatalf("bind status/body = %d / %s", bind.Code, bind.Body)
|
||||
}
|
||||
deleteWithReference := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodDelete,
|
||||
"/api/v1/freight-orders/"+orderBody.Items[0].ID,
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
)
|
||||
if deleteWithReference.Code != http.StatusConflict ||
|
||||
!strings.Contains(
|
||||
deleteWithReference.Body.String(),
|
||||
`"code":"FREIGHT_ORDER_HAS_REFERENCE"`,
|
||||
) {
|
||||
t.Fatalf(
|
||||
"delete with reference status/body = %d / %s",
|
||||
deleteWithReference.Code,
|
||||
deleteWithReference.Body,
|
||||
)
|
||||
}
|
||||
createTask := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
@@ -847,6 +847,26 @@ func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) {
|
||||
createTask.Body,
|
||||
)
|
||||
}
|
||||
deleteWithTask := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodDelete,
|
||||
"/api/v1/freight-orders/"+orderBody.Items[0].ID,
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
)
|
||||
if deleteWithTask.Code != http.StatusConflict ||
|
||||
!strings.Contains(
|
||||
deleteWithTask.Body.String(),
|
||||
`"code":"FREIGHT_ORDER_HAS_PURCHASE_TASK"`,
|
||||
) {
|
||||
t.Fatalf(
|
||||
"delete with task status/body = %d / %s",
|
||||
deleteWithTask.Code,
|
||||
deleteWithTask.Body,
|
||||
)
|
||||
}
|
||||
var taskBody struct {
|
||||
Task struct {
|
||||
ID string `json:"id"`
|
||||
@@ -899,6 +919,90 @@ func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminFreightDeleteRemovesNeedsImageDraft(t *testing.T) {
|
||||
fixture := newAdminIntegrationFixture(t)
|
||||
createSync := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodPost,
|
||||
"/api/v1/freight-syncs",
|
||||
"application/json",
|
||||
strings.NewReader(
|
||||
`{"mode":"ORDER_NUMBER","order_number":"DRAFT-DELETE"}`,
|
||||
),
|
||||
"draft-delete-freight-sync",
|
||||
)
|
||||
requireAdminStatus(t, createSync, http.StatusCreated)
|
||||
orders := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodGet,
|
||||
"/api/v1/freight-orders",
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
)
|
||||
var orderBody struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"items"`
|
||||
}
|
||||
decodeResponse(t, orders, &orderBody)
|
||||
detail := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodGet,
|
||||
"/api/v1/freight-orders/"+orderBody.Items[0].ID,
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
)
|
||||
var detailBody struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"items"`
|
||||
}
|
||||
decodeResponse(t, detail, &detailBody)
|
||||
createRequest := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodPost,
|
||||
"/api/v1/freight-items/"+detailBody.Items[0].ID+
|
||||
"/procurement-request",
|
||||
"application/json",
|
||||
strings.NewReader(`{"confirm_procurement_needed":true}`),
|
||||
"",
|
||||
)
|
||||
if createRequest.Code != http.StatusCreated ||
|
||||
!strings.Contains(createRequest.Body.String(), `"status":"NEEDS_IMAGE"`) {
|
||||
t.Fatalf(
|
||||
"create request status/body = %d / %s",
|
||||
createRequest.Code,
|
||||
createRequest.Body,
|
||||
)
|
||||
}
|
||||
deleteDraft := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodDelete,
|
||||
"/api/v1/freight-orders/"+orderBody.Items[0].ID,
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
)
|
||||
requireAdminStatus(t, deleteDraft, http.StatusNoContent)
|
||||
deletedDetail := performAdminRequest(
|
||||
t,
|
||||
fixture.router,
|
||||
http.MethodGet,
|
||||
"/api/v1/freight-orders/"+orderBody.Items[0].ID,
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
)
|
||||
requireAdminStatus(t, deletedDetail, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAdminOrderAuthorizationIsIdempotentAndRevisioned(t *testing.T) {
|
||||
fixture := newAdminIntegrationFixture(t)
|
||||
taskID, executionID, taskHash, firstKey, secondKey :=
|
||||
|
||||
@@ -561,10 +561,17 @@ func (h *Handler) DeleteFreightOrder(ctx *gin.Context) {
|
||||
OrderID: orderID,
|
||||
},
|
||||
)
|
||||
if errors.Is(err, ErrFreightOrderInUse) {
|
||||
if errors.Is(err, ErrFreightOrderHasPurchaseTask) {
|
||||
ctx.Redirect(
|
||||
http.StatusSeeOther,
|
||||
"/freight/"+pathEscape(orderID)+"?notice=delete-conflict",
|
||||
"/freight/"+pathEscape(orderID)+"?notice=delete-task-conflict",
|
||||
)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ErrFreightOrderHasReference) {
|
||||
ctx.Redirect(
|
||||
http.StatusSeeOther,
|
||||
"/freight/"+pathEscape(orderID)+"?notice=delete-reference-conflict",
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -720,8 +727,10 @@ func freightNotice(value string) string {
|
||||
return "ERP 货运单已同步,货运信息和商品明细已更新。"
|
||||
case "deleted":
|
||||
return "货运单已从本系统删除,不影响顺运宝 ERP。"
|
||||
case "delete-conflict":
|
||||
return "该货运单已进入采购流程,不能删除。"
|
||||
case "delete-task-conflict":
|
||||
return "该货运单已生成采购任务,不能删除。"
|
||||
case "delete-reference-conflict":
|
||||
return "该货运单已绑定采购参考图,不能删除。"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1330,7 +1330,11 @@ func TestFreightDeleteRequiresConfirmationAndHandlesConflict(t *testing.T) {
|
||||
)
|
||||
if detail.Code != http.StatusOK ||
|
||||
!strings.Contains(detail.Body.String(), "删除本地货运单") ||
|
||||
!strings.Contains(detail.Body.String(), "data-freight-delete-dialog") {
|
||||
!strings.Contains(detail.Body.String(), "data-freight-delete-dialog") ||
|
||||
!strings.Contains(
|
||||
detail.Body.String(),
|
||||
"未绑定参考图的采购需求草稿",
|
||||
) {
|
||||
t.Fatalf("delete controls status/body = %d / %s", detail.Code, detail.Body)
|
||||
}
|
||||
cookie := csrfCookie(t, detail)
|
||||
@@ -1374,7 +1378,7 @@ func TestFreightDeleteRequiresConfirmationAndHandlesConflict(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
service.deleteErr = ErrFreightOrderInUse
|
||||
service.deleteErr = ErrFreightOrderHasReference
|
||||
request = httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/freight/"+testTaskID+"/delete",
|
||||
@@ -1386,10 +1390,32 @@ func TestFreightDeleteRequiresConfirmationAndHandlesConflict(t *testing.T) {
|
||||
router.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusSeeOther ||
|
||||
response.Header().Get("Location") !=
|
||||
"/freight/"+testTaskID+"?notice=delete-conflict" ||
|
||||
"/freight/"+testTaskID+"?notice=delete-reference-conflict" ||
|
||||
len(logged) != 1 {
|
||||
t.Fatalf(
|
||||
"conflicting delete status/location/log = %d / %q / %#v",
|
||||
"reference conflict status/location/log = %d / %q / %#v",
|
||||
response.Code,
|
||||
response.Header().Get("Location"),
|
||||
logged,
|
||||
)
|
||||
}
|
||||
|
||||
service.deleteErr = ErrFreightOrderHasPurchaseTask
|
||||
request = httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/freight/"+testTaskID+"/delete",
|
||||
strings.NewReader(values.Encode()),
|
||||
)
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.AddCookie(cookie)
|
||||
response = httptest.NewRecorder()
|
||||
router.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusSeeOther ||
|
||||
response.Header().Get("Location") !=
|
||||
"/freight/"+testTaskID+"?notice=delete-task-conflict" ||
|
||||
len(logged) != 1 {
|
||||
t.Fatalf(
|
||||
"task conflict status/location/log = %d / %q / %#v",
|
||||
response.Code,
|
||||
response.Header().Get("Location"),
|
||||
logged,
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
<noscript>
|
||||
<section class="noscript-cancel" aria-labelledby="freight-delete-noscript-title">
|
||||
<h2 id="freight-delete-noscript-title">删除本地货运单</h2>
|
||||
<p>只删除本系统保存的信息,不影响顺运宝 ERP;已进入采购流程的货运单不能删除。</p>
|
||||
<p>只删除本系统保存的信息,不影响顺运宝 ERP;未绑定参考图的采购需求草稿会一并删除,已绑定参考图或已生成任务时系统拒绝删除。</p>
|
||||
<form method="post" action="/freight/{{pathPart .Detail.Order.ID}}/delete">
|
||||
<input type="hidden" name="csrf_token" value="{{.Page.CSRFToken}}">
|
||||
<label class="confirm-line">
|
||||
@@ -152,7 +152,7 @@
|
||||
<dialog class="confirm-dialog" data-freight-delete-dialog
|
||||
aria-labelledby="freight-delete-title">
|
||||
<h2 id="freight-delete-title">删除本地货运单?</h2>
|
||||
<p>将删除本系统保存的 {{if .Detail.Order.SourceCode}}{{.Detail.Order.SourceCode}}{{else}}该货运单{{end}} 及商品图片,不影响顺运宝 ERP。已进入采购流程时系统会拒绝删除。</p>
|
||||
<p>将删除本系统保存的 {{if .Detail.Order.SourceCode}}{{.Detail.Order.SourceCode}}{{else}}该货运单{{end}}、商品图片及未绑定参考图的采购需求草稿,不影响顺运宝 ERP。已绑定参考图或已生成任务时系统拒绝删除。</p>
|
||||
<div class="dialog-actions">
|
||||
<button class="button" type="button"
|
||||
data-keep-freight-order>保留货运单</button>
|
||||
|
||||
@@ -8,23 +8,28 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrForbidden = errors.New("resource forbidden")
|
||||
ErrConflict = errors.New("resource conflict")
|
||||
ErrValidation = errors.New("validation failed")
|
||||
ErrInvalidFile = errors.New("invalid file")
|
||||
ErrUnavailable = errors.New("service unavailable")
|
||||
ErrERPNotConfigured = errors.New("ERP is not configured")
|
||||
ErrERPSessionNeeded = errors.New("ERP session is required")
|
||||
ErrERPCaptchaInvalid = errors.New("ERP captcha is invalid")
|
||||
ErrERPLoginRejected = errors.New("ERP login was rejected")
|
||||
ErrERPProtocol = errors.New("ERP protocol is invalid")
|
||||
ErrERPUnavailable = errors.New("ERP is unavailable")
|
||||
ErrERPFreightNotFound = errors.New("ERP freight order was not found")
|
||||
ErrOCRServiceInvalid = errors.New("OCR service is invalid")
|
||||
ErrFreightSyncBusy = errors.New("freight sync is busy")
|
||||
ErrFreightSyncTimeout = errors.New("freight sync timed out")
|
||||
ErrFreightOrderInUse = errors.New("freight order is in use")
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrForbidden = errors.New("resource forbidden")
|
||||
ErrConflict = errors.New("resource conflict")
|
||||
ErrValidation = errors.New("validation failed")
|
||||
ErrInvalidFile = errors.New("invalid file")
|
||||
ErrUnavailable = errors.New("service unavailable")
|
||||
ErrERPNotConfigured = errors.New("ERP is not configured")
|
||||
ErrERPSessionNeeded = errors.New("ERP session is required")
|
||||
ErrERPCaptchaInvalid = errors.New("ERP captcha is invalid")
|
||||
ErrERPLoginRejected = errors.New("ERP login was rejected")
|
||||
ErrERPProtocol = errors.New("ERP protocol is invalid")
|
||||
ErrERPUnavailable = errors.New("ERP is unavailable")
|
||||
ErrERPFreightNotFound = errors.New("ERP freight order was not found")
|
||||
ErrOCRServiceInvalid = errors.New("OCR service is invalid")
|
||||
ErrFreightSyncBusy = errors.New("freight sync is busy")
|
||||
ErrFreightSyncTimeout = errors.New("freight sync timed out")
|
||||
ErrFreightOrderHasReference = errors.New(
|
||||
"freight order has a procurement reference",
|
||||
)
|
||||
ErrFreightOrderHasPurchaseTask = errors.New(
|
||||
"freight order has a purchase task",
|
||||
)
|
||||
)
|
||||
|
||||
// Service is the application boundary required by the server-rendered admin UI.
|
||||
|
||||
@@ -309,7 +309,7 @@ func procurementStatusLabel(
|
||||
case domain.ProcurementBlocked:
|
||||
return "资料阻塞"
|
||||
case domain.ProcurementNeedsImage:
|
||||
return "需要参考图"
|
||||
return "需要参考图,尚未生成任务"
|
||||
case domain.ProcurementReady:
|
||||
return "可以生成任务"
|
||||
case domain.ProcurementTaskCreated:
|
||||
@@ -954,8 +954,12 @@ func mapUsecaseError(err error) error {
|
||||
return err
|
||||
}
|
||||
var public error
|
||||
if typed.Code == "FREIGHT_ORDER_IN_USE" {
|
||||
public = ErrFreightOrderInUse
|
||||
if typed.Code == "FREIGHT_ORDER_HAS_PURCHASE_TASK" {
|
||||
public = ErrFreightOrderHasPurchaseTask
|
||||
return &adapterError{public: public, cause: err}
|
||||
}
|
||||
if typed.Code == "FREIGHT_ORDER_HAS_REFERENCE" {
|
||||
public = ErrFreightOrderHasReference
|
||||
return &adapterError{public: public, cause: err}
|
||||
}
|
||||
switch typed.Kind {
|
||||
|
||||
Reference in New Issue
Block a user