diff --git a/.phpunit.result.cache b/.phpunit.result.cache
index dab5692..9adf4d4 100644
--- a/.phpunit.result.cache
+++ b/.phpunit.result.cache
@@ -1 +1 @@
-{"version":2,"defects":{"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":8,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":8,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":7,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":8,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":8,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":8,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":8,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":8,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":8,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":7,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":7,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":8,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":7,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":8,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":8,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":7,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":7,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":7,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":5,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":7,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":7,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":8,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":8,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":7,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":8,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":8,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":8,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":8,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":8,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":8,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":8,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":8,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":7,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":7},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.003,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.015,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.01,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.007,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.007,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.008,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.014,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.009,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.012,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.025,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.019,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.022,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.081,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.021,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.026,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.017,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.029,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.032,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.043,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.039,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.031,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.012,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.018,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.016,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.002,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.005,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.001,"Tests\\Feature\\MiniAuthTest::test_sys_token_cannot_access_mini":0.005,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.004,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.054,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.018,"Tests\\Feature\\ExportTest::test_export_pdf":1.303,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.018,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.018,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.003,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.008,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.008,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.006,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.007,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.006,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.01,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.012,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.005,"Tests\\Feature\\CartTest::test_update_quantity":0.008,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.008,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.006,"Tests\\Feature\\CartTest::test_delete_item":0.008,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.015,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.008,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.007,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.009,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.012,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.006,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.011,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.013,"Tests\\Feature\\PurchaseGenerateTest::test_generate_uses_min_actual_price_across_types":0.015,"Tests\\Feature\\ProductPriceTest::test_store_without_level_sees_products_without_price":0.01,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.012,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.009,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.01,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.002,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.003,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.005,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.013,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.041,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.02,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.015,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.018,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.025,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.048,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.011,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.015,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.014,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.016,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.012,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.013,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.013,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.012,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.017}}
\ No newline at end of file
+{"version":2,"defects":{"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":8,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":8,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":7,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":8,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":8,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":8,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":8,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":8,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":8,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":7,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":7,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":8,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":7,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":8,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":8,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":7,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":7,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":7,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":5,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":7,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":7,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":8,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":8,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":7,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":8,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":8,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":8,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":8,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":8,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":8,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":8,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":8,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":7,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":7},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.003,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.018,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.01,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.013,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.008,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.007,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.01,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.016,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.014,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.011,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.033,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.03,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.076,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.022,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.028,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.021,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.045,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.032,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.035,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.034,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.043,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.016,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.026,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.019,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.002,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.005,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.001,"Tests\\Feature\\MiniAuthTest::test_sys_token_cannot_access_mini":0.005,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.003,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.069,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.019,"Tests\\Feature\\ExportTest::test_export_pdf":1.302,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.019,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.019,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.003,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.009,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.009,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.007,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.007,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.006,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.01,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.014,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.005,"Tests\\Feature\\CartTest::test_update_quantity":0.008,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.01,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.01,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.007,"Tests\\Feature\\CartTest::test_delete_item":0.009,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.011,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.017,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.009,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.007,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.01,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.01,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.011,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.02,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.013,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.016,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.007,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.01,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_uses_min_actual_price_across_types":0.014,"Tests\\Feature\\ProductPriceTest::test_store_without_level_sees_products_without_price":0.009,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.014,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.022,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.039,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.005,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.003,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.005,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.006,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.007,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.016,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.018,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.014,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.012,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.016,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.05,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.008,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.015,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.017,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.021,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.015,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.011,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.014,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.014,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.014,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.017,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.023,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.042,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.014}}
\ No newline at end of file
diff --git a/app/Http/Controllers/Order/StoreOrderController.php b/app/Http/Controllers/Order/StoreOrderController.php
index fae22ec..38961d2 100644
--- a/app/Http/Controllers/Order/StoreOrderController.php
+++ b/app/Http/Controllers/Order/StoreOrderController.php
@@ -12,6 +12,7 @@ use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
+use Modules\AnnoRoute\Attribute\DeleteRoute;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
@@ -400,6 +401,27 @@ class StoreOrderController extends BaseController
return $this->success(['success' => $orders->count()]);
}
+ /**
+ * 删除订单(软删除):仅已取消订单允许删除;删除后后台列表/详情、小程序端均不可见
+ */
+ #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
+ public function delete(int $id): JsonResponse
+ {
+ $order = StoreOrderModel::find($id);
+ if (empty($order)) {
+ throw new RepositoryException('订单不存在');
+ }
+ if ($order->status !== StoreOrderModel::STATUS_CANCELLED) {
+ throw new RepositoryException(
+ '订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,仅已取消订单可删除'
+ );
+ }
+
+ $order->delete();
+
+ return $this->success();
+ }
+
/**
* 状态流转合法路径(与迁移状态定义一致):
* 待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
diff --git a/app/Models/StoreOrderModel.php b/app/Models/StoreOrderModel.php
index 7e246da..2bdbcfc 100644
--- a/app/Models/StoreOrderModel.php
+++ b/app/Models/StoreOrderModel.php
@@ -6,13 +6,14 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\SoftDeletes;
/**
- * 门店订单模型(小程序下单,快照等级价)
+ * 门店订单模型(小程序下单,快照等级价;软删除,仅已取消订单可由后台删除)
*/
class StoreOrderModel extends Model
{
- use HasFactory;
+ use HasFactory, SoftDeletes;
/** 状态:待接单(可被采购单生成归集、可取消) */
public const int STATUS_PENDING = 0;
diff --git a/app/Services/PurchaseAllocateService.php b/app/Services/PurchaseAllocateService.php
index b007bc5..73047b4 100644
--- a/app/Services/PurchaseAllocateService.php
+++ b/app/Services/PurchaseAllocateService.php
@@ -41,6 +41,7 @@ class PurchaseAllocateService
$orderItems = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order.purchase_id', $purchase->id)
+ ->whereNull('store_order.deleted_at')
->select('store_order_item.*')
->get()
->groupBy('product_id');
diff --git a/app/Services/StatementGenerateService.php b/app/Services/StatementGenerateService.php
index 05a5435..a20d268 100644
--- a/app/Services/StatementGenerateService.php
+++ b/app/Services/StatementGenerateService.php
@@ -39,6 +39,7 @@ class StatementGenerateService
->whereDate('store_order.order_date', '>=', $periodStart)
->whereDate('store_order.order_date', '<=', $periodEnd)
->where('store_order.status', '<>', StoreOrderModel::STATUS_CANCELLED)
+ ->whereNull('store_order.deleted_at')
->select('store_order_item.*')
->get();
diff --git a/database/migrations/2026_07_23_030610_create_store_order_table.php b/database/migrations/2026_07_23_030610_create_store_order_table.php
index 1749edf..2c22930 100644
--- a/database/migrations/2026_07_23_030610_create_store_order_table.php
+++ b/database/migrations/2026_07_23_030610_create_store_order_table.php
@@ -30,6 +30,7 @@ return new class extends Migration
$table->decimal('tray_num', 10, 2)->default(0)->comment('周转托盘数量');
$table->integer('status')->default(0)->comment('订单状态(0待接单 1已接单 2采购中 3配送中 4已完成 9已取消)');
$table->string('remark', 255)->default('')->comment('订单备注');
+ $table->softDeletes();
$table->timestamps();
$table->index(['store_id', 'order_date'], 'store_order_store_date_index');
$table->index(['status'], 'store_order_status_index');
diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php
index f357dff..8452bc8 100644
--- a/database/seeders/PermissionSeeder.php
+++ b/database/seeders/PermissionSeeder.php
@@ -173,6 +173,7 @@ class PermissionSeeder extends Seeder
'children' => [
['type' => 'rule', 'key' => 'order.store.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'order.store.update', 'name' => '状态流转'],
+ ['type' => 'rule', 'key' => 'order.store.delete', 'name' => '删除(仅已取消订单)'],
],
],
],
diff --git a/tests/Feature/StoreOrderTest.php b/tests/Feature/StoreOrderTest.php
index 5e8f3ef..18d6602 100644
--- a/tests/Feature/StoreOrderTest.php
+++ b/tests/Feature/StoreOrderTest.php
@@ -250,4 +250,57 @@ class StoreOrderTest extends ProcurementTestCase
->assertJsonPath('success', false)
->assertJsonPath('msg', '周转框数量不能小于 0');
}
+
+ /** 软删除:仅已取消订单可由后台删除,删除后后台/小程序端均不可见 */
+ public function test_soft_delete_only_cancelled_orders(): void
+ {
+ $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_CANCELLED);
+
+ // 删除前小程序端可见
+ $this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 1);
+
+ $this->actingAsSysUser();
+ $this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
+
+ $this->assertNotNull($order->fresh()->deleted_at, '软删除应写入 deleted_at');
+ $this->assertNull(StoreOrderModel::find($order->id), '默认查询不可见软删除订单');
+ $this->assertNotNull(StoreOrderModel::withTrashed()->find($order->id), '数据仍保留在库中');
+
+ // 后台列表/详情不可见
+ $this->getJson('/order/store')->assertOk()->assertJsonPath('data.total', 0);
+ $this->getJson("/order/store/{$order->id}")->assertJsonPath('success', false);
+
+ // 小程序端历史订单同样不可见
+ $this->actingAsMiniUser(UserModel::where('store_id', $order->store_id)->first());
+ $this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 0);
+ $this->getJson("/mini/order/{$order->id}")->assertJsonPath('success', false);
+ }
+
+ /** 软删除:非已取消订单拒绝删除 */
+ public function test_soft_delete_rejected_for_non_cancelled_orders(): void
+ {
+ foreach ([
+ StoreOrderModel::STATUS_PENDING,
+ StoreOrderModel::STATUS_SUMMARIZED,
+ StoreOrderModel::STATUS_DELIVERING,
+ StoreOrderModel::STATUS_DISTRIBUTION,
+ StoreOrderModel::STATUS_COMPLETED,
+ ] as $status) {
+ $order = $this->makeOrderWithStatus($status);
+ $this->actingAsSysUser();
+ $this->deleteJson("/order/store/{$order->id}")
+ ->assertOk()->assertJsonPath('success', false);
+ $this->assertNull($order->fresh()->deleted_at, '非已取消订单不得删除');
+ }
+ }
+
+ /** 软删除:重复删除返回订单不存在 */
+ public function test_soft_delete_twice_rejected(): void
+ {
+ $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_CANCELLED);
+ $this->actingAsSysUser();
+
+ $this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
+ $this->deleteJson("/order/store/{$order->id}")->assertJsonPath('success', false);
+ }
}
diff --git a/web/api/order/store.ts b/web/api/order/store.ts
index dfec2ca..562b1c2 100644
--- a/web/api/order/store.ts
+++ b/web/api/order/store.ts
@@ -61,3 +61,11 @@ export async function syncOrderItem(itemId: number) {
method: 'put',
});
}
+
+/** 删除订单(软删除,仅已取消订单允许) */
+export async function deleteStoreOrder(id: number) {
+ return createAxios({
+ url: `/order/store/${id}`,
+ method: 'delete',
+ });
+}
diff --git a/web/pages/order/store.tsx b/web/pages/order/store.tsx
index ab48ae1..f3d80c2 100644
--- a/web/pages/order/store.tsx
+++ b/web/pages/order/store.tsx
@@ -35,6 +35,7 @@ import {
batchUpdateOrderStatus,
updateOrderItem,
syncOrderItem,
+ deleteStoreOrder,
} from '@/api/order/store.ts';
import { generatePurchase } from '@/api/purchase/order.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
@@ -42,7 +43,7 @@ import { getSupplierOptions } from '@/api/customer/supplier.ts';
import type IStore from '@/domain/iStore.ts';
import type ISupplier from '@/domain/iSupplier.ts';
import AuthButton from '@/components/AuthButton';
-import { EditOutlined, SettingOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
+import { DeleteOutlined, EditOutlined, SettingOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
import TextArea from "antd/es/input/TextArea";
const { Title, Text } = Typography;
@@ -224,6 +225,14 @@ const StoreOrderPage: React.FC = () => {
await tableRef.current?.reload();
};
+ /** 删除订单(软删除,仅已取消订单可删;删除后后台/小程序端均不可见) */
+ const handleDelete = async (id: number) => {
+ await deleteStoreOrder(id);
+ message.success('订单已删除');
+ setDetailOpen(false);
+ await tableRef.current?.reload();
+ };
+
// 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额
const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0);
const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0);
@@ -338,7 +347,7 @@ const StoreOrderPage: React.FC = () => {
align: 'center',
},
{
- title: '订单信息',
+ title: '附加信息',
hideInForm: true,
dataIndex: 'status',
hideInSearch: true,
@@ -361,7 +370,7 @@ const StoreOrderPage: React.FC = () => {
)
},
{
- title: '附加信息',
+ title: '订单金额',
hideInForm: true,
dataIndex: 'box_num',
hideInSearch: true,
@@ -455,6 +464,17 @@ const StoreOrderPage: React.FC = () => {
)}
+ { record.status === 9 && (
+
+ handleDelete(record.id!)}
+ >
+ } color='danger' variant="solid" />
+
+
+ )}
)
}
@@ -542,6 +562,19 @@ const StoreOrderPage: React.FC = () => {
)}
+ { detail.status === 9 && (
+
+ handleDelete(detail.id!)}
+ >
+ } color='danger' variant="solid">
+ 删除
+
+
+
+ )}
) : null
}