From cf1d3a6229554db235f97af825d0bb5674625692 Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Fri, 4 Sep 2026 21:22:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7?= =?UTF-8?q?=E6=94=AF=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 6 + .phpunit.result.cache | 2 +- .../Mini/OnlinePaymentController.php | 16 +- .../Controllers/Mini/PaymentController.php | 3 + app/Models/StoreModel.php | 1 + app/Services/OnlinePaymentService.php | 40 ++- app/Services/WangpuPayService.php | 14 + app/Services/WechatMpService.php | 72 ++++ app/Services/WechatPayService.php | 18 +- config/services.php | 8 +- .../2026_07_23_030608_create_store_table.php | 1 + database/seeders/SysDataSeeder.php | 1 + tests/Feature/MiniMpPaymentTest.php | 336 ++++++++++++++++++ 13 files changed, 494 insertions(+), 24 deletions(-) create mode 100644 app/Services/WechatMpService.php create mode 100644 tests/Feature/MiniMpPaymentTest.php diff --git a/.env.example b/.env.example index 35ae32a..e87864c 100644 --- a/.env.example +++ b/.env.example @@ -51,6 +51,10 @@ MAIL_FROM_NAME= WECHAT_MINI_APPID= WECHAT_MINI_SECRET= +# 微信公众号(网页授权换 openid,公众号 H5 JSAPI 支付用;后台 系统设置→微信应用配置 优先) +WECHAT_MP_APPID= +WECHAT_MP_SECRET= + # 在线支付渠道开关:wangpu=旺铺支付网关 / wechat=微信官方支付(后台 系统设置→支付配置 优先) PAY_ONLINE_CHANNEL=wangpu @@ -64,6 +68,8 @@ WANGPU_PUBLIC_KEY= WANGPU_PRIVATE_KEY= WANGPU_SUB_APPID= WANGPU_PAYWAY_CODE=WECHAT_MINI +# 公众号场景支付方式代码(留空回退 WANGPU_PAYWAY_CODE) +WANGPU_MP_PAYWAY_CODE= # 微信官方支付 APIv3(也可在后台 系统设置→微信官方支付 中维护,后台配置优先) WXPAY_MCH_ID= diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 977de0c..578a8c5 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":8,"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,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":8,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":8,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":8,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":7,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":7,"Tests\\Feature\\BillPaymentTest::test_payment_detail_returns_voucher_urls":8,"Tests\\Feature\\ContainerReturnTest::test_press_containers_adds_amount_and_writes_record":7,"Tests\\Feature\\ContainerReturnTest::test_return_containers_deducts_amount":7,"Tests\\Feature\\ContainerReturnTest::test_mixed_sign_containers":7,"Tests\\Feature\\ContainerReturnTest::test_zero_containers_writes_no_record":7,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":7,"Tests\\Feature\\DashboardTest::test_requires_dashboard_analysis_ability":7,"Tests\\Feature\\DashboardTest::test_recon_summary_and_receivable":8,"Tests\\Feature\\DashboardDebugTest::test_debug":8,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":8,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":8,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":8,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":8,"Tests\\Feature\\StoreOrderItemTest::test_add_store_item_uses_level_percent_price":8,"Tests\\Feature\\ExportTest::test_export_suppliers_multi_sheet":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_skips_catalog_sync_when_product_deleted":7,"Tests\\Feature\\ProductImportExportTest::test_export_filters_by_category":8,"Tests\\Feature\\ProductImportExportTest::test_import_creates_products_and_auto_supplier":7,"Tests\\Feature\\ProductImportExportTest::test_import_matches_existing_supplier":7,"Tests\\Feature\\ProductImportExportTest::test_import_errors_abort_all_rows":7,"Tests\\Feature\\ProductImportExportTest::test_import_category_path_resolution":7,"Tests\\Feature\\MiniReportTest::test_report_accumulates_same_product_across_orders":7,"Tests\\Feature\\ExportTest::test_export_suppliers_split_by_market":7,"Tests\\Feature\\MiniOnlinePaymentTest::test_query_syncs_gateway_status":7,"Tests\\Feature\\MiniSpecialTest::test_cart_quantity_attached_for_store":8,"Tests\\Feature\\MiniWechatPaymentTest::test_create_wechat_payment_success":7,"Tests\\Feature\\ExportTest::test_export_lists_all_catalog_products_with_totals_row":7},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.018,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.089,"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.006,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.047,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.046,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.052,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.053,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.093,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.102,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.101,"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.006,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.045,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.112,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.017,"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.056,"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.047,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.048,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.046,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.008,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.006,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.086,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.049,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.052,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.057,"Tests\\Feature\\CartTest::test_update_quantity":0.048,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.047,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.046,"Tests\\Feature\\CartTest::test_delete_item":0.047,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.097,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.011,"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.045,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.008,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.01,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.01,"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.009,"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.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.01,"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.046,"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.01,"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.003,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info":0.047,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.047,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.046,"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.052,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.104,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.049,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.05,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.054,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.011,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.011,"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.01,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.01,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.058,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.063,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.246,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.053,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.107,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.102,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.104,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.032,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.099,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.101,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.1,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.016,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.108,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.019,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.103,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.101,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.101,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.012,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.12,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.107,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.102,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.059,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.101,"Tests\\Feature\\ExportTest::test_export_invalid_type_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_missing_purchase_rejected":0.008,"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.006,"Tests\\Feature\\ProductCategoryTest::test_create_category_under_second_level_rejected":0.006,"Tests\\Feature\\ProductCategoryTest::test_update_second_level_move_to_another_top":0.006,"Tests\\Feature\\ProductCategoryTest::test_update_parent_with_children_cannot_move_under_category":0.007,"Tests\\Feature\\ProductCategoryTest::test_update_with_missing_parent_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_parent_category_rejected":0.008,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":0.007,"Tests\\Feature\\ProductCategoryTest::test_mini_categories_returns_all_enabled_categories":0.045,"Tests\\Feature\\MiniBillTest::test_bill_list_derives_pay_state_and_settlement_date":0.047,"Tests\\Feature\\MiniBillTest::test_bill_list_summary_aggregates_unpaid":0.047,"Tests\\Feature\\MiniBillTest::test_bill_list_payable_filter":0.047,"Tests\\Feature\\MiniBillTest::test_bill_list_filters_by_bill_date_range":0.051,"Tests\\Feature\\MiniBillTest::test_bill_data_isolated_between_stores":0.088,"Tests\\Feature\\MiniBillTest::test_bill_detail_outputs_bill_items_and_orders":0.051,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":0.054,"Tests\\Feature\\StoreOrderTest::test_order_list_filters_by_order_date_range":0.058,"Tests\\Feature\\StoreOrderTest::test_order_list_rejects_invalid_filters":0.049,"Tests\\Feature\\BillExportTest::test_export_merges_items_across_bills":0.112,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":0.05,"Tests\\Feature\\BillExportTest::test_export_empty_category_rejected":0.089,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":0.05,"Tests\\Feature\\BillExportTest::test_export_invalid_ids_rejected":0.009,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":0.098,"Tests\\Feature\\BillExportTest::test_mini_export_scoped_to_bound_store":0.014,"Tests\\Feature\\BillExportTest::test_mini_export_requires_store_binding":0.005,"Tests\\Feature\\MiniNoticeTest::test_notice_list_includes_personal_and_broadcast":0.047,"Tests\\Feature\\MiniNoticeTest::test_broadcast_read_creates_personal_copy":0.048,"Tests\\Feature\\MiniNoticeTest::test_personal_notice_read_marks_directly":0.046,"Tests\\Feature\\MiniNoticeTest::test_other_users_notice_not_found":0.007,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_only_accept_and_cancel":0.1,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_rejects_delivery_and_completion":0.148,"Tests\\Feature\\StoreOrderTest::test_batch_status_only_accept_and_cancel":0.101,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":0.066,"Tests\\Feature\\BillPaymentTest::test_manual_pay_accumulates_store_total_purchase_amount":0.069,"Tests\\Feature\\BillPaymentTest::test_payment_audit_pass_accumulates_store_total_purchase_amount":0.051,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":0.048,"Tests\\Feature\\MiniProductTest::test_product_detail_guest_sees_null_price":0.045,"Tests\\Feature\\MiniProductTest::test_product_detail_store_user_sees_level_price":0.006,"Tests\\Feature\\MiniProductTest::test_product_detail_percent_price_calculated_from_cost":0.046,"Tests\\Feature\\MiniProductTest::test_product_detail_off_shelf_or_missing_rejected":0.046,"Tests\\Feature\\MiniProductTest::test_product_list_store_user_sees_level_price":0.005,"Tests\\Feature\\MiniProductTest::test_product_list_guest_sees_null_price":0.044,"Tests\\Feature\\BillPaymentTest::test_admin_order_list_and_detail_show_bill_info":0.069,"Tests\\Feature\\BillPaymentTest::test_payment_detail_returns_voucher_urls":0.052,"Tests\\Feature\\StoreOrderTest::test_order_list_preview_includes_image_spec_and_unit":0.051,"Tests\\Feature\\ContainerReturnTest::test_press_containers_adds_amount_and_writes_record":0.062,"Tests\\Feature\\ContainerReturnTest::test_return_containers_deducts_amount":0.059,"Tests\\Feature\\ContainerReturnTest::test_mixed_sign_containers":0.059,"Tests\\Feature\\ContainerReturnTest::test_zero_containers_writes_no_record":0.06,"Tests\\Feature\\ContainerReturnTest::test_non_integer_containers_rejected":0.048,"Tests\\Feature\\ContainerReturnTest::test_manual_create_and_delete_routes_removed":0.023,"Tests\\Feature\\ContainerReturnTest::test_export_matrix_merges_same_day_and_zero_fills":0.089,"Tests\\Feature\\ContainerReturnTest::test_export_filters_by_selected_stores":0.128,"Tests\\Feature\\ContainerReturnTest::test_export_invalid_params_rejected":0.009,"Tests\\Feature\\ContainerReturnTest::test_export_empty_range_rejected":0.006,"Tests\\Feature\\ContainerReturnTest::test_export_requires_export_permission":0.007,"Tests\\Feature\\DashboardTest::test_guest_cannot_access_dashboard":0.002,"Tests\\Feature\\DashboardTest::test_requires_dashboard_analysis_ability":0.007,"Tests\\Feature\\DashboardTest::test_empty_database_returns_zero_structure":0.01,"Tests\\Feature\\DashboardTest::test_overview_excludes_cancelled_orders":0.05,"Tests\\Feature\\DashboardTest::test_week_growth_rate":0.054,"Tests\\Feature\\DashboardTest::test_trend_covers_30_days_with_zero_filling":0.051,"Tests\\Feature\\DashboardTest::test_category_sales_grouped_by_category":0.051,"Tests\\Feature\\DashboardTest::test_top_products_aggregated_across_orders":0.052,"Tests\\Feature\\DashboardTest::test_top_stores_ranked_by_amount":0.09,"Tests\\Feature\\DashboardTest::test_purchase_amount_prefers_actual_over_estimate":0.01,"Tests\\Feature\\DashboardTest::test_recon_summary_and_receivable":0.052,"Tests\\Feature\\DashboardTest::test_latest_orders_limited_and_ordered":0.052,"Tests\\Feature\\DashboardTest::test_archives_count_active_records_only":0.132,"Tests\\Feature\\DashboardDebugTest::test_debug_ability_check":0.044,"Tests\\Feature\\DashboardDebugTest::test_debug":0.045,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_cost_and_notifies_all_stores":0.09,"Tests\\Feature\\ProductPriceTest::test_level_percent_update_notifies_level_stores":0.103,"Tests\\Feature\\ProductPriceTest::test_level_update_without_percent_change_sends_no_notice":0.048,"Tests\\Feature\\ProductPriceTest::test_level_percent_validation":0.007,"Tests\\Feature\\ProductCostPriceTest::test_calc_level_price":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_level_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_level_prices":0.008,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_price":0.007,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_row_update":0.051,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_level_price_columns":0.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_level_price":0.048,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_level_price":0.049,"Tests\\Feature\\CartTest::test_store_level_missing_rejected":0.006,"Tests\\Feature\\StoreOrderTest::test_store_level_missing_rejected":0.047,"Tests\\Feature\\PurchaseEditTest::test_update_row_skips_catalog_sync_when_product_deleted":0.104,"Tests\\Feature\\PurchaseEditTest::test_store_item_add_creates_item_and_recalculates":0.105,"Tests\\Feature\\PurchaseEditTest::test_store_item_update_merges_multi_order_rows":0.059,"Tests\\Feature\\PurchaseEditTest::test_store_item_remove_deletes_and_recalculates":0.105,"Tests\\Feature\\StoreOrderItemTest::test_update_store_item_recalculates_totals":0.059,"Tests\\Feature\\StoreOrderItemTest::test_store_item_edits_rejected_when_purchase_completed":0.061,"Tests\\Feature\\StoreOrderItemTest::test_update_store_item_validates_negative_values":0.056,"Tests\\Feature\\StoreOrderItemTest::test_add_store_item_uses_level_percent_price":0.057,"Tests\\Feature\\StoreOrderItemTest::test_add_store_item_rejected_when_product_deleted":0.058,"Tests\\Feature\\ExportTest::test_export_lists_all_catalog_products_with_totals_row":0.101,"Tests\\Feature\\ExportTest::test_export_supplier_filter":0.101,"Tests\\Feature\\ExportTest::test_export_stores_multi_sheet":0.103,"Tests\\Feature\\ExportTest::test_export_stores_single_store":0.101,"Tests\\Feature\\ExportTest::test_export_stores_without_items_rejected":0.143,"Tests\\Feature\\ExportTest::test_export_suppliers_multi_sheet":0.102,"Tests\\Feature\\ExportTest::test_export_suppliers_single":0.101,"Tests\\Feature\\ExportTest::test_export_suppliers_without_items_rejected":0.101,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_change_reprices_items_by_store_level":0.102,"Tests\\Feature\\PurchaseEditTest::test_update_row_without_cost_change_keeps_item_price":0.105,"Tests\\Feature\\CartTest::test_update_other_stores_item_rejected":0.09,"Tests\\Feature\\CartTest::test_delete_other_stores_item_rejected":0.089,"Tests\\Feature\\CartTest::test_cart_isolated_between_stores":0.089,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_valid_credentials":0.126,"Tests\\Feature\\MiniAuthTest::test_login_rejects_wrong_password":0.124,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unknown_username":0.002,"Tests\\Feature\\MiniAuthTest::test_disabled_store_cannot_login":0.123,"Tests\\Feature\\MiniAuthTest::test_login_validates_required_params":0.003,"Tests\\Feature\\MiniAuthTest::test_login_token_accesses_protected_routes":0.128,"Tests\\Feature\\MiniAuthTest::test_set_password_succeeds":0.21,"Tests\\Feature\\MiniAuthTest::test_set_password_rejects_wrong_old_password":0.126,"Tests\\Feature\\MiniAuthTest::test_set_password_validates_confirmation":0.046,"Tests\\Feature\\BillExportTest::test_mini_export_scoped_to_own_store":0.092,"Tests\\Feature\\MiniNoticeTest::test_other_stores_notice_not_found":0.086,"Tests\\Feature\\MiniProductTest::test_product_detail_store_sees_level_price":0.046,"Tests\\Feature\\MiniProductTest::test_product_list_store_sees_level_price":0.047,"Tests\\Feature\\MiniStoreTest::test_info_returns_current_store":0.045,"Tests\\Feature\\MiniStoreTest::test_disabled_store_rejected":0.048,"Tests\\Feature\\ProductImportExportTest::test_export_downloads_all_products":0.008,"Tests\\Feature\\ProductImportExportTest::test_export_filters_by_category":0.008,"Tests\\Feature\\ProductImportExportTest::test_export_template_mode":0.005,"Tests\\Feature\\ProductImportExportTest::test_export_requires_permission":0.007,"Tests\\Feature\\ProductImportExportTest::test_import_creates_products_and_auto_supplier":0.06,"Tests\\Feature\\ProductImportExportTest::test_import_matches_existing_supplier":0.016,"Tests\\Feature\\ProductImportExportTest::test_import_errors_abort_all_rows":0.015,"Tests\\Feature\\ProductImportExportTest::test_import_category_path_resolution":0.027,"Tests\\Feature\\ProductImportExportTest::test_import_rejects_invalid_header":0.016,"Tests\\Feature\\ProductImportExportTest::test_import_requires_permission":0.009,"Tests\\Feature\\ProductImportExportTest::test_list_filters_by_market":0.007,"Tests\\Feature\\MiniReportTest::test_report_aggregates_total_and_item_percent":0.047,"Tests\\Feature\\MiniReportTest::test_report_accumulates_same_product_across_orders":0.058,"Tests\\Feature\\MiniReportTest::test_report_filters_by_custom_date_range":0.047,"Tests\\Feature\\MiniReportTest::test_report_excludes_cancelled_and_deleted_orders":0.048,"Tests\\Feature\\MiniReportTest::test_report_preset_month_and_last_month":0.049,"Tests\\Feature\\MiniReportTest::test_report_preset_week_and_last_week":0.065,"Tests\\Feature\\MiniReportTest::test_report_defaults_to_current_month":0.048,"Tests\\Feature\\MiniReportTest::test_report_data_isolated_between_stores":0.088,"Tests\\Feature\\MiniReportTest::test_report_empty_period_returns_zero":0.046,"Tests\\Feature\\MiniReportTest::test_report_validates_params":0.052,"Tests\\Feature\\ProductBatchDeleteTest::test_batch_delete_soft_deletes_selected_products":0.021,"Tests\\Feature\\ProductBatchDeleteTest::test_batch_delete_requires_non_empty_ids":0.007,"Tests\\Feature\\ProductBatchDeleteTest::test_batch_delete_forbidden_without_permission":0.007,"Tests\\Feature\\CartTest::test_place_order_clears_ordered_items_from_cart":0.058,"Tests\\Feature\\StoreOrderItemTest::test_admin_order_list_filters_by_order_date_range":0.076,"Tests\\Feature\\StoreOrderItemTest::test_admin_order_list_search_by_purchase_no":0.063,"Tests\\Feature\\ExportTest::test_export_suppliers_split_by_market":0.057,"Tests\\Feature\\BillPaymentTest::test_generate_bill_saves_after_sale_and_remark":0.115,"Tests\\Feature\\ProductCostPriceTest::test_product_price_unit_default_and_editable":0.013,"Tests\\Feature\\StoreOrderTest::test_place_order_blocked_by_overdue_unpaid_bill":0.053,"Tests\\Feature\\StoreOrderTest::test_place_order_allowed_within_payment_cycle":0.054,"Tests\\Feature\\CartTest::test_cart_summary_endpoint":0.053,"Tests\\Feature\\CartTest::test_cart_summary_requires_login":0.002,"Tests\\Feature\\MiniProductTest::test_product_list_appends_cart_quantity_and_summary":0.052,"Tests\\Feature\\MiniProductTest::test_product_list_guest_sees_zero_cart_fields":0.046,"Tests\\Feature\\MiniProductTest::test_product_detail_appends_cart_quantity":0.049,"Tests\\Feature\\MiniProductTest::test_home_appends_cart_summary":0.05,"Tests\\Feature\\MiniOnlinePaymentTest::test_sign_matches_document_example":0.002,"Tests\\Feature\\MiniOnlinePaymentTest::test_create_online_payment_success":0.073,"Tests\\Feature\\MiniOnlinePaymentTest::test_create_online_payment_validation":0.098,"Tests\\Feature\\MiniOnlinePaymentTest::test_create_fails_when_code2session_fails":0.048,"Tests\\Feature\\MiniOnlinePaymentTest::test_create_gateway_failure_releases_bills":0.052,"Tests\\Feature\\MiniOnlinePaymentTest::test_notify_settles_payment":0.053,"Tests\\Feature\\MiniOnlinePaymentTest::test_notify_rejects_invalid_messages":0.058,"Tests\\Feature\\MiniOnlinePaymentTest::test_query_syncs_gateway_status":0.061,"Tests\\Feature\\MiniOnlinePaymentTest::test_query_rejects_other_store_payment":0.091,"Tests\\Feature\\MiniOnlinePaymentTest::test_offline_payment_flow_unaffected":0.046,"Tests\\Feature\\BillPaymentTest::test_generate_bill_positive_after_sale_added_to_total":0.112,"Tests\\Feature\\BillPaymentTest::test_generate_bill_rejects_non_numeric_after_sale":0.048,"Tests\\Feature\\ClientSpecialTest::test_batch_add_creates_specials":0.021,"Tests\\Feature\\ClientSpecialTest::test_batch_add_skips_existing_products":0.011,"Tests\\Feature\\ClientSpecialTest::test_batch_add_requires_non_empty_product_ids":0.006,"Tests\\Feature\\ClientSpecialTest::test_batch_add_forbidden_without_permission":0.007,"Tests\\Feature\\ClientSpecialTest::test_query_filters_by_product_name_keyword":0.008,"Tests\\Feature\\ClientSpecialTest::test_update_changes_sort_and_status":0.006,"Tests\\Feature\\ClientSpecialTest::test_delete_and_batch_delete":0.008,"Tests\\Feature\\ClientSpecialTest::test_batch_delete_requires_non_empty_ids":0.006,"Tests\\Feature\\MiniSpecialTest::test_guest_sees_specials_with_null_price":0.045,"Tests\\Feature\\MiniSpecialTest::test_store_sees_level_price":0.046,"Tests\\Feature\\MiniSpecialTest::test_disabled_special_and_off_shelf_product_excluded":0.058,"Tests\\Feature\\MiniSpecialTest::test_specials_sorted_by_sort_asc":0.087,"Tests\\Feature\\MiniSpecialTest::test_cart_quantity_attached_for_store":0.047,"Tests\\Feature\\MiniOnlinePaymentTest::test_aes_encryption_matches_demo_golden":0.002,"Tests\\Feature\\MiniOnlinePaymentTest::test_notify_envelope_round_trip":0.005,"Tests\\Feature\\MiniOnlinePaymentTest::test_swapped_key_config_fails_with_clear_message":0.004,"Tests\\Feature\\MiniWechatPaymentTest::test_create_wechat_payment_success":0.062,"Tests\\Feature\\MiniWechatPaymentTest::test_create_wechat_payment_gateway_failure_releases_bills":0.067,"Tests\\Feature\\MiniWechatPaymentTest::test_wechat_notify_settles_payment":0.056,"Tests\\Feature\\MiniWechatPaymentTest::test_wechat_notify_rejects_invalid_messages":0.056,"Tests\\Feature\\MiniWechatPaymentTest::test_wechat_notify_with_platform_cert_validates_signature":0.055,"Tests\\Feature\\MiniWechatPaymentTest::test_wechat_query_syncs_status":0.057,"Tests\\Feature\\MiniWechatPaymentTest::test_wangpu_channel_remains_default":0.051,"Tests\\Feature\\BillPaymentTest::test_payment_config_returns_pay_methods":0.047}} \ 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":8,"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,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":8,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":8,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":8,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":7,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":7,"Tests\\Feature\\BillPaymentTest::test_payment_detail_returns_voucher_urls":8,"Tests\\Feature\\ContainerReturnTest::test_press_containers_adds_amount_and_writes_record":7,"Tests\\Feature\\ContainerReturnTest::test_return_containers_deducts_amount":7,"Tests\\Feature\\ContainerReturnTest::test_mixed_sign_containers":7,"Tests\\Feature\\ContainerReturnTest::test_zero_containers_writes_no_record":7,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":7,"Tests\\Feature\\DashboardTest::test_requires_dashboard_analysis_ability":7,"Tests\\Feature\\DashboardTest::test_recon_summary_and_receivable":8,"Tests\\Feature\\DashboardDebugTest::test_debug":8,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":8,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":8,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":8,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":8,"Tests\\Feature\\StoreOrderItemTest::test_add_store_item_uses_level_percent_price":8,"Tests\\Feature\\ExportTest::test_export_suppliers_multi_sheet":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_skips_catalog_sync_when_product_deleted":7,"Tests\\Feature\\ProductImportExportTest::test_export_filters_by_category":8,"Tests\\Feature\\ProductImportExportTest::test_import_creates_products_and_auto_supplier":7,"Tests\\Feature\\ProductImportExportTest::test_import_matches_existing_supplier":7,"Tests\\Feature\\ProductImportExportTest::test_import_errors_abort_all_rows":7,"Tests\\Feature\\ProductImportExportTest::test_import_category_path_resolution":7,"Tests\\Feature\\MiniReportTest::test_report_accumulates_same_product_across_orders":7,"Tests\\Feature\\ExportTest::test_export_suppliers_split_by_market":7,"Tests\\Feature\\MiniOnlinePaymentTest::test_query_syncs_gateway_status":7,"Tests\\Feature\\MiniSpecialTest::test_cart_quantity_attached_for_store":8,"Tests\\Feature\\MiniWechatPaymentTest::test_create_wechat_payment_success":7,"Tests\\Feature\\ExportTest::test_export_lists_all_catalog_products_with_totals_row":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.091,"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.052,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.05,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.05,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.05,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.092,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.103,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.103,"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.006,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.044,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.085,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.017,"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.056,"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.047,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.049,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.047,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.008,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.006,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.087,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.063,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.053,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.047,"Tests\\Feature\\CartTest::test_update_quantity":0.05,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.048,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.046,"Tests\\Feature\\CartTest::test_delete_item":0.047,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.095,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.011,"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.051,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.008,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.01,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.01,"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.007,"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.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.01,"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.047,"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.01,"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.003,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info":0.045,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.046,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.045,"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.052,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.104,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.049,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.052,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.06,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.011,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.011,"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.01,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.01,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.057,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.095,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.256,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.053,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.131,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.133,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.112,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.032,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.101,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.103,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.103,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.016,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.103,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.019,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.104,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.103,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.102,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.012,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.111,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.105,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.102,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.062,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.11,"Tests\\Feature\\ExportTest::test_export_invalid_type_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_missing_purchase_rejected":0.009,"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.006,"Tests\\Feature\\ProductCategoryTest::test_create_category_under_second_level_rejected":0.006,"Tests\\Feature\\ProductCategoryTest::test_update_second_level_move_to_another_top":0.006,"Tests\\Feature\\ProductCategoryTest::test_update_parent_with_children_cannot_move_under_category":0.008,"Tests\\Feature\\ProductCategoryTest::test_update_with_missing_parent_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_parent_category_rejected":0.008,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":0.006,"Tests\\Feature\\ProductCategoryTest::test_mini_categories_returns_all_enabled_categories":0.046,"Tests\\Feature\\MiniBillTest::test_bill_list_derives_pay_state_and_settlement_date":0.05,"Tests\\Feature\\MiniBillTest::test_bill_list_summary_aggregates_unpaid":0.053,"Tests\\Feature\\MiniBillTest::test_bill_list_payable_filter":0.049,"Tests\\Feature\\MiniBillTest::test_bill_list_filters_by_bill_date_range":0.053,"Tests\\Feature\\MiniBillTest::test_bill_data_isolated_between_stores":0.09,"Tests\\Feature\\MiniBillTest::test_bill_detail_outputs_bill_items_and_orders":0.052,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":0.052,"Tests\\Feature\\StoreOrderTest::test_order_list_filters_by_order_date_range":0.058,"Tests\\Feature\\StoreOrderTest::test_order_list_rejects_invalid_filters":0.056,"Tests\\Feature\\BillExportTest::test_export_merges_items_across_bills":0.096,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":0.051,"Tests\\Feature\\BillExportTest::test_export_empty_category_rejected":0.081,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":0.048,"Tests\\Feature\\BillExportTest::test_export_invalid_ids_rejected":0.01,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":0.098,"Tests\\Feature\\BillExportTest::test_mini_export_scoped_to_bound_store":0.014,"Tests\\Feature\\BillExportTest::test_mini_export_requires_store_binding":0.005,"Tests\\Feature\\MiniNoticeTest::test_notice_list_includes_personal_and_broadcast":0.046,"Tests\\Feature\\MiniNoticeTest::test_broadcast_read_creates_personal_copy":0.051,"Tests\\Feature\\MiniNoticeTest::test_personal_notice_read_marks_directly":0.046,"Tests\\Feature\\MiniNoticeTest::test_other_users_notice_not_found":0.007,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_only_accept_and_cancel":0.101,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_rejects_delivery_and_completion":0.152,"Tests\\Feature\\StoreOrderTest::test_batch_status_only_accept_and_cancel":0.104,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":0.059,"Tests\\Feature\\BillPaymentTest::test_manual_pay_accumulates_store_total_purchase_amount":0.067,"Tests\\Feature\\BillPaymentTest::test_payment_audit_pass_accumulates_store_total_purchase_amount":0.051,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":0.049,"Tests\\Feature\\MiniProductTest::test_product_detail_guest_sees_null_price":0.046,"Tests\\Feature\\MiniProductTest::test_product_detail_store_user_sees_level_price":0.006,"Tests\\Feature\\MiniProductTest::test_product_detail_percent_price_calculated_from_cost":0.046,"Tests\\Feature\\MiniProductTest::test_product_detail_off_shelf_or_missing_rejected":0.048,"Tests\\Feature\\MiniProductTest::test_product_list_store_user_sees_level_price":0.005,"Tests\\Feature\\MiniProductTest::test_product_list_guest_sees_null_price":0.044,"Tests\\Feature\\BillPaymentTest::test_admin_order_list_and_detail_show_bill_info":0.071,"Tests\\Feature\\BillPaymentTest::test_payment_detail_returns_voucher_urls":0.054,"Tests\\Feature\\StoreOrderTest::test_order_list_preview_includes_image_spec_and_unit":0.05,"Tests\\Feature\\ContainerReturnTest::test_press_containers_adds_amount_and_writes_record":0.063,"Tests\\Feature\\ContainerReturnTest::test_return_containers_deducts_amount":0.06,"Tests\\Feature\\ContainerReturnTest::test_mixed_sign_containers":0.072,"Tests\\Feature\\ContainerReturnTest::test_zero_containers_writes_no_record":0.059,"Tests\\Feature\\ContainerReturnTest::test_non_integer_containers_rejected":0.047,"Tests\\Feature\\ContainerReturnTest::test_manual_create_and_delete_routes_removed":0.008,"Tests\\Feature\\ContainerReturnTest::test_export_matrix_merges_same_day_and_zero_fills":0.089,"Tests\\Feature\\ContainerReturnTest::test_export_filters_by_selected_stores":0.128,"Tests\\Feature\\ContainerReturnTest::test_export_invalid_params_rejected":0.009,"Tests\\Feature\\ContainerReturnTest::test_export_empty_range_rejected":0.006,"Tests\\Feature\\ContainerReturnTest::test_export_requires_export_permission":0.006,"Tests\\Feature\\DashboardTest::test_guest_cannot_access_dashboard":0.002,"Tests\\Feature\\DashboardTest::test_requires_dashboard_analysis_ability":0.006,"Tests\\Feature\\DashboardTest::test_empty_database_returns_zero_structure":0.009,"Tests\\Feature\\DashboardTest::test_overview_excludes_cancelled_orders":0.051,"Tests\\Feature\\DashboardTest::test_week_growth_rate":0.054,"Tests\\Feature\\DashboardTest::test_trend_covers_30_days_with_zero_filling":0.051,"Tests\\Feature\\DashboardTest::test_category_sales_grouped_by_category":0.052,"Tests\\Feature\\DashboardTest::test_top_products_aggregated_across_orders":0.051,"Tests\\Feature\\DashboardTest::test_top_stores_ranked_by_amount":0.091,"Tests\\Feature\\DashboardTest::test_purchase_amount_prefers_actual_over_estimate":0.009,"Tests\\Feature\\DashboardTest::test_recon_summary_and_receivable":0.05,"Tests\\Feature\\DashboardTest::test_latest_orders_limited_and_ordered":0.052,"Tests\\Feature\\DashboardTest::test_archives_count_active_records_only":0.131,"Tests\\Feature\\DashboardDebugTest::test_debug_ability_check":0.044,"Tests\\Feature\\DashboardDebugTest::test_debug":0.045,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_cost_and_notifies_all_stores":0.092,"Tests\\Feature\\ProductPriceTest::test_level_percent_update_notifies_level_stores":0.089,"Tests\\Feature\\ProductPriceTest::test_level_update_without_percent_change_sends_no_notice":0.048,"Tests\\Feature\\ProductPriceTest::test_level_percent_validation":0.01,"Tests\\Feature\\ProductCostPriceTest::test_calc_level_price":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_level_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_level_prices":0.008,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_price":0.007,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_row_update":0.052,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_level_price_columns":0.008,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_level_price":0.049,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_level_price":0.048,"Tests\\Feature\\CartTest::test_store_level_missing_rejected":0.006,"Tests\\Feature\\StoreOrderTest::test_store_level_missing_rejected":0.046,"Tests\\Feature\\PurchaseEditTest::test_update_row_skips_catalog_sync_when_product_deleted":0.103,"Tests\\Feature\\PurchaseEditTest::test_store_item_add_creates_item_and_recalculates":0.105,"Tests\\Feature\\PurchaseEditTest::test_store_item_update_merges_multi_order_rows":0.06,"Tests\\Feature\\PurchaseEditTest::test_store_item_remove_deletes_and_recalculates":0.103,"Tests\\Feature\\StoreOrderItemTest::test_update_store_item_recalculates_totals":0.059,"Tests\\Feature\\StoreOrderItemTest::test_store_item_edits_rejected_when_purchase_completed":0.065,"Tests\\Feature\\StoreOrderItemTest::test_update_store_item_validates_negative_values":0.058,"Tests\\Feature\\StoreOrderItemTest::test_add_store_item_uses_level_percent_price":0.059,"Tests\\Feature\\StoreOrderItemTest::test_add_store_item_rejected_when_product_deleted":0.06,"Tests\\Feature\\ExportTest::test_export_lists_all_catalog_products_with_totals_row":0.101,"Tests\\Feature\\ExportTest::test_export_supplier_filter":0.102,"Tests\\Feature\\ExportTest::test_export_stores_multi_sheet":0.108,"Tests\\Feature\\ExportTest::test_export_stores_single_store":0.101,"Tests\\Feature\\ExportTest::test_export_stores_without_items_rejected":0.143,"Tests\\Feature\\ExportTest::test_export_suppliers_multi_sheet":0.103,"Tests\\Feature\\ExportTest::test_export_suppliers_single":0.102,"Tests\\Feature\\ExportTest::test_export_suppliers_without_items_rejected":0.101,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_change_reprices_items_by_store_level":0.105,"Tests\\Feature\\PurchaseEditTest::test_update_row_without_cost_change_keeps_item_price":0.106,"Tests\\Feature\\CartTest::test_update_other_stores_item_rejected":0.09,"Tests\\Feature\\CartTest::test_delete_other_stores_item_rejected":0.091,"Tests\\Feature\\CartTest::test_cart_isolated_between_stores":0.089,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_valid_credentials":0.126,"Tests\\Feature\\MiniAuthTest::test_login_rejects_wrong_password":0.13,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unknown_username":0.004,"Tests\\Feature\\MiniAuthTest::test_disabled_store_cannot_login":0.124,"Tests\\Feature\\MiniAuthTest::test_login_validates_required_params":0.003,"Tests\\Feature\\MiniAuthTest::test_login_token_accesses_protected_routes":0.128,"Tests\\Feature\\MiniAuthTest::test_set_password_succeeds":0.208,"Tests\\Feature\\MiniAuthTest::test_set_password_rejects_wrong_old_password":0.125,"Tests\\Feature\\MiniAuthTest::test_set_password_validates_confirmation":0.045,"Tests\\Feature\\BillExportTest::test_mini_export_scoped_to_own_store":0.093,"Tests\\Feature\\MiniNoticeTest::test_other_stores_notice_not_found":0.087,"Tests\\Feature\\MiniProductTest::test_product_detail_store_sees_level_price":0.045,"Tests\\Feature\\MiniProductTest::test_product_list_store_sees_level_price":0.047,"Tests\\Feature\\MiniStoreTest::test_info_returns_current_store":0.045,"Tests\\Feature\\MiniStoreTest::test_disabled_store_rejected":0.047,"Tests\\Feature\\ProductImportExportTest::test_export_downloads_all_products":0.008,"Tests\\Feature\\ProductImportExportTest::test_export_filters_by_category":0.008,"Tests\\Feature\\ProductImportExportTest::test_export_template_mode":0.005,"Tests\\Feature\\ProductImportExportTest::test_export_requires_permission":0.007,"Tests\\Feature\\ProductImportExportTest::test_import_creates_products_and_auto_supplier":0.042,"Tests\\Feature\\ProductImportExportTest::test_import_matches_existing_supplier":0.016,"Tests\\Feature\\ProductImportExportTest::test_import_errors_abort_all_rows":0.021,"Tests\\Feature\\ProductImportExportTest::test_import_category_path_resolution":0.032,"Tests\\Feature\\ProductImportExportTest::test_import_rejects_invalid_header":0.021,"Tests\\Feature\\ProductImportExportTest::test_import_requires_permission":0.01,"Tests\\Feature\\ProductImportExportTest::test_list_filters_by_market":0.008,"Tests\\Feature\\MiniReportTest::test_report_aggregates_total_and_item_percent":0.048,"Tests\\Feature\\MiniReportTest::test_report_accumulates_same_product_across_orders":0.047,"Tests\\Feature\\MiniReportTest::test_report_filters_by_custom_date_range":0.048,"Tests\\Feature\\MiniReportTest::test_report_excludes_cancelled_and_deleted_orders":0.048,"Tests\\Feature\\MiniReportTest::test_report_preset_month_and_last_month":0.049,"Tests\\Feature\\MiniReportTest::test_report_preset_week_and_last_week":0.051,"Tests\\Feature\\MiniReportTest::test_report_defaults_to_current_month":0.047,"Tests\\Feature\\MiniReportTest::test_report_data_isolated_between_stores":0.088,"Tests\\Feature\\MiniReportTest::test_report_empty_period_returns_zero":0.046,"Tests\\Feature\\MiniReportTest::test_report_validates_params":0.049,"Tests\\Feature\\ProductBatchDeleteTest::test_batch_delete_soft_deletes_selected_products":0.008,"Tests\\Feature\\ProductBatchDeleteTest::test_batch_delete_requires_non_empty_ids":0.006,"Tests\\Feature\\ProductBatchDeleteTest::test_batch_delete_forbidden_without_permission":0.007,"Tests\\Feature\\CartTest::test_place_order_clears_ordered_items_from_cart":0.056,"Tests\\Feature\\StoreOrderItemTest::test_admin_order_list_filters_by_order_date_range":0.063,"Tests\\Feature\\StoreOrderItemTest::test_admin_order_list_search_by_purchase_no":0.068,"Tests\\Feature\\ExportTest::test_export_suppliers_split_by_market":0.057,"Tests\\Feature\\BillPaymentTest::test_generate_bill_saves_after_sale_and_remark":0.117,"Tests\\Feature\\ProductCostPriceTest::test_product_price_unit_default_and_editable":0.013,"Tests\\Feature\\StoreOrderTest::test_place_order_blocked_by_overdue_unpaid_bill":0.054,"Tests\\Feature\\StoreOrderTest::test_place_order_allowed_within_payment_cycle":0.06,"Tests\\Feature\\CartTest::test_cart_summary_endpoint":0.054,"Tests\\Feature\\CartTest::test_cart_summary_requires_login":0.002,"Tests\\Feature\\MiniProductTest::test_product_list_appends_cart_quantity_and_summary":0.054,"Tests\\Feature\\MiniProductTest::test_product_list_guest_sees_zero_cart_fields":0.047,"Tests\\Feature\\MiniProductTest::test_product_detail_appends_cart_quantity":0.049,"Tests\\Feature\\MiniProductTest::test_home_appends_cart_summary":0.05,"Tests\\Feature\\MiniOnlinePaymentTest::test_sign_matches_document_example":0.002,"Tests\\Feature\\MiniOnlinePaymentTest::test_create_online_payment_success":0.056,"Tests\\Feature\\MiniOnlinePaymentTest::test_create_online_payment_validation":0.101,"Tests\\Feature\\MiniOnlinePaymentTest::test_create_fails_when_code2session_fails":0.05,"Tests\\Feature\\MiniOnlinePaymentTest::test_create_gateway_failure_releases_bills":0.054,"Tests\\Feature\\MiniOnlinePaymentTest::test_notify_settles_payment":0.057,"Tests\\Feature\\MiniOnlinePaymentTest::test_notify_rejects_invalid_messages":0.06,"Tests\\Feature\\MiniOnlinePaymentTest::test_query_syncs_gateway_status":0.06,"Tests\\Feature\\MiniOnlinePaymentTest::test_query_rejects_other_store_payment":0.087,"Tests\\Feature\\MiniOnlinePaymentTest::test_offline_payment_flow_unaffected":0.051,"Tests\\Feature\\BillPaymentTest::test_generate_bill_positive_after_sale_added_to_total":0.114,"Tests\\Feature\\BillPaymentTest::test_generate_bill_rejects_non_numeric_after_sale":0.047,"Tests\\Feature\\ClientSpecialTest::test_batch_add_creates_specials":0.008,"Tests\\Feature\\ClientSpecialTest::test_batch_add_skips_existing_products":0.009,"Tests\\Feature\\ClientSpecialTest::test_batch_add_requires_non_empty_product_ids":0.006,"Tests\\Feature\\ClientSpecialTest::test_batch_add_forbidden_without_permission":0.007,"Tests\\Feature\\ClientSpecialTest::test_query_filters_by_product_name_keyword":0.007,"Tests\\Feature\\ClientSpecialTest::test_update_changes_sort_and_status":0.007,"Tests\\Feature\\ClientSpecialTest::test_delete_and_batch_delete":0.008,"Tests\\Feature\\ClientSpecialTest::test_batch_delete_requires_non_empty_ids":0.006,"Tests\\Feature\\MiniSpecialTest::test_guest_sees_specials_with_null_price":0.045,"Tests\\Feature\\MiniSpecialTest::test_store_sees_level_price":0.045,"Tests\\Feature\\MiniSpecialTest::test_disabled_special_and_off_shelf_product_excluded":0.046,"Tests\\Feature\\MiniSpecialTest::test_specials_sorted_by_sort_asc":0.088,"Tests\\Feature\\MiniSpecialTest::test_cart_quantity_attached_for_store":0.047,"Tests\\Feature\\MiniOnlinePaymentTest::test_aes_encryption_matches_demo_golden":0.001,"Tests\\Feature\\MiniOnlinePaymentTest::test_notify_envelope_round_trip":0.004,"Tests\\Feature\\MiniOnlinePaymentTest::test_swapped_key_config_fails_with_clear_message":0.004,"Tests\\Feature\\MiniWechatPaymentTest::test_create_wechat_payment_success":0.056,"Tests\\Feature\\MiniWechatPaymentTest::test_create_wechat_payment_gateway_failure_releases_bills":0.052,"Tests\\Feature\\MiniWechatPaymentTest::test_wechat_notify_settles_payment":0.058,"Tests\\Feature\\MiniWechatPaymentTest::test_wechat_notify_rejects_invalid_messages":0.052,"Tests\\Feature\\MiniWechatPaymentTest::test_wechat_notify_with_platform_cert_validates_signature":0.052,"Tests\\Feature\\MiniWechatPaymentTest::test_wechat_query_syncs_status":0.057,"Tests\\Feature\\MiniWechatPaymentTest::test_wangpu_channel_remains_default":0.05,"Tests\\Feature\\BillPaymentTest::test_payment_config_returns_pay_methods":0.048,"Tests\\Feature\\MiniMpPaymentTest::test_mp_scene_wechat_channel":0.076,"Tests\\Feature\\MiniMpPaymentTest::test_mp_scene_wangpu_channel":0.055,"Tests\\Feature\\MiniMpPaymentTest::test_mp_scene_oauth_failure":0.048,"Tests\\Feature\\MiniMpPaymentTest::test_mp_scene_requires_mp_config":0.047,"Tests\\Feature\\MiniMpPaymentTest::test_default_scene_remains_mini":0.053}} \ No newline at end of file diff --git a/app/Http/Controllers/Mini/OnlinePaymentController.php b/app/Http/Controllers/Mini/OnlinePaymentController.php index b50f9bf..ef212eb 100644 --- a/app/Http/Controllers/Mini/OnlinePaymentController.php +++ b/app/Http/Controllers/Mini/OnlinePaymentController.php @@ -16,10 +16,10 @@ use Modules\AnnoRoute\Attribute\RequestAttribute; use Throwable; /** - * 小程序在线支付(渠道二选一:旺铺网关 JSAPI / 微信官方支付 APIv3) + * 小程序在线支付(渠道二选一:旺铺网关 JSAPI / 微信官方支付 APIv3;场景二选一:小程序 / 公众号 H5) * - * 链路:POST /mini/payment/online 下单(返回调起支付参数) - * → 小程序 wx.requestPayment 完成支付 + * 链路:POST /mini/payment/online 下单(scene=mini 小程序 / scene=mp 公众号,返回调起支付参数) + * → 小程序 wx.requestPayment / 公众号 H5 WeixinJSBridge 完成支付 * → 渠道后台通知(旺铺 /mini/payment/notify;微信 /mini/payment/wechat-notify)验签 + 幂等结账 * → 小程序 GET /mini/payment/online/{paymentNo}/query 主动同步支付结果(回调兜底) */ @@ -33,7 +33,10 @@ class OnlinePaymentController extends BaseMiniController ) {} /** - * 发起在线支付:合并选择本店未支付账单 → 旺铺下单 → 返回调起支付参数 + * 发起在线支付:合并选择本店未支付账单 → 渠道下单 → 返回调起支付参数 + * + * scene=mini(默认):小程序 wx.login code 换 openid,wx.requestPayment 调起; + * scene=mp:公众号 H5 网页授权 code 换 openid,WeixinJSBridge getBrandWCPayRequest 调起。 * @throws Throwable */ #[PostRoute('/payment/online', authorize: true)] @@ -43,11 +46,13 @@ class OnlinePaymentController extends BaseMiniController 'bill_ids' => 'required|array|min:1', 'bill_ids.*' => 'integer|distinct', 'code' => 'required|string|max:64', + 'scene' => 'nullable|string|in:mini,mp', 'remark' => 'nullable|string|max:255', ], [ 'bill_ids.required' => '请选择要付款的账单', 'bill_ids.min' => '请选择要付款的账单', - 'code.required' => '微信登录凭证缺失,请重新进入小程序', + 'code.required' => '微信登录/授权凭证缺失,请重新进入', + 'scene.in' => '支付场景不正确', 'remark.max' => '备注超过最大长度', ]); @@ -58,6 +63,7 @@ class OnlinePaymentController extends BaseMiniController array_map('intval', $data['bill_ids']), (string) $data['code'], (string) ($data['remark'] ?? ''), + (string) ($data['scene'] ?? OnlinePaymentService::SCENE_MINI), ); return $this->success([ diff --git a/app/Http/Controllers/Mini/PaymentController.php b/app/Http/Controllers/Mini/PaymentController.php index a80c17f..3842fa7 100644 --- a/app/Http/Controllers/Mini/PaymentController.php +++ b/app/Http/Controllers/Mini/PaymentController.php @@ -6,6 +6,7 @@ use App\Exceptions\RepositoryException; use App\Models\BillModel; use App\Models\PaymentModel; use App\Services\BillNumberService; +use App\Services\WechatMpService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -48,6 +49,8 @@ class PaymentController extends BaseMiniController 'wechat_qrcode' => $resolve(site_config('pay.wechat_qrcode', '')), 'alipay_qrcode' => $resolve(site_config('pay.alipay_qrcode', '')), 'bank_info' => (string) site_config('pay.bank_info', ''), + // 公众号 AppID(H5 在微信内置浏览器拼网页授权链接使用,授权回调 code 传给 /mini/payment/online scene=mp) + 'mp_appid' => app(WechatMpService::class)->appid(), ]); } diff --git a/app/Models/StoreModel.php b/app/Models/StoreModel.php index 7a8e913..9996b03 100644 --- a/app/Models/StoreModel.php +++ b/app/Models/StoreModel.php @@ -38,6 +38,7 @@ class StoreModel extends Authenticatable 'address', 'payment_cycle_days', 'openid', + 'mp_openid', 'status', 'remark', ]; diff --git a/app/Services/OnlinePaymentService.php b/app/Services/OnlinePaymentService.php index ecdce00..b74a3d6 100644 --- a/app/Services/OnlinePaymentService.php +++ b/app/Services/OnlinePaymentService.php @@ -32,11 +32,17 @@ class OnlinePaymentService /** 在线支付渠道:微信官方支付 */ public const string CHANNEL_WECHAT = 'wechat'; + /** 支付场景:微信小程序(wx.login 换 openid,wx.requestPayment 调起) */ + public const string SCENE_MINI = 'mini'; + /** 支付场景:公众号 H5(网页授权换 openid,WeixinJSBridge 调起) */ + public const string SCENE_MP = 'mp'; + public function __construct( protected BillNumberService $billNumber, protected WangpuPayService $wangpu, protected WechatPayService $wxpay, protected WechatMiniService $wechat, + protected WechatMpService $wechatMp, ) {} /** @@ -55,20 +61,29 @@ class OnlinePaymentService * 发起在线支付 * * @param array $billIds 合并付款的账单ID - * @param string $code 小程序 wx.login() 返回的登录凭证 + * @param string $code 小程序 wx.login() 登录凭证 / 公众号网页授权 code(按 scene 区分) + * @param string $scene 支付场景:mini=小程序(默认) / mp=公众号 H5 * @return array{0: PaymentModel, 1: array} 支付单与渠道返回的调起支付参数 * @throws Throwable */ - public function create(StoreModel $store, array $billIds, string $code, string $remark = ''): array + public function create(StoreModel $store, array $billIds, string $code, string $remark = '', string $scene = self::SCENE_MINI): array { $channel = $this->channel(); $payMethod = $channel === self::CHANNEL_WECHAT ? PaymentModel::METHOD_WECHAT : PaymentModel::METHOD_WANGPU; - // 换取付款人 openid 并绑定到门店(下次可直接复用) - $openid = $this->wechat->code2session($code); - if ((string) $store->openid !== $openid) { - $store->openid = $openid; - $store->save(); + // 换取付款人 openid 并绑定到门店(小程序/公众号分场景绑定,两者 openid 维度不同不可混用) + if ($scene === self::SCENE_MP) { + $openid = $this->wechatMp->code2openid($code); + if ((string) $store->mp_openid !== $openid) { + $store->mp_openid = $openid; + $store->save(); + } + } else { + $openid = $this->wechat->code2session($code); + if ((string) $store->openid !== $openid) { + $store->openid = $openid; + $store->save(); + } } [$payment, $bills] = DB::transaction(function () use ($store, $billIds, $openid, $remark, $payMethod) { @@ -122,12 +137,14 @@ class OnlinePaymentService (string) $payment->amount, $openid, '账单合并付款-' . $payment->payment_no, + $scene === self::SCENE_MP ? $this->wechatMp->appid() : '', ) : $this->wangpu->createOrder([ 'mer_order_id' => $payment->payment_no, 'order_amt' => (string) $payment->amount, 'open_id' => $openid, - 'sub_appid' => $this->subAppid(), + 'sub_appid' => $this->subAppid($scene), + 'payway_code' => $this->wangpu->paywayCode($scene), 'order_title' => '账单合并付款-' . $payment->payment_no, 'notifyurl' => $this->wangpu->notifyUrl(), ]); @@ -391,10 +408,13 @@ class OnlinePaymentService } /** - * 下单微信子 appid(默认取小程序 appid) + * 下单微信子 appid(默认取小程序 appid;公众号 H5 场景取公众号 appid) */ - protected function subAppid(): string + protected function subAppid(string $scene = self::SCENE_MINI): string { + if ($scene === self::SCENE_MP) { + return $this->wechatMp->appid(); + } $subAppid = (string) site_config('wangpu.sub_appid', ''); if ($subAppid === '') { $subAppid = (string) config('services.wangpu.sub_appid', ''); diff --git a/app/Services/WangpuPayService.php b/app/Services/WangpuPayService.php index 0967e41..65be1ce 100644 --- a/app/Services/WangpuPayService.php +++ b/app/Services/WangpuPayService.php @@ -90,6 +90,20 @@ class WangpuPayService return rtrim((string) config('app.url'), '/') . '/mini/payment/notify'; } + /** + * 支付方式代码(公众号 H5 场景优先取 mp_payway_code,留空回退 payway_code) + */ + public function paywayCode(string $scene = 'mini'): string + { + if ($scene === 'mp') { + $mpPayway = $this->config('mp_payway_code'); + if ($mpPayway !== '') { + return $mpPayway; + } + } + return $this->config('payway_code'); + } + /** * 网关 POST 请求:业务报文加密装信封 → JSON 发送 → 校验应答码 → 解密应答 * diff --git a/app/Services/WechatMpService.php b/app/Services/WechatMpService.php new file mode 100644 index 0000000..2341b59 --- /dev/null +++ b/app/Services/WechatMpService.php @@ -0,0 +1,72 @@ +appid(); + $secret = $this->config('mp_secret'); + if ($appid === '' || $secret === '') { + throw new RepositoryException('微信公众号未配置 AppID/Secret,请联系管理员'); + } + + $result = Http::timeout(10)->withOptions([ + 'verify' => false, // 禁用 SSL 证书验证 + ])->get('https://api.weixin.qq.com/sns/oauth2/access_token', [ + 'appid' => $appid, + 'secret' => $secret, + 'code' => $code, + 'grant_type' => 'authorization_code', + ])->json(); + + $openid = is_array($result) ? (string) ($result['openid'] ?? '') : ''; + if ($openid === '') { + Log::warning('微信网页授权失败', ['response' => $result]); + throw new RepositoryException('微信网页授权失败:' . ($result['errmsg'] ?? '请重新授权')); + } + + return $openid; + } + + /** + * 公众号 AppID(公众号 JSAPI 下单 appid、H5 拼网页授权链接使用) + */ + public function appid(): string + { + return $this->config('mp_appid'); + } + + /** + * 读取应用配置:后台站点配置(微信应用配置分组)优先,为空回退 config/services.php(env) + */ + protected function config(string $key): string + { + $value = site_config('wechat.' . $key, ''); + if ($value === null || $value === '') { + $value = match ($key) { + 'mp_appid' => config('services.wechat.mp.appid', ''), + 'mp_secret' => config('services.wechat.mp.secret', ''), + default => '', + }; + } + return trim((string) $value); + } +} diff --git a/app/Services/WechatPayService.php b/app/Services/WechatPayService.php index ab41407..3f1ae8c 100644 --- a/app/Services/WechatPayService.php +++ b/app/Services/WechatPayService.php @@ -29,19 +29,22 @@ class WechatPayService protected ?Application $application = null; /** - * 统一下单(小程序 JSAPI) + * 统一下单(JSAPI:小程序 / 公众号 H5 通用) * * @param string $paymentNo 商户订单号(本系统支付单号) * @param string $amountYuan 金额(元,两位小数) - * @param string $openid 付款人 openid + * @param string $openid 付款人 openid(按下单 appid 维度:小程序 openid 或公众号 openid) * @param string $description 订单标题 - * @return array wx.requestPayment 调起参数(timeStamp/nonceStr/package/signType/paySign) + * @param string $appid 下单应用 appid(公众号 JSAPI 传公众号 appid;留空取配置的小程序 appid) + * @return array 调起支付参数(appId/timeStamp/nonceStr/package/signType/paySign, + * 小程序喂 wx.requestPayment,公众号 H5 喂 WeixinJSBridge getBrandWCPayRequest,结构同构) * @throws RepositoryException 未配置或下单失败 */ - public function createOrder(string $paymentNo, string $amountYuan, string $openid, string $description): array + public function createOrder(string $paymentNo, string $amountYuan, string $openid, string $description, string $appid = ''): array { + $appid = trim($appid) !== '' ? trim($appid) : $this->appid(); $result = $this->call('POST', '/v3/pay/transactions/jsapi', [ - 'appid' => $this->appid(), + 'appid' => $appid, 'mchid' => $this->config('mch_id'), 'description' => mb_substr($description, 0, 127), 'out_trade_no' => $paymentNo, @@ -59,8 +62,9 @@ class WechatPayService throw new RepositoryException('微信支付下单失败:应答缺少 prepay_id'); } - $params = $this->app()->getUtils()->buildMiniAppConfig($prepayId, $this->appid()); - Log::driver('pay')->info('微信支付下单成功', ['out_trade_no' => $paymentNo, 'prepay_id' => $prepayId]); + // buildMiniAppConfig 即 buildBridgeConfig(小程序 wx.requestPayment 与公众号 WeixinJSBridge 参数同构) + $params = $this->app()->getUtils()->buildMiniAppConfig($prepayId, $appid); + Log::driver('pay')->info('微信支付下单成功', ['out_trade_no' => $paymentNo, 'prepay_id' => $prepayId, 'appid' => $appid]); return $params; } diff --git a/config/services.php b/config/services.php index 00ceaaf..69b0dbb 100644 --- a/config/services.php +++ b/config/services.php @@ -46,12 +46,17 @@ return [ /* * 微信小程序(登录 code2Session / 手机号授权),AppID/Secret 需业务方提供 * 优先读取后台「系统设置 → 微信应用配置」(site_config wechat.mini_appid / wechat.mini_secret),为空时回退到这里的 env 配置 + * 公众号(mp):网页授权换 openid、公众号 JSAPI 支付使用,后台 site_config wechat.mp_appid / wechat.mp_secret 优先 */ 'wechat' => [ 'mini' => [ 'appid' => env('WECHAT_MINI_APPID', ''), 'secret' => env('WECHAT_MINI_SECRET', ''), ], + 'mp' => [ + 'appid' => env('WECHAT_MP_APPID', ''), + 'secret' => env('WECHAT_MP_SECRET', ''), + ], ], /* @@ -75,7 +80,8 @@ return [ 'public_key' => env('WANGPU_PUBLIC_KEY', ''), // 旺铺平台公钥(base64 单行,加密请求报文 AES 密钥) 'private_key' => env('WANGPU_PRIVATE_KEY', ''), // 商户 RSA 私钥(base64 单行,解密网关应答/通知报文) 'sub_appid' => env('WANGPU_SUB_APPID', ''), // 下单微信子 appid(默认取小程序 appid) - 'payway_code' => env('WANGPU_PAYWAY_CODE', 'WECHAT_MINI'), // 支付方式代码(主扫必填) + 'payway_code' => env('WANGPU_PAYWAY_CODE', 'WECHAT_MINI'), // 支付方式代码(小程序主扫必填) + 'mp_payway_code' => env('WANGPU_MP_PAYWAY_CODE', ''), // 公众号场景支付方式代码(留空回退 payway_code) ], /* diff --git a/database/migrations/2026_07_23_030608_create_store_table.php b/database/migrations/2026_07_23_030608_create_store_table.php index a99a280..202afc5 100644 --- a/database/migrations/2026_07_23_030608_create_store_table.php +++ b/database/migrations/2026_07_23_030608_create_store_table.php @@ -43,6 +43,7 @@ return new class extends Migration $table->string('address', 255)->default('')->comment('门店地址'); $table->integer('payment_cycle_days')->default(0)->comment('回款周期(天)'); $table->string('openid', 64)->default('')->comment('微信小程序 openid(在线支付付款人标识,wx.login 换取后绑定)'); + $table->string('mp_openid', 64)->default('')->comment('微信公众号 openid(公众号 JSAPI 支付付款人标识,网页授权换取后绑定)'); $table->decimal('total_purchase_amount', 12, 2)->default(0)->comment('总采购金额(只统计商品金额,账单支付后累加)'); $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); $table->string('remark', 255)->nullable()->default('')->comment('备注'); diff --git a/database/seeders/SysDataSeeder.php b/database/seeders/SysDataSeeder.php index 09b05bd..dbd000a 100644 --- a/database/seeders/SysDataSeeder.php +++ b/database/seeders/SysDataSeeder.php @@ -44,6 +44,7 @@ class SysDataSeeder extends Seeder ['id' => 17, 'group_id' => 5, 'key' => 'public_key', 'title' => '旺铺平台公钥', 'describe' => '旺铺平台 RSA 公钥(base64 单行,不含 PEM 头尾),用于加密请求报文 AES 密钥,旺铺提供', 'values' => '', 'type' => 'TextArea','options' => "", 'sort' => 5, 'created_at' => $date, 'updated_at' => $date], ['id' => 18, 'group_id' => 5, 'key' => 'sub_appid', 'title' => '旺铺下单子appid', 'describe' => '下单微信子 appid(sub_appid),留空则取小程序自身 appid', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 6, 'created_at' => $date, 'updated_at' => $date], ['id' => 19, 'group_id' => 5, 'key' => 'payway_code', 'title' => '旺铺支付方式代码', 'describe' => '支付方式代码 payway_code(小程序主扫必填,如 WECHAT_MINI),见旺铺数据词典', 'values' => 'WECHAT_MINI', 'type' => 'Input','options' => "", 'sort' => 7, 'created_at' => $date, 'updated_at' => $date], + ['id' => 34, 'group_id' => 5, 'key' => 'mp_payway_code', 'title' => '旺铺公众号支付方式代码', 'describe' => '公众号 H5 场景支付方式代码 payway_code(如 WECHAT_JSPAY),留空则回退小程序支付方式代码', 'values' => 'WECHAT_JSPAY', 'type' => 'Input','options' => "", 'sort' => 9, 'created_at' => $date, 'updated_at' => $date], ['id' => 20, 'group_id' => 5, 'key' => 'private_key', 'title' => '商户RSA私钥', 'describe' => '商户 RSA 私钥(base64 单行,不含 PEM 头尾),用于解密网关应答/支付通知报文,请勿泄露', 'values' => '', 'type' => 'TextArea','options' => "", 'sort' => 8, 'created_at' => $date, 'updated_at' => $date], // 微信官方支付(wxpay):微信支付 APIv3 ['id' => 22, 'group_id' => 6, 'key' => 'mch_id', 'title' => '微信支付商户号', 'describe' => '微信支付商户号 mch_id(微信商户平台分配)', 'values' => '', 'type' => 'Input','options' => "", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date], diff --git a/tests/Feature/MiniMpPaymentTest.php b/tests/Feature/MiniMpPaymentTest.php new file mode 100644 index 0000000..f591091 --- /dev/null +++ b/tests/Feature/MiniMpPaymentTest.php @@ -0,0 +1,336 @@ + 'wx-mp-test', + 'services.wechat.mp.secret' => 'wx-mp-secret', + ]); + } + + /** 模拟公众号网页授权 code 换 openid 成功 */ + private function fakeMpOauth(string $openid = 'oMpOpenid001'): void + { + Http::fake([ + 'https://api.weixin.qq.com/*' => Http::response([ + 'access_token' => 'mp-access-token', + 'expires_in' => 7200, + 'refresh_token' => 'mp-refresh-token', + 'openid' => $openid, + 'scope' => 'snsapi_base', + ], 200), + ]); + } + + /** 以 MockHttpClient 替换微信支付接口应答,并注入容器(控制器经由容器取同一实例) */ + private function fakeWechatApi(callable $callback): void + { + $service = app(WechatPayService::class); + $service->app()->setHttpClient(new MockHttpClient($callback, 'https://api.mch.weixin.qq.com')); + $this->app->instance(WechatPayService::class, $service); + } + + /** 密钥 PEM 包装(与服务内实现一致) */ + private function pem(string $body, string $kind): string + { + return "-----BEGIN {$kind} KEY-----\n" . wordwrap($body, 64, "\n", true) . "\n-----END {$kind} KEY-----"; + } + + /** + * 模拟网关加密信封(demo 协议:AES-128-ECB 加密报文 + RSA 公钥加密 AES 密钥) + * + * @param array $data 业务报文 + * @return array + */ + private function gatewayEnvelope(array $data): array + { + $key = Str::random(16); + openssl_public_encrypt($key, $encryptedKey, $this->pem(self::WANGPU_PUBLIC_KEY, 'PUBLIC')); + return [ + 'serialNo' => Str::random(32), + 'version' => '1.0', + 'timestamp' => now()->format('YmdHis'), + 'data' => base64_encode((string) openssl_encrypt((string) json_encode($data), 'AES-128-ECB', $key, OPENSSL_RAW_DATA)), + 'signature' => base64_encode($encryptedKey), + 'extras' => '', + 'organizNo' => 'org001', + ]; + } + + /** + * 解密我方发往网关的请求信封(断言上送报文用;服务侧 urlencode 需先解码) + * + * @param array $body 请求信封 + * @return array + */ + private function decryptRequest(array $body): array + { + openssl_private_decrypt( + (string) base64_decode(urldecode((string) $body['signature'])), + $key, + $this->pem(self::WANGPU_PRIVATE_KEY, 'PRIVATE') + ); + $plain = openssl_decrypt( + (string) base64_decode(urldecode((string) $body['data'])), + 'AES-128-ECB', + (string) $key, + OPENSSL_RAW_DATA + ); + return (array) json_decode((string) $plain, true); + } + + /** 旺铺渠道测试配置 */ + private function configWangpu(): void + { + config([ + 'services.wangpu.base_url' => 'https://wangpu.test', + 'services.wangpu.organiz_no' => 'org001', + 'services.wangpu.mer_no' => 'mer001', + 'services.wangpu.mer_code' => 'code001', + 'services.wangpu.term_code' => 'term001', + 'services.wangpu.public_key' => self::WANGPU_PUBLIC_KEY, + 'services.wangpu.private_key' => self::WANGPU_PRIVATE_KEY, + 'services.wangpu.payway_code' => 'WECHAT_MINI', + 'services.wangpu.mp_payway_code' => 'WECHAT_JSPAY', + ]); + } + + /** 造一张指定金额的未支付账单(总额=商品金额) */ + private function makeBill(StoreModel $store, string $amount, array $attributes = []): BillModel + { + return BillModel::create(array_merge([ + 'bill_no' => 'ZD' . random_int(100000000000, 999999999999), + 'purchase_id' => PurchaseOrderModel::factory()->create()->id, + 'store_id' => $store->id, + 'bill_date' => '2026-09-04', + 'product_amount' => $amount, + 'delivery_fee' => '0.00', + 'box_num' => 0, + 'tray_num' => 0, + 'box_price' => '0.00', + 'tray_price' => '0.00', + 'added_amount' => '0.00', + 'total_amount' => $amount, + 'status' => BillModel::STATUS_UNPAID, + ], $attributes)); + } + + /** 微信官方渠道公众号下单:JSAPI appid=公众号、付款人=公众号 openid、调起参数同构、mp_openid 绑定且不覆盖小程序 openid */ + public function test_mp_scene_wechat_channel(): void + { + config([ + 'services.pay.online_channel' => 'wechat', + 'services.wxpay.mch_id' => '1630000001', + 'services.wxpay.private_key' => self::WXPAY_PRIVATE_KEY, + 'services.wxpay.certificate' => self::WXPAY_CERTIFICATE, + 'services.wxpay.secret_key' => self::WXPAY_SECRET_KEY, + ]); + + $this->fakeMpOauth(); + $captured = []; + $this->fakeWechatApi(function (string $method, string $url, array $options) use (&$captured) { + $captured = compact('method', 'url', 'options'); + return new MockResponse((string) json_encode(['prepay_id' => 'wx-mp-prepay-001']), ['http_code' => 200]); + }); + + $store = StoreModel::factory()->create(['openid' => 'oMiniOpenidKeep']); + $bill = $this->makeBill($store, '66.00'); + + $this->actingAsMiniStore($store); + $response = $this->postJson('/mini/payment/online', [ + 'bill_ids' => [$bill->id], + 'code' => 'mp-oauth-code', + 'scene' => 'mp', + ])->assertJsonPath('success', true); + + // 调起参数:appId=公众号 appid,package 含 prepay_id(H5 喂 WeixinJSBridge getBrandWCPayRequest) + $payParams = $response->json('data.pay_params'); + $this->assertSame('wx-mp-test', $payParams['appId']); + $this->assertSame('prepay_id=wx-mp-prepay-001', $payParams['package']); + $this->assertNotEmpty($payParams['timeStamp']); + $this->assertNotEmpty($payParams['nonceStr']); + $this->assertSame('RSA', $payParams['signType']); + $this->assertNotEmpty($payParams['paySign']); + + // 上送微信报文:appid=公众号 appid、payer=公众号 openid、金额元转分 + $body = (array) json_decode((string) ($captured['options']['body'] ?? ''), true); + $this->assertSame('wx-mp-test', $body['appid'] ?? null); + $this->assertSame('1630000001', $body['mchid'] ?? null); + $this->assertSame('oMpOpenid001', $body['payer']['openid'] ?? null); + $this->assertSame(6600, $body['amount']['total'] ?? null); + + // 支付单与门店绑定:公众号 openid 写 mp_openid,不覆盖小程序 openid + $payment = PaymentModel::where('payment_no', $response->json('data.payment_no'))->first(); + $this->assertSame('oMpOpenid001', $payment->openid); + $store->refresh(); + $this->assertSame('oMpOpenid001', $store->mp_openid); + $this->assertSame('oMiniOpenidKeep', $store->openid, '公众号场景不得覆盖小程序 openid'); + } + + /** 旺铺渠道公众号下单:sub_appid=公众号 appid、payway_code=公众号支付方式代码、open_id=公众号 openid */ + public function test_mp_scene_wangpu_channel(): void + { + $this->configWangpu(); + $this->fakeMpOauth(); + Http::fake([ + 'https://api.weixin.qq.com/*' => Http::response(['openid' => 'oMpOpenid001', 'access_token' => 'at'], 200), + 'https://wangpu.test/industrial/payment/order' => Http::response( + ['code' => '0000', 'msg' => '调用成功'] + $this->gatewayEnvelope([ + 'order_id' => 'WP202609040001', + 'tradeNo' => 'T20260904001', + 'user_openid' => 'oMpOpenid001', + ]), + 200 + ), + ]); + + $store = StoreModel::factory()->create(); + $bill = $this->makeBill($store, '88.00'); + + $this->actingAsMiniStore($store); + $response = $this->postJson('/mini/payment/online', [ + 'bill_ids' => [$bill->id], + 'code' => 'mp-oauth-code', + 'scene' => 'mp', + ])->assertJsonPath('success', true); + + $paymentNo = $response->json('data.payment_no'); + $this->assertSame('oMpOpenid001', $store->fresh()->mp_openid); + + // 上送网关的加密信封:解出业务报文校验公众号 sub_appid / 公众号 payway_code / 公众号 openid + Http::assertSent(function ($request) use ($paymentNo) { + if (! str_contains($request->url(), '/industrial/payment/order')) { + return false; + } + $plain = $this->decryptRequest($request->data()); + return ($plain['mer_order_id'] ?? '') === $paymentNo + && ($plain['order_amt'] ?? '') === '88.00' + && ($plain['open_id'] ?? '') === 'oMpOpenid001' + && ($plain['sub_appid'] ?? '') === 'wx-mp-test' + && ($plain['payway_code'] ?? '') === 'WECHAT_JSPAY' + && ! empty($plain['notifyurl']); + }); + } + + /** 公众号网页授权失败:errcode 应答 → 下单拒绝并提示授权失败 */ + public function test_mp_scene_oauth_failure(): void + { + Http::fake([ + 'https://api.weixin.qq.com/*' => Http::response(['errcode' => 40029, 'errmsg' => 'invalid code'], 200), + ]); + + $store = StoreModel::factory()->create(); + $bill = $this->makeBill($store, '10.00'); + + $this->actingAsMiniStore($store); + $response = $this->postJson('/mini/payment/online', [ + 'bill_ids' => [$bill->id], + 'code' => 'bad-code', + 'scene' => 'mp', + ])->assertJsonPath('success', false); + + $this->assertStringContainsString('微信网页授权失败', (string) $response->json('msg')); + // 授权失败不产生支付单、账单不被锁定 + $this->assertSame(0, PaymentModel::count()); + $this->assertSame(0, $bill->fresh()->payment_id); + } + + /** 公众号未配置 AppID/Secret:明确报错 */ + public function test_mp_scene_requires_mp_config(): void + { + config([ + 'services.wechat.mp.appid' => '', + 'services.wechat.mp.secret' => '', + ]); + + $store = StoreModel::factory()->create(); + $bill = $this->makeBill($store, '10.00'); + + $this->actingAsMiniStore($store); + $response = $this->postJson('/mini/payment/online', [ + 'bill_ids' => [$bill->id], + 'code' => 'mp-oauth-code', + 'scene' => 'mp', + ])->assertJsonPath('success', false); + + $this->assertStringContainsString('微信公众号未配置', (string) $response->json('msg')); + $this->assertSame(0, PaymentModel::count()); + } + + /** 不传 scene 默认走小程序链路(code2session,小程序 appid 下单) */ + public function test_default_scene_remains_mini(): void + { + config([ + 'services.wechat.mini.appid' => 'wx-mini-test', + 'services.wechat.mini.secret' => 'wx-secret-test', + 'services.pay.online_channel' => 'wechat', + 'services.wxpay.mch_id' => '1630000001', + 'services.wxpay.private_key' => self::WXPAY_PRIVATE_KEY, + 'services.wxpay.certificate' => self::WXPAY_CERTIFICATE, + 'services.wxpay.secret_key' => self::WXPAY_SECRET_KEY, + ]); + + Http::fake([ + 'https://api.weixin.qq.com/*' => Http::response(['openid' => 'oMiniOpenid001', 'session_key' => 'sk'], 200), + ]); + $captured = []; + $this->fakeWechatApi(function (string $method, string $url, array $options) use (&$captured) { + $captured = compact('method', 'url', 'options'); + return new MockResponse((string) json_encode(['prepay_id' => 'wx-mini-prepay-001']), ['http_code' => 200]); + }); + + $store = StoreModel::factory()->create(); + $bill = $this->makeBill($store, '20.00'); + + $this->actingAsMiniStore($store); + $response = $this->postJson('/mini/payment/online', [ + 'bill_ids' => [$bill->id], + 'code' => 'wx-login-code', + ])->assertJsonPath('success', true); + + // 小程序链路:appId=小程序 appid、openid 绑定到 store.openid,mp_openid 保持空 + $this->assertSame('wx-mini-test', $response->json('data.pay_params.appId')); + $body = (array) json_decode((string) ($captured['options']['body'] ?? ''), true); + $this->assertSame('wx-mini-test', $body['appid'] ?? null); + $store->refresh(); + $this->assertSame('oMiniOpenid001', $store->openid); + $this->assertSame('', (string) $store->mp_openid); + } +}