diff --git a/.phpunit.result.cache b/.phpunit.result.cache index f75f386..50cfe46 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,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":7,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":7,"Tests\\Feature\\ExportTest::test_export_pdf":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":7,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":7,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":7,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":8,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":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.008,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.011,"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.006,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.01,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.009,"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.032,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.019,"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.019,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.022,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.019,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.02,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.036,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.011,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.013,"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.003,"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.072,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.018,"Tests\\Feature\\ExportTest::test_export_pdf":0.015,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.016,"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.045,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.009,"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.01,"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.011,"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.007,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.008,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.013,"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.008,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.006,"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.011,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.009,"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.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.008,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.008,"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.004,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.009,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.008,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.009,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.002,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.002,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.006,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.005,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.012,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.022,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.02,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.012,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.021,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.008,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.01,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.014,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.012,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.011,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.01,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.011,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.012,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.01,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.016,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.019,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.041,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.012,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.019,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.032,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.02,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.018,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.016,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.026,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.021,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.019,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.021,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.012,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.018,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.02,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.017,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.02,"Tests\\Feature\\ExportTest::test_export_invalid_type_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_missing_purchase_rejected":0.006,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_container_summary":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_container_syncs_order_amounts":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejected_when_purchase_completed":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejects_incomplete_or_foreign_orders":0.026,"Tests\\Feature\\PurchaseEditTest::test_update_container_validates_negative_and_duplicate_orders":0.018,"Tests\\Feature\\ProductCategoryTest::test_create_child_category_under_top_level":0.044,"Tests\\Feature\\ProductCategoryTest::test_create_category_under_second_level_rejected":0.008,"Tests\\Feature\\ProductCategoryTest::test_update_second_level_move_to_another_top":0.007,"Tests\\Feature\\ProductCategoryTest::test_update_parent_with_children_cannot_move_under_category":0.006,"Tests\\Feature\\ProductCategoryTest::test_update_with_missing_parent_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_parent_category_rejected":0.009,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":0.007,"Tests\\Feature\\ProductCategoryTest::test_mini_categories_returns_all_enabled_categories":0.007}} \ 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,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":7,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":7,"Tests\\Feature\\ExportTest::test_export_pdf":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":7,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":7,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":7,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":8,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":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.008,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.011,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.007,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.01,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.007,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.019,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.01,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.009,"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.018,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.02,"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.019,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.022,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.019,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.02,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.036,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.011,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.013,"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.003,"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.066,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.02,"Tests\\Feature\\ExportTest::test_export_pdf":0.015,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.015,"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.045,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.009,"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.01,"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.011,"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.007,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.008,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.013,"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.008,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.006,"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.011,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.009,"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.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.008,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.008,"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.004,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.009,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.008,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.009,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.002,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.002,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.006,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.005,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.012,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.022,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.02,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.021,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.008,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.01,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.014,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.012,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.011,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.01,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.011,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.012,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.01,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.016,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.021,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.042,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.013,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.019,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.032,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.02,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.019,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.016,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.026,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.021,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.019,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.021,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.012,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.018,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.02,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.017,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.02,"Tests\\Feature\\ExportTest::test_export_invalid_type_rejected":0.017,"Tests\\Feature\\ExportTest::test_export_missing_purchase_rejected":0.006,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_container_summary":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_container_syncs_order_amounts":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejected_when_purchase_completed":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejects_incomplete_or_foreign_orders":0.026,"Tests\\Feature\\PurchaseEditTest::test_update_container_validates_negative_and_duplicate_orders":0.018,"Tests\\Feature\\ProductCategoryTest::test_create_child_category_under_top_level":0.044,"Tests\\Feature\\ProductCategoryTest::test_create_category_under_second_level_rejected":0.008,"Tests\\Feature\\ProductCategoryTest::test_update_second_level_move_to_another_top":0.007,"Tests\\Feature\\ProductCategoryTest::test_update_parent_with_children_cannot_move_under_category":0.006,"Tests\\Feature\\ProductCategoryTest::test_update_with_missing_parent_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_parent_category_rejected":0.009,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":0.007,"Tests\\Feature\\ProductCategoryTest::test_mini_categories_returns_all_enabled_categories":0.007,"Tests\\Feature\\MiniBillTest::test_bill_list_derives_pay_state_and_settlement_date":0.059,"Tests\\Feature\\MiniBillTest::test_bill_list_summary_aggregates_unpaid":0.008,"Tests\\Feature\\MiniBillTest::test_bill_list_payable_filter":0.008,"Tests\\Feature\\MiniBillTest::test_bill_list_filters_by_bill_date_range":0.012,"Tests\\Feature\\MiniBillTest::test_bill_data_isolated_between_stores":0.009,"Tests\\Feature\\MiniBillTest::test_bill_detail_outputs_bill_items_and_orders":0.01,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":0.014,"Tests\\Feature\\StoreOrderTest::test_order_list_filters_by_order_date_range":0.02,"Tests\\Feature\\StoreOrderTest::test_order_list_rejects_invalid_filters":0.009}} \ No newline at end of file diff --git a/app/Http/Controllers/Mini/BillController.php b/app/Http/Controllers/Mini/BillController.php index 3910c3a..45679d9 100644 --- a/app/Http/Controllers/Mini/BillController.php +++ b/app/Http/Controllers/Mini/BillController.php @@ -16,24 +16,70 @@ use Modules\AnnoRoute\Attribute\RequestAttribute; #[RequestAttribute('/mini', 'mini', authGuard: 'users')] class BillController extends BaseMiniController { - /** 账单列表:当前门店强制过滤,?status= 按支付状态筛选(0未支付 1已支付) */ + /** + * 账单列表:当前门店强制过滤 + * ?status= 支付状态(0未支付 1已支付);?payable=1 仅可发起付款(未支付且未在审核中,供合并付款选择页); + * ?start_date=&end_date= 账单日期区间;?page=&pageSize= 分页(pageSize 上限 50) + * 响应附加 summary:待支付笔数/金额(仅按门店口径,不受列表筛选影响) + */ #[GetRoute('/bill', authorize: true)] public function index(Request $request): JsonResponse { + $params = $request->validate([ + 'status' => 'nullable|integer|in:0,1', + 'payable' => 'nullable|boolean', + 'start_date' => 'nullable|date_format:Y-m-d', + 'end_date' => 'nullable|date_format:Y-m-d|after_or_equal:start_date', + 'page' => 'nullable|integer|min:1', + 'pageSize' => 'nullable|integer|min:1|max:50', + ], [ + 'status.in' => '支付状态不正确', + 'start_date.date_format' => '开始日期格式为 Y-m-d', + 'end_date.date_format' => '结束日期格式为 Y-m-d', + 'end_date.after_or_equal' => '结束日期不能早于开始日期', + 'pageSize.max' => '每页数量最大 50', + ]); + $user = $this->currentUser($request); $store = $this->ensureStoreBound($user); $query = BillModel::query() ->where('store_id', $store->id) ->with('purchase:id,purchase_no,purchase_date'); - if ($request->filled('status')) { - $query->where('status', (int) $request->input('status')); + if (isset($params['status'])) { + $query->where('status', (int) $params['status']); + } + if ($request->boolean('payable')) { + $query->where('status', BillModel::STATUS_UNPAID)->where('payment_id', 0); + } + if (! empty($params['start_date'])) { + $query->whereDate('bill_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->whereDate('bill_date', '<=', $params['end_date']); } - $data = $query->orderBy('bill_date', 'desc') + $paginator = $query->orderBy('bill_date', 'desc') ->orderBy('id', 'desc') - ->paginate((int) $request->input('pageSize', 10)) - ->toArray(); + ->paginate((int) ($params['pageSize'] ?? 10)); + + $cycleDays = (int) $store->payment_cycle_days; + $paginator->getCollection()->transform( + fn (BillModel $bill): array => $this->formatBill($bill, $cycleDays) + ); + + // 待支付汇总(合并付款入口的头部统计) + $unpaid = BillModel::query() + ->where('store_id', $store->id) + ->where('status', BillModel::STATUS_UNPAID) + ->selectRaw('COUNT(*) as aggregate_count, COALESCE(SUM(total_amount), 0) as aggregate_amount') + ->first(); + + $data = $paginator->toArray(); + $data['summary'] = [ + 'unpaid_count' => (int) $unpaid->aggregate_count, + 'unpaid_amount' => bcadd((string) $unpaid->aggregate_amount, '0', 2), + ]; return $this->success($data); } @@ -58,9 +104,48 @@ class BillController extends BaseMiniController ->toArray(); return $this->success([ - 'bill' => $bill->toArray(), + 'bill' => $this->formatBill($bill, (int) $store->payment_cycle_days), 'items' => app(BillDetailService::class)->mergedItems($bill), 'orders' => $orders, ]); } + + /** + * 账单输出格式化(列表行与详情共用) + * 推导支付进度(待支付/审核中/已支付),按门店回款周期计算应结算日期; + * 剔除收款操作人等后台字段 + * + * @return array + */ + private function formatBill(BillModel $bill, int $cycleDays): array + { + $payState = $bill->payState(); + + return [ + 'id' => $bill->id, + 'bill_no' => $bill->bill_no, + 'bill_date' => $bill->bill_date->toDateString(), + 'purchase_id' => $bill->purchase_id, + 'purchase' => $bill->purchase?->toArray(), + 'product_amount' => $bill->product_amount, + 'delivery_fee' => $bill->delivery_fee, + 'box_num' => $bill->box_num, + 'tray_num' => $bill->tray_num, + 'box_price' => $bill->box_price, + 'tray_price' => $bill->tray_price, + 'added_amount' => $bill->added_amount, + 'total_amount' => $bill->total_amount, + 'status' => $bill->status, + 'status_name' => BillModel::STATUS_NAMES[$bill->status] ?? '', + 'pay_state' => $payState, + 'pay_state_name' => BillModel::PAY_STATE_NAMES[$payState], + 'can_pay' => $payState === BillModel::PAY_STATE_UNPAID, + 'payment_id' => $bill->payment_id, + 'settlement_date' => $bill->bill_date->copy()->addDays($cycleDays)->toDateString(), + 'paid_at' => $bill->paid_at?->toDateTimeString(), + 'pay_remark' => $bill->pay_remark, + 'remark' => $bill->remark, + 'created_at' => $bill->created_at?->toDateTimeString(), + ]; + } } diff --git a/app/Http/Controllers/Mini/OrderController.php b/app/Http/Controllers/Mini/OrderController.php index ddece83..17f381d 100644 --- a/app/Http/Controllers/Mini/OrderController.php +++ b/app/Http/Controllers/Mini/OrderController.php @@ -128,24 +128,81 @@ class OrderController extends BaseMiniController ], '下单成功'); } - /** 历史订单:当前门店强制过滤,?status=&page=&pageSize= */ + /** + * 历史订单:当前门店强制过滤 + * ?status= 状态筛选;?start_date=&end_date= 订货日期区间(配合 /order/summary 周期下钻); + * ?page=&pageSize= 分页(pageSize 上限 50) + * 行数据附带 status_name / can_cancel / 商品预览(前 3 条明细)/ item_count,完整明细走详情接口 + */ #[GetRoute('/order', authorize: true)] public function index(Request $request): JsonResponse { + $params = $request->validate([ + 'status' => 'nullable|integer|in:0,1,2,3,4,9', + 'start_date' => 'nullable|date_format:Y-m-d', + 'end_date' => 'nullable|date_format:Y-m-d|after_or_equal:start_date', + 'page' => 'nullable|integer|min:1', + 'pageSize' => 'nullable|integer|min:1|max:50', + ], [ + 'status.in' => '订单状态不正确', + 'start_date.date_format' => '开始日期格式为 Y-m-d', + 'end_date.date_format' => '结束日期格式为 Y-m-d', + 'end_date.after_or_equal' => '结束日期不能早于开始日期', + 'pageSize.max' => '每页数量最大 50', + ]); + $user = $this->currentUser($request); $store = $this->ensureStoreBound($user); - $query = StoreOrderModel::query()->where('store_id', $store->id); - if ($request->filled('status')) { - $query->where('status', (int) $request->input('status')); + $query = StoreOrderModel::query() + ->where('store_id', $store->id) + ->select([ + 'id', 'order_no', 'order_date', 'total_quantity', 'total_weight', + 'total_amount', 'status', 'remark', 'purchase_id', 'bill_id', 'created_at', + ]) + ->with(['items' => static fn ($itemsQuery) => $itemsQuery + ->select(['id', 'order_id', 'product_name', 'quantity', 'unit']) + ->orderBy('id')]); + if (isset($params['status'])) { + $query->where('status', (int) $params['status']); + } + if (! empty($params['start_date'])) { + $query->whereDate('order_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->whereDate('order_date', '<=', $params['end_date']); } - $data = $query->orderBy('order_date', 'desc') + $paginator = $query->orderBy('order_date', 'desc') ->orderBy('id', 'desc') - ->paginate((int) $request->input('pageSize', 10)) - ->toArray(); + ->paginate((int) ($params['pageSize'] ?? 10)); - return $this->success($data); + $paginator->getCollection()->transform( + static fn (StoreOrderModel $order): array => [ + 'id' => $order->id, + 'order_no' => $order->order_no, + 'order_date' => $order->order_date->toDateString(), + 'status' => $order->status, + 'status_name' => StoreOrderModel::STATUS_NAMES[$order->status] ?? '', + 'can_cancel' => $order->status === StoreOrderModel::STATUS_PENDING, + 'total_quantity' => $order->total_quantity, + 'total_weight' => $order->total_weight, + 'total_amount' => $order->total_amount, + 'remark' => $order->remark, + 'purchase_id' => $order->purchase_id, + 'bill_id' => $order->bill_id, + 'created_at' => $order->created_at?->toDateTimeString(), + 'item_count' => $order->items->count(), + 'items' => $order->items->take(3) + ->map(static fn (StoreOrderItemModel $item): array => [ + 'product_name' => $item->product_name, + 'quantity' => $item->quantity, + 'unit' => $item->unit, + ])->values()->all(), + ] + ); + + return $this->success($paginator->toArray()); } /** diff --git a/app/Models/BillModel.php b/app/Models/BillModel.php index 0439261..484fcbf 100644 --- a/app/Models/BillModel.php +++ b/app/Models/BillModel.php @@ -26,6 +26,32 @@ class BillModel extends Model self::STATUS_PAID => '已支付', ]; + /** 支付进度(小程序端):待支付(可发起合并付款) */ + public const int PAY_STATE_UNPAID = 0; + /** 支付进度:审核中(已提交合并付款凭证,待后台审核;审核拒绝后释放回待支付) */ + public const int PAY_STATE_REVIEWING = 1; + /** 支付进度:已支付 */ + public const int PAY_STATE_PAID = 2; + + /** 支付进度中文名 */ + public const array PAY_STATE_NAMES = [ + self::PAY_STATE_UNPAID => '待支付', + self::PAY_STATE_REVIEWING => '审核中', + self::PAY_STATE_PAID => '已支付', + ]; + + /** + * 支付进度推导:已支付 > 审核中(payment_id 锁定中)> 待支付 + * (支付审核拒绝后 payment_id 释放为 0,回到待支付;线下收款直接置已支付) + */ + public function payState(): int + { + if ($this->status === self::STATUS_PAID) { + return self::PAY_STATE_PAID; + } + return $this->payment_id > 0 ? self::PAY_STATE_REVIEWING : self::PAY_STATE_UNPAID; + } + protected $table = 'bill'; protected $primaryKey = 'id'; diff --git a/docs/mini-home-api.md b/docs/mini-home-api.md deleted file mode 100644 index e00d2fd..0000000 --- a/docs/mini-home-api.md +++ /dev/null @@ -1,172 +0,0 @@ -# 小程序首页接口文档 - -> 适用端:微信小程序(h5 仓库,Taro) -> 后台配置入口:PC 后台「客户端配置」菜单(首页轮播图 / 宫格导航 / 促销推荐卡片) -> 更新日期:2026-08-14 - -## 通用约定 - -| 项 | 说明 | -|---|---| -| 根地址 | `BASE_URL`(见 `src/utils/request.ts`,如 `http://localhost:8000/index.php`) | -| 认证 | 请求头 `Authorization: Bearer {token}`,token 来自登录接口,本地存储 key `auth_token` | -| 响应包络 | `{ success: boolean, data: T, msg?: string, showType?: number }` | -| 失败处理 | `success=false` 时 `msg` 为中文错误信息;HTTP 401 表示登录过期,需重新登录 | - -## GET /mini/home - -首页配置聚合接口:一次返回轮播图、宫格导航、促销推荐卡片三组数据,均为**启用状态(status=1)**且按 `sort` 升序(越小越靠前)。 - -- **权限**:需登录(Bearer token) -- **请求参数**:无 - -### 响应示例 - -```json -{ - "success": true, - "msg": "ok", - "data": { - "banners": [ - { - "id": 1, - "title": "新鲜直采", - "image_id": 123, - "link": "/pages/goods/detail?id=1", - "sort": 0, - "image_url": "http://localhost:8000/storage/uploads/2026/08/14/xxx.jpg" - } - ], - "navs": [ - { - "id": 1, - "name": "蔬菜专区", - "image_id": 124, - "link": "/pages/category/index?id=1", - "sort": 0, - "image_url": "http://localhost:8000/storage/uploads/2026/08/14/yyy.png" - } - ], - "promos": [ - { - "id": 1, - "title": "限时特惠", - "sub_title": "全场 8 折起", - "image_id": 125, - "link": "/pages/promo/detail?id=1", - "sort": 0, - "image_url": "http://localhost:8000/storage/uploads/2026/08/14/zzz.jpg" - } - ] - } -} -``` - -### 字段说明 - -**banners(轮播图)** - -| 字段 | 类型 | 说明 | -|---|---|---| -| id | number | 轮播图 ID | -| title | string | 标题(后台维护,可用于无障碍/占位) | -| image_id | number | 图片文件 ID(sys_file),一般无需使用 | -| **image_url** | string \| null | 轮播图片完整 URL,直接用于 ``;未传图时为 null | -| link | string | 小程序页面跳转路径,**空字符串表示点击不跳转** | -| sort | number | 排序值(已按此升序返回,前端无需再排) | - -**navs(宫格导航)** - -| 字段 | 类型 | 说明 | -|---|---|---| -| id | number | 导航 ID | -| name | string | 导航名称(宫格文字) | -| **image_url** | string \| null | 导航图标完整 URL | -| link | string | 小程序页面跳转路径,空字符串不跳转 | -| sort | number | 排序值 | - -**promos(促销推荐卡片)** - -| 字段 | 类型 | 说明 | -|---|---|---| -| id | number | 卡片 ID | -| title | string | 卡片标题 | -| sub_title | string | 副标题/促销文案,可能为空字符串 | -| **image_url** | string \| null | 卡片图片完整 URL | -| link | string | 小程序页面跳转路径,空字符串不跳转 | -| sort | number | 排序值 | - -### 前端接入示例 - -`src/services/home.ts`(新增): - -```ts -import { get } from '@/utils/request' - -/** 首页轮播图项 */ -export interface HomeBanner { - id: number - title: string - image_id: number - image_url: string | null - link: string - sort: number -} - -/** 首页宫格导航项 */ -export interface HomeNav { - id: number - name: string - image_id: number - image_url: string | null - link: string - sort: number -} - -/** 首页促销推荐卡片 */ -export interface HomePromo { - id: number - title: string - sub_title: string - image_id: number - image_url: string | null - link: string - sort: number -} - -/** 首页配置聚合数据 */ -export interface HomeConfig { - banners: HomeBanner[] - navs: HomeNav[] - promos: HomePromo[] -} - -/** 首页配置(轮播图 + 宫格导航 + 促销卡片):GET /mini/home */ -export function getHomeConfigApi() { - return get('/mini/home') -} -``` - -页面中使用(跳转需兼容空链接): - -```tsx -const [config, setConfig] = useState({ banners: [], navs: [], promos: [] }) - -useEffect(() => { - getHomeConfigApi().then(res => setConfig(res.data)) -}, []) - -/** 统一跳转:link 为空不跳转 */ -function handleLink(link: string) { - if (!link) return - Taro.navigateTo({ url: link }) -} -``` - -### 注意事项 - -1. **三组数据均可能为空数组**(后台未配置或全部停用),页面需做空态处理。 -2. `image_url` 可能为 `null`(后台未上传图片),渲染前判空。 -3. `link` 为小程序内部页面路径(以 `/` 开头),用 `Taro.navigateTo` 跳转;若目标为 tabBar 页面需改用 `Taro.switchTab`(建议后台配置时避免填 tabBar 路径)。 -4. 数据实时生效:后台修改后,小程序下次进入首页请求即为最新内容,无缓存。 -5. 接口需登录后调用;未登录(401)会由 request 封装自动跳登录页。 diff --git a/tests/Feature/MiniBillTest.php b/tests/Feature/MiniBillTest.php new file mode 100644 index 0000000..bb3b4c5 --- /dev/null +++ b/tests/Feature/MiniBillTest.php @@ -0,0 +1,188 @@ +paymentCycle($cycleDays)->create(); + + return [$store, UserModel::factory()->forStore($store->id)->create()]; + } + + /** 造一张账单(默认 2026-08-10 出账,总额 140.00,未支付) */ + private function makeBill(StoreModel $store, array $attributes = []): BillModel + { + $seq = ++self::$billSeq; + + return BillModel::create(array_merge([ + 'bill_no' => 'ZD' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT), + 'purchase_id' => PurchaseOrderModel::factory()->create()->id, + 'store_id' => $store->id, + 'bill_date' => '2026-08-10', + 'product_amount' => '100.00', + 'delivery_fee' => '10.00', + 'box_num' => 2, + 'tray_num' => 1, + 'box_price' => '5.00', + 'tray_price' => '20.00', + 'added_amount' => '30.00', + 'total_amount' => '140.00', + 'status' => BillModel::STATUS_UNPAID, + ], $attributes)); + } + + /** 支付进度推导与应结算日期(账单日期 + 门店回款周期) */ + public function test_bill_list_derives_pay_state_and_settlement_date(): void + { + [$store, $user] = $this->makeStoreWithUser(3); + $this->actingAsMiniUser($user); + + $unpaid = $this->makeBill($store); + $reviewing = $this->makeBill($store, ['payment_id' => 99]); + $paid = $this->makeBill($store, ['status' => BillModel::STATUS_PAID, 'paid_at' => '2026-08-12 10:00:00']); + + $rows = collect($this->getJson('/mini/bill')->assertOk()->json('data.data'))->keyBy('id'); + + $unpaidRow = $rows[$unpaid->id]; + $this->assertSame(BillModel::PAY_STATE_UNPAID, $unpaidRow['pay_state']); + $this->assertSame('待支付', $unpaidRow['pay_state_name']); + $this->assertTrue($unpaidRow['can_pay']); + $this->assertSame('2026-08-13', $unpaidRow['settlement_date'], '账单日期 2026-08-10 + 回款周期 3 天'); + $this->assertArrayNotHasKey('operator_id', $unpaidRow, '小程序端不输出后台操作人字段'); + $this->assertArrayNotHasKey('paid_operator_id', $unpaidRow); + + $reviewingRow = $rows[$reviewing->id]; + $this->assertSame(BillModel::PAY_STATE_REVIEWING, $reviewingRow['pay_state']); + $this->assertSame('审核中', $reviewingRow['pay_state_name']); + $this->assertFalse($reviewingRow['can_pay']); + + $paidRow = $rows[$paid->id]; + $this->assertSame(BillModel::PAY_STATE_PAID, $paidRow['pay_state']); + $this->assertSame('已支付', $paidRow['pay_state_name']); + $this->assertFalse($paidRow['can_pay']); + $this->assertSame('2026-08-12 10:00:00', $paidRow['paid_at']); + } + + /** 待支付汇总:未支付笔数/金额(含审核中),不受列表筛选影响 */ + public function test_bill_list_summary_aggregates_unpaid(): void + { + [$store, $user] = $this->makeStoreWithUser(); + $this->actingAsMiniUser($user); + + $this->makeBill($store, ['total_amount' => '100.00']); + $this->makeBill($store, ['total_amount' => '50.50', 'payment_id' => 99]); + $this->makeBill($store, ['total_amount' => '200.00', 'status' => BillModel::STATUS_PAID]); + + $response = $this->getJson('/mini/bill?status=1')->assertOk(); + $this->assertSame(1, $response->json('data.total'), 'status 筛选只影响列表'); + $this->assertSame(2, $response->json('data.summary.unpaid_count')); + $this->assertSame('150.50', $response->json('data.summary.unpaid_amount')); + } + + /** 可付款筛选:仅未支付且未锁定到支付记录的账单(合并付款选择页) */ + public function test_bill_list_payable_filter(): void + { + [$store, $user] = $this->makeStoreWithUser(); + $this->actingAsMiniUser($user); + + $payable = $this->makeBill($store); + $this->makeBill($store, ['payment_id' => 99]); + $this->makeBill($store, ['status' => BillModel::STATUS_PAID]); + + $rows = $this->getJson('/mini/bill?payable=1')->assertOk()->json('data.data'); + $this->assertCount(1, $rows); + $this->assertSame($payable->id, $rows[0]['id']); + } + + /** 账单日期区间筛选 */ + public function test_bill_list_filters_by_bill_date_range(): void + { + [$store, $user] = $this->makeStoreWithUser(); + $this->actingAsMiniUser($user); + + $this->makeBill($store, ['bill_date' => '2026-08-01']); + $this->makeBill($store, ['bill_date' => '2026-08-05']); + $this->makeBill($store, ['bill_date' => '2026-08-10']); + + $this->getJson('/mini/bill?start_date=2026-08-03')->assertJsonPath('data.total', 2); + $this->getJson('/mini/bill?start_date=2026-08-03&end_date=2026-08-06')->assertJsonPath('data.total', 1); + $this->getJson('/mini/bill?end_date=2026-08-06')->assertJsonPath('data.total', 2); + } + + /** 门店数据隔离:只能查看本店账单 */ + public function test_bill_data_isolated_between_stores(): void + { + [$storeA] = $this->makeStoreWithUser(); + $billOfA = $this->makeBill($storeA); + + [, $userB] = $this->makeStoreWithUser(); + $this->actingAsMiniUser($userB); + + $this->getJson('/mini/bill')->assertJsonPath('data.total', 0); + $this->getJson("/mini/bill/{$billOfA->id}")->assertJsonPath('success', false); + } + + /** 详情:格式化账单 + 合并商品明细 + 关联订单 */ + public function test_bill_detail_outputs_bill_items_and_orders(): void + { + [$store, $user] = $this->makeStoreWithUser(0); + $this->actingAsMiniUser($user); + + $bill = $this->makeBill($store); + $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + $order = StoreOrderModel::factory()->create([ + 'store_id' => $store->id, + 'purchase_id' => $bill->purchase_id, + 'bill_id' => $bill->id, + 'total_amount' => '100.00', + ]); + StoreOrderItemModel::factory()->create([ + 'order_id' => $order->id, + 'purchase_id' => $bill->purchase_id, + 'bill_id' => $bill->id, + 'store_id' => $store->id, + 'product_id' => $product->id, + 'product_name' => '白菜', + 'price' => '5.00', + 'quantity' => 20, + 'amount' => '100.00', + ]); + + $data = $this->getJson("/mini/bill/{$bill->id}")->assertOk()->json('data'); + + $this->assertSame($bill->bill_no, $data['bill']['bill_no']); + $this->assertSame('待支付', $data['bill']['pay_state_name']); + $this->assertSame('2026-08-10', $data['bill']['settlement_date'], '回款周期 0 天,出账当天应结算'); + $this->assertSame($bill->purchase->purchase_no, $data['bill']['purchase']['purchase_no']); + + $this->assertCount(1, $data['items']); + $this->assertSame('白菜', $data['items'][0]['product_name']); + $this->assertSame('5.00', $data['items'][0]['price'], '合并明细单价 = Σ金额 ÷ Σ数量'); + $this->assertSame(20, $data['items'][0]['quantity']); + $this->assertSame('100.00', $data['items'][0]['amount']); + + $this->assertCount(1, $data['orders']); + $this->assertSame($order->order_no, $data['orders'][0]['order_no']); + } +} diff --git a/tests/Feature/StoreOrderTest.php b/tests/Feature/StoreOrderTest.php index d968109..dc3187d 100644 --- a/tests/Feature/StoreOrderTest.php +++ b/tests/Feature/StoreOrderTest.php @@ -130,6 +130,68 @@ class StoreOrderTest extends ProcurementTestCase $this->getJson('/mini/order')->assertJsonPath('data.total', 0); } + /** 列表输出:状态名 / 可取消标记 / 商品预览(前 3 条)与明细种数 */ + public function test_order_list_outputs_status_name_and_item_preview(): void + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); + + $items = []; + foreach (['白菜', '土豆', '番茄', '黄瓜'] as $name) { + $product = ProductModel::factory()->create(['name' => $name, 'status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 5.00]); + $items[] = ['product_id' => $product->id, 'quantity' => 2]; + } + $this->postJson('/mini/order', ['items' => $items])->assertJsonPath('success', true); + $order = StoreOrderModel::where('store_id', $store->id)->first(); + + $row = collect($this->getJson('/mini/order')->assertOk()->json('data.data'))->firstWhere('id', $order->id); + $this->assertSame('待接单', $row['status_name']); + $this->assertTrue($row['can_cancel']); + $this->assertSame(4, $row['item_count']); + $this->assertCount(3, $row['items'], '预览仅输出前 3 条明细,完整明细走详情接口'); + $this->assertSame('白菜', $row['items'][0]['product_name']); + $this->assertSame(2, $row['items'][0]['quantity']); + + // 已接单后状态名变化且不可取消 + $order->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + $row = collect($this->getJson('/mini/order')->json('data.data'))->firstWhere('id', $order->id); + $this->assertSame('已接单', $row['status_name']); + $this->assertFalse($row['can_cancel']); + } + + /** 订货日期区间筛选(配合 /order/summary 周期下钻) */ + public function test_order_list_filters_by_order_date_range(): void + { + [$store, $product, $user] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($user); + + foreach ([1, 1, 1] as $qty) { + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]]) + ->assertJsonPath('success', true); + } + $orders = StoreOrderModel::where('store_id', $store->id)->orderBy('id')->get(); + $orders[0]->update(['order_date' => '2026-08-01']); + $orders[1]->update(['order_date' => '2026-08-05']); + $orders[2]->update(['order_date' => '2026-08-10']); + + $this->getJson('/mini/order?start_date=2026-08-03')->assertJsonPath('data.total', 2); + $this->getJson('/mini/order?start_date=2026-08-03&end_date=2026-08-06')->assertJsonPath('data.total', 1); + $this->getJson('/mini/order?end_date=2026-08-06')->assertJsonPath('data.total', 2); + } + + /** 非法筛选参数被拦截(状态枚举 / 分页上限 / 日期区间倒置) */ + public function test_order_list_rejects_invalid_filters(): void + { + [, , $user] = $this->makeStoreWithProduct(); + $this->actingAsMiniUser($user); + + $this->getJson('/mini/order?status=7')->assertJsonPath('success', false); + $this->getJson('/mini/order?pageSize=100')->assertJsonPath('success', false); + $this->getJson('/mini/order?start_date=2026-08-10&end_date=2026-08-01')->assertJsonPath('success', false); + } + /** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */ private function makeOrderWithStatus(int $status): StoreOrderModel {