Преглед изворни кода

Merge branch 'dev' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev

chengwl пре 3 дана
родитељ
комит
219f3694cb
82 измењених фајлова са 2706 додато и 169 уклоњено
  1. 82 80
      bootstrap/cache/services.php
  2. 1 0
      bootstrap/providers.php
  3. 1 0
      composer.json
  4. 37 0
      packages/Longyi/Gift/src/Database/Migrations/2026_08_11_000003_add_invoiced_fields_to_orders_table.php
  5. 29 0
      packages/Longyi/Gift/src/Listeners/ApplepayBreakdownHandler.php
  6. 4 0
      packages/Longyi/Gift/src/Providers/EventServiceProvider.php
  7. 10 2
      packages/Longyi/Gift/src/Repositories/CustomInvoiceRepository.php
  8. 19 11
      packages/Longyi/Gift/src/Repositories/CustomOrderRepository.php
  9. 12 0
      packages/Longyi/Gift/src/Resources/views/sales/invoices/view.blade.php
  10. 18 0
      packages/Longyi/Member/src/DataGrids/Member/MemberDataGrid.php
  11. 28 0
      packages/Longyi/Member/src/Database/Migrations/2026_08_10_000000_add_previous_expire_date_to_member_log_table.php
  12. 29 0
      packages/Longyi/Member/src/Listeners/ApplepayBreakdownHandler.php
  13. 41 26
      packages/Longyi/Member/src/Listeners/OrderPlacedHandler.php
  14. 107 0
      packages/Longyi/Member/src/Listeners/VipRevokeHandler.php
  15. 2 0
      packages/Longyi/Member/src/Models/MemberLog.php
  16. 8 1
      packages/Longyi/Member/src/Providers/EventServiceProvider.php
  17. 3 0
      packages/Longyi/Pay/Afterpay/src/Payment/Afterpay.php
  18. 24 0
      packages/Longyi/Pay/Applepay/src/Events/CollectingAmountBreakdown.php
  19. 23 22
      packages/Longyi/Pay/Applepay/src/Payment/Applepay.php
  20. 167 0
      packages/Longyi/Pay/Applepay/src/Support/PayPalBreakdown.php
  21. 3 0
      packages/Longyi/Pay/AwxAfterpay/src/Payment/AwxAfterpay.php
  22. 3 0
      packages/Longyi/Pay/AwxKlarna/src/Payment/AwxKlarna.php
  23. 3 0
      packages/Longyi/Pay/Klarna/src/Payment/Klarna.php
  24. 14 1
      packages/Longyi/RewardPoints/src/Config/TransactionType.php
  25. 38 0
      packages/Longyi/RewardPoints/src/Database/Migrations/2026_08_18_000001_add_reward_points_fields_to_cart_table.php
  26. 40 0
      packages/Longyi/RewardPoints/src/Database/Migrations/2026_08_21_000001_add_reward_points_fields_to_orders_table.php
  27. 54 0
      packages/Longyi/RewardPoints/src/Database/Migrations/2026_08_21_000002_add_reward_points_invoiced_fields.php
  28. 39 0
      packages/Longyi/RewardPoints/src/Listeners/OrderEvents.php
  29. 49 0
      packages/Longyi/RewardPoints/src/Listeners/RewardPointsHandler.php
  30. 7 0
      packages/Longyi/RewardPoints/src/Providers/RewardPointsServiceProvider.php
  31. 2 0
      packages/Longyi/RewardPoints/src/Resources/lang/en/rewardpoints.php
  32. 2 0
      packages/Longyi/RewardPoints/src/Resources/lang/zh_CN/rewardpoints.php
  33. 10 0
      packages/Longyi/ShippingInsurance/src/Config/acl.php
  34. 11 0
      packages/Longyi/ShippingInsurance/src/Config/admin-menu.php
  35. 38 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000000_add_shipping_insurance_to_cart_table.php
  36. 32 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000001_add_shipping_insurance_meta_to_cart_table.php
  37. 26 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000002_add_use_shipping_insurance_to_cart_table.php
  38. 32 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000003_add_shipping_insurance_type_value_to_cart_table.php
  39. 29 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000000_add_shipping_insurance_to_orders_table.php
  40. 29 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000001_add_shipping_insurance_to_invoices_table.php
  41. 29 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000002_add_shipping_insurance_invoiced_to_orders_table.php
  42. 65 0
      packages/Longyi/ShippingInsurance/src/Http/Controllers/Admin/ShippingInsuranceController.php
  43. 42 0
      packages/Longyi/ShippingInsurance/src/Http/Controllers/Shop/ShippingInsuranceController.php
  44. 24 0
      packages/Longyi/ShippingInsurance/src/Listeners/ApplepayBreakdownHandler.php
  45. 37 0
      packages/Longyi/ShippingInsurance/src/Listeners/OrderViewHandler.php
  46. 78 0
      packages/Longyi/ShippingInsurance/src/Listeners/ShippingInsuranceHandler.php
  47. 39 0
      packages/Longyi/ShippingInsurance/src/Providers/EventServiceProvider.php
  48. 49 0
      packages/Longyi/ShippingInsurance/src/Providers/ShippingInsuranceServiceProvider.php
  49. 27 0
      packages/Longyi/ShippingInsurance/src/Resources/lang/en/app.php
  50. 27 0
      packages/Longyi/ShippingInsurance/src/Resources/lang/zh_CN/app.php
  51. 102 0
      packages/Longyi/ShippingInsurance/src/Resources/views/admin/index.blade.php
  52. 89 0
      packages/Longyi/ShippingInsurance/src/Resources/views/components/shipping-insurance-cartsummary.blade.php
  53. 16 0
      packages/Longyi/ShippingInsurance/src/Resources/views/components/shipping-insurance-ordersummary.blade.php
  54. 11 0
      packages/Longyi/ShippingInsurance/src/Routes/admin-routes.php
  55. 9 0
      packages/Longyi/ShippingInsurance/src/Routes/shop-routes.php
  56. 44 0
      packages/Longyi/ShippingInsurance/src/Services/ShippingInsuranceService.php
  57. 48 0
      packages/Webkul/Admin/src/Resources/views/sales/refunds/create.blade.php
  58. 64 1
      packages/Webkul/Admin/src/Resources/views/sales/refunds/view.blade.php
  59. 26 0
      packages/Webkul/BagistoApi/src/Database/Migrations/2026_08_15_000000_add_like_count_to_product_reviews_table.php
  60. 105 0
      packages/Webkul/BagistoApi/src/Dto/CartData.php
  61. 12 0
      packages/Webkul/BagistoApi/src/Dto/CustomerOrder/CustomerOrderDetailDto.php
  62. 8 0
      packages/Webkul/BagistoApi/src/Dto/CustomerOrder/CustomerOrderListDto.php
  63. 0 0
      packages/Webkul/BagistoApi/src/Dto/ProductDetail/ProductAttributeDto.php
  64. 15 0
      packages/Webkul/BagistoApi/src/Dto/ProductDetail/ProductDetailDto.php
  65. 11 0
      packages/Webkul/BagistoApi/src/Dto/SaveCheckoutCartInput.php
  66. 16 0
      packages/Webkul/BagistoApi/src/Models/AttributeOption.php
  67. 6 0
      packages/Webkul/BagistoApi/src/Models/CustomerOrder.php
  68. 66 0
      packages/Webkul/BagistoApi/src/Models/Product.php
  69. 70 3
      packages/Webkul/BagistoApi/src/Models/ProductReview.php
  70. 6 1
      packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php
  71. 96 2
      packages/Webkul/BagistoApi/src/State/FilterableAttributesProvider.php
  72. 4 0
      packages/Webkul/BagistoApi/src/State/ProductDetailProvider.php
  73. 71 0
      packages/Webkul/BagistoApi/src/State/ProductReviewLikeProcessor.php
  74. 25 15
      packages/Webkul/BagistoApi/src/State/ProductReviewProcessor.php
  75. 10 0
      packages/Webkul/BagistoApi/src/State/ProductReviewProvider.php
  76. 84 0
      packages/Webkul/BagistoApi/src/State/SaveCheckoutCartProcessor.php
  77. 9 0
      packages/Webkul/BagistoApi/src/Transformers/ExpressOrderResource.php
  78. 4 1
      packages/Webkul/Checkout/src/Models/Cart.php
  79. 92 3
      packages/Webkul/Sales/src/Repositories/RefundRepository.php
  80. 9 0
      packages/Webkul/Sales/src/Transformers/OrderResource.php
  81. 15 0
      packages/Webkul/Shop/src/Http/Resources/CartResource.php
  82. 40 0
      packages/Webkul/Shop/src/Resources/views/customers/account/orders/pdf.blade.php

+ 82 - 80
bootstrap/cache/services.php

@@ -60,46 +60,47 @@
     56 => 'Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider',
     57 => 'Longyi\\Member\\Providers\\MemberServiceProvider',
     58 => 'Longyi\\Gift\\Providers\\GiftServiceProvider',
-    59 => 'Longyi\\Pay\\AwxAfterpay\\Providers\\AwxAfterpayServiceProvider',
-    60 => 'Longyi\\Pay\\AwxKlarna\\Providers\\AwxKlarnaServiceProvider',
-    61 => 'Longyi\\Pay\\Applepay\\Providers\\ApplepayServiceProvider',
-    62 => 'Longyi\\Pay\\Airwallex\\Providers\\AirwallexServiceProvider',
-    63 => 'Longyi\\Pay\\Afterpay\\Providers\\AfterpayServiceProvider',
-    64 => 'Longyi\\Pay\\Klarna\\Providers\\KlarnaServiceProvider',
-    65 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
-    66 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
-    67 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
-    68 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
-    69 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
-    70 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
-    71 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
-    72 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
-    73 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
-    74 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
-    75 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
-    76 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
-    77 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
-    78 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
-    79 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
-    80 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
-    81 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
-    82 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
-    83 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
-    84 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
-    85 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
-    86 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
-    87 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
-    88 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
-    89 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
-    90 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
-    91 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
-    92 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
-    93 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
-    94 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
-    95 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
-    96 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
-    97 => 'Webkul\\User\\Providers\\UserServiceProvider',
-    98 => 'Webkul\\BagistoApi\\Providers\\BagistoApiServiceProvider',
+    59 => 'Longyi\\ShippingInsurance\\Providers\\ShippingInsuranceServiceProvider',
+    60 => 'Longyi\\Pay\\AwxAfterpay\\Providers\\AwxAfterpayServiceProvider',
+    61 => 'Longyi\\Pay\\AwxKlarna\\Providers\\AwxKlarnaServiceProvider',
+    62 => 'Longyi\\Pay\\Applepay\\Providers\\ApplepayServiceProvider',
+    63 => 'Longyi\\Pay\\Airwallex\\Providers\\AirwallexServiceProvider',
+    64 => 'Longyi\\Pay\\Afterpay\\Providers\\AfterpayServiceProvider',
+    65 => 'Longyi\\Pay\\Klarna\\Providers\\KlarnaServiceProvider',
+    66 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
+    67 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
+    68 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
+    69 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
+    70 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
+    71 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
+    72 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
+    73 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
+    74 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
+    75 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
+    76 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
+    77 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
+    78 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
+    79 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
+    80 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
+    81 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
+    82 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
+    83 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
+    84 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
+    85 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
+    86 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
+    87 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
+    88 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
+    89 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
+    90 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
+    91 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
+    92 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
+    93 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
+    94 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
+    95 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
+    96 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
+    97 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
+    98 => 'Webkul\\User\\Providers\\UserServiceProvider',
+    99 => 'Webkul\\BagistoApi\\Providers\\BagistoApiServiceProvider',
   ),
   'eager' => 
   array (
@@ -145,46 +146,47 @@
     39 => 'Longyi\\RewardPoints\\Providers\\RewardPointsServiceProvider',
     40 => 'Longyi\\Member\\Providers\\MemberServiceProvider',
     41 => 'Longyi\\Gift\\Providers\\GiftServiceProvider',
-    42 => 'Longyi\\Pay\\AwxAfterpay\\Providers\\AwxAfterpayServiceProvider',
-    43 => 'Longyi\\Pay\\AwxKlarna\\Providers\\AwxKlarnaServiceProvider',
-    44 => 'Longyi\\Pay\\Applepay\\Providers\\ApplepayServiceProvider',
-    45 => 'Longyi\\Pay\\Airwallex\\Providers\\AirwallexServiceProvider',
-    46 => 'Longyi\\Pay\\Afterpay\\Providers\\AfterpayServiceProvider',
-    47 => 'Longyi\\Pay\\Klarna\\Providers\\KlarnaServiceProvider',
-    48 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
-    49 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
-    50 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
-    51 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
-    52 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
-    53 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
-    54 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
-    55 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
-    56 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
-    57 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
-    58 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
-    59 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
-    60 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
-    61 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
-    62 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
-    63 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
-    64 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
-    65 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
-    66 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
-    67 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
-    68 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
-    69 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
-    70 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
-    71 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
-    72 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
-    73 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
-    74 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
-    75 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
-    76 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
-    77 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
-    78 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
-    79 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
-    80 => 'Webkul\\User\\Providers\\UserServiceProvider',
-    81 => 'Webkul\\BagistoApi\\Providers\\BagistoApiServiceProvider',
+    42 => 'Longyi\\ShippingInsurance\\Providers\\ShippingInsuranceServiceProvider',
+    43 => 'Longyi\\Pay\\AwxAfterpay\\Providers\\AwxAfterpayServiceProvider',
+    44 => 'Longyi\\Pay\\AwxKlarna\\Providers\\AwxKlarnaServiceProvider',
+    45 => 'Longyi\\Pay\\Applepay\\Providers\\ApplepayServiceProvider',
+    46 => 'Longyi\\Pay\\Airwallex\\Providers\\AirwallexServiceProvider',
+    47 => 'Longyi\\Pay\\Afterpay\\Providers\\AfterpayServiceProvider',
+    48 => 'Longyi\\Pay\\Klarna\\Providers\\KlarnaServiceProvider',
+    49 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
+    50 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
+    51 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
+    52 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
+    53 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
+    54 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
+    55 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
+    56 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
+    57 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
+    58 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
+    59 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
+    60 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
+    61 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
+    62 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
+    63 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
+    64 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
+    65 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
+    66 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
+    67 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
+    68 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
+    69 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
+    70 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
+    71 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
+    72 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
+    73 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
+    74 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
+    75 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
+    76 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
+    77 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
+    78 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
+    79 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
+    80 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
+    81 => 'Webkul\\User\\Providers\\UserServiceProvider',
+    82 => 'Webkul\\BagistoApi\\Providers\\BagistoApiServiceProvider',
   ),
   'deferred' => 
   array (

+ 1 - 0
bootstrap/providers.php

@@ -17,6 +17,7 @@ return [
     Longyi\RewardPoints\Providers\RewardPointsServiceProvider::class,
     Longyi\Member\Providers\MemberServiceProvider::class,
     Longyi\Gift\Providers\GiftServiceProvider::class,
+    Longyi\ShippingInsurance\Providers\ShippingInsuranceServiceProvider::class,
     Longyi\Pay\AwxAfterpay\Providers\AwxAfterpayServiceProvider::class,
     Longyi\Pay\AwxKlarna\Providers\AwxKlarnaServiceProvider::class,
     Longyi\Pay\Applepay\Providers\ApplepayServiceProvider::class,

+ 1 - 0
composer.json

@@ -84,6 +84,7 @@
             "Longyi\\RewardPoints\\": "packages/Longyi/RewardPoints/src/",
             "Longyi\\Member\\": "packages/Longyi/Member/src/",
             "Longyi\\Gift\\": "packages/Longyi/Gift/src/",
+            "Longyi\\ShippingInsurance\\": "packages/Longyi/ShippingInsurance/src/",
             "Longyi\\Pay\\Airwallex\\": "packages/Longyi/Pay/Airwallex/src/",
             "Longyi\\Pay\\Applepay\\": "packages/Longyi/Pay/Applepay/src/",
             "Longyi\\Pay\\AwxKlarna\\": "packages/Longyi/Pay/AwxKlarna/src/",

+ 37 - 0
packages/Longyi/Gift/src/Database/Migrations/2026_08_11_000003_add_invoiced_fields_to_orders_table.php

@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->decimal('giftcard_invoiced', 10, 2)->default(0)->nullable()->after('base_giftcard_amount');
+            $table->decimal('base_giftcard_invoiced', 10, 2)->default(0)->nullable()->after('giftcard_invoiced');
+            $table->decimal('vip_plus_invoiced', 10, 2)->default(0)->nullable()->after('base_giftcard_invoiced');
+            $table->decimal('base_vip_plus_invoiced', 10, 2)->default(0)->nullable()->after('vip_plus_invoiced');
+            $table->decimal('vip_discount_invoiced', 10, 2)->default(0)->nullable()->after('base_vip_plus_invoiced');
+            $table->decimal('base_vip_discount_invoiced', 10, 2)->default(0)->nullable()->after('vip_discount_invoiced');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->dropColumn([
+                'giftcard_invoiced', 'base_giftcard_invoiced',
+                'vip_plus_invoiced', 'base_vip_plus_invoiced',
+                'vip_discount_invoiced', 'base_vip_discount_invoiced',
+            ]);
+        });
+    }
+};

+ 29 - 0
packages/Longyi/Gift/src/Listeners/ApplepayBreakdownHandler.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Longyi\Gift\Listeners;
+
+use Longyi\Pay\Applepay\Events\CollectingAmountBreakdown;
+
+class ApplepayBreakdownHandler
+{
+    /**
+     * Map gift card amount into the Apple Pay breakdown.
+     *
+     * @param  \Longyi\Pay\Applepay\Events\CollectingAmountBreakdown  $event
+     * @return void
+     */
+    public function collect(CollectingAmountBreakdown $event): void
+    {
+        $order = $event->order;
+
+        // 礼品卡金额 -> discount(正数递减总额)
+        if (! empty($order->giftcard_amount)) {
+            $event->breakdown->addDiscount((float) $order->giftcard_amount);
+        }
+
+        // 积分抵扣金额 -> discount(正数递减总额)
+        if (! empty($order->reward_points_amount)) {
+            $event->breakdown->addDiscount((float) $order->reward_points_amount);
+        }
+    }
+}

+ 4 - 0
packages/Longyi/Gift/src/Providers/EventServiceProvider.php

@@ -4,6 +4,7 @@ namespace Longyi\Gift\Providers;
 
 use Illuminate\Support\Facades\Event;
 use Illuminate\Support\ServiceProvider;
+use Longyi\Pay\Applepay\Events\CollectingAmountBreakdown;
 
 class EventServiceProvider extends ServiceProvider
 {
@@ -22,5 +23,8 @@ class EventServiceProvider extends ServiceProvider
         Event::listen('bagisto.shop.customers.account.orders.view.information.discount.after', 'Longyi\Gift\Listeners\OrderViewHandler@renderGiftCardInfo');
         Event::listen('sales.invoice.save.after', 'Longyi\Gift\Listeners\InvoiceHandler@saveAfter');
         Event::listen('sales.order.cancel.after', 'Longyi\Gift\Listeners\OrderCancelHandler@afterCancelOrder');
+
+        // 将礼品卡金额映射进 Apple Pay breakdown
+        Event::listen(CollectingAmountBreakdown::class, 'Longyi\Gift\Listeners\ApplepayBreakdownHandler@collect');
     }
 }

+ 10 - 2
packages/Longyi/Gift/src/Repositories/CustomInvoiceRepository.php

@@ -18,6 +18,8 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
         $invoice->base_giftcard_amount = $invoice->base_giftcard_amount = 0;
         $invoice->vip_plus_amount = $invoice->base_vip_plus_amount = 0;
         $invoice->vip_discount_amount = $invoice->base_vip_discount_amount = 0;
+        $invoice->shipping_insurance_amount = $invoice->base_shipping_insurance_amount = 0;
+        $invoice->reward_points_amount = $invoice->base_reward_points_amount = 0;
 
         foreach ($invoice->items as $item) {
             $invoice->tax_amount += $item->tax_amount;
@@ -51,6 +53,12 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
         $invoice->vip_discount_amount += $invoice->order->vip_discount_amount;
         $invoice->base_vip_discount_amount += $invoice->order->base_vip_discount_amount;
 
+        $invoice->shipping_insurance_amount += $invoice->order->shipping_insurance_amount;
+        $invoice->base_shipping_insurance_amount += $invoice->order->base_shipping_insurance_amount;
+
+        $invoice->reward_points_amount += $invoice->order->reward_points_amount;
+        $invoice->base_reward_points_amount += $invoice->order->base_reward_points_amount;
+
         if ($invoice->order->shipping_tax_amount) {
             $invoice->shipping_tax_amount = $invoice->order->shipping_tax_amount;
 
@@ -85,8 +93,8 @@ class CustomInvoiceRepository extends BaseInvoiceRepository
             }
         }
 
-        $invoice->grand_total = $invoice->sub_total + $invoice->tax_amount + $invoice->shipping_amount - $invoice->discount_amount - $invoice->giftcard_amount + $invoice->vip_plus_amount - $invoice->vip_discount_amount;
-        $invoice->base_grand_total = $invoice->base_sub_total + $invoice->base_tax_amount + $invoice->base_shipping_amount - $invoice->base_discount_amount - $invoice->base_giftcard_amount + $invoice->base_vip_plus_amount - $invoice->base_vip_discount_amount;
+        $invoice->grand_total = $invoice->sub_total + $invoice->tax_amount + $invoice->shipping_amount - $invoice->discount_amount - $invoice->giftcard_amount + $invoice->vip_plus_amount - $invoice->vip_discount_amount + $invoice->shipping_insurance_amount - $invoice->reward_points_amount;
+        $invoice->base_grand_total = $invoice->base_sub_total + $invoice->base_tax_amount + $invoice->base_shipping_amount - $invoice->base_discount_amount - $invoice->base_giftcard_amount + $invoice->base_vip_plus_amount - $invoice->base_vip_discount_amount + $invoice->base_shipping_insurance_amount - $invoice->base_reward_points_amount;
 
         $invoice->save();
         return $invoice;

+ 19 - 11
packages/Longyi/Gift/src/Repositories/CustomOrderRepository.php

@@ -13,9 +13,11 @@ class CustomOrderRepository extends BaseOrderRepository
         $order->shipping_invoiced = $order->base_shipping_invoiced = 0;
         $order->tax_amount_invoiced = $order->base_tax_amount_invoiced = 0;
         $order->discount_invoiced = $order->base_discount_invoiced = 0;
-        $order->giftcard_amount = $order->base_giftcard_amount = 0;
-        $order->vip_plus_amount = $order->base_vip_plus_amount = 0;
-        $order->vip_discount_amount = $order->base_vip_discount_amount = 0;
+        $order->giftcard_invoiced = $order->base_giftcard_invoiced = 0;
+        $order->vip_plus_invoiced = $order->base_vip_plus_invoiced = 0;
+        $order->vip_discount_invoiced = $order->base_vip_discount_invoiced = 0;
+        $order->shipping_insurance_invoiced = $order->base_shipping_insurance_invoiced = 0;
+        $order->reward_points_invoiced = $order->base_reward_points_invoiced = 0;
 
         foreach ($order->invoices as $invoice) {
             $order->sub_total_invoiced += $invoice->sub_total;
@@ -30,18 +32,24 @@ class CustomOrderRepository extends BaseOrderRepository
             $order->discount_invoiced += $invoice->discount_amount;
             $order->base_discount_invoiced += $invoice->base_discount_amount;
 
-            $order->giftcard_amount += $invoice->giftcard_amount;
-            $order->base_giftcard_amount += $invoice->base_giftcard_amount;
+            $order->giftcard_invoiced += $invoice->giftcard_amount;
+            $order->base_giftcard_invoiced += $invoice->base_giftcard_amount;
 
-            $order->vip_plus_amount += $invoice->vip_plus_amount;
-            $order->base_vip_plus_amount += $invoice->base_vip_plus_amount;
+            $order->vip_plus_invoiced += $invoice->vip_plus_amount;
+            $order->base_vip_plus_invoiced += $invoice->base_vip_plus_amount;
 
-            $order->vip_discount_amount += $invoice->vip_discount_amount;
-            $order->base_vip_discount_amount += $invoice->base_vip_discount_amount;
+            $order->vip_discount_invoiced += $invoice->vip_discount_amount;
+            $order->base_vip_discount_invoiced += $invoice->base_vip_discount_amount;
+
+            $order->shipping_insurance_invoiced += $invoice->shipping_insurance_amount;
+            $order->base_shipping_insurance_invoiced += $invoice->base_shipping_insurance_amount;
+
+            $order->reward_points_invoiced += $invoice->reward_points_amount;
+            $order->base_reward_points_invoiced += $invoice->base_reward_points_amount;
         }
 
-        $order->grand_total_invoiced = $order->sub_total_invoiced + $order->shipping_invoiced + $order->tax_amount_invoiced - $order->discount_invoiced - $order->giftcard_amount + $order->vip_plus_amount - $order->vip_discount_amount;
-        $order->base_grand_total_invoiced = $order->base_sub_total_invoiced + $order->base_shipping_invoiced + $order->base_tax_amount_invoiced - $order->base_discount_invoiced - $order->base_giftcard_amount + $order->base_vip_plus_amount - $order->base_vip_discount_amount;
+        $order->grand_total_invoiced = $order->sub_total_invoiced + $order->shipping_invoiced + $order->tax_amount_invoiced - $order->discount_invoiced - $order->giftcard_invoiced + $order->vip_plus_invoiced - $order->vip_discount_invoiced + $order->shipping_insurance_invoiced - $order->reward_points_invoiced;
+        $order->base_grand_total_invoiced = $order->base_sub_total_invoiced + $order->base_shipping_invoiced + $order->base_tax_amount_invoiced - $order->base_discount_invoiced - $order->base_giftcard_invoiced + $order->base_vip_plus_invoiced - $order->base_vip_discount_invoiced + $order->base_shipping_insurance_invoiced - $order->base_reward_points_invoiced;
 
         // order refund total
         $order->sub_total_refunded = $order->base_sub_total_refunded = 0;

+ 12 - 0
packages/Longyi/Gift/src/Resources/views/sales/invoices/view.blade.php

@@ -298,6 +298,12 @@
                             </p>
                         @endif
 
+                        @if ($invoice->base_shipping_insurance_amount > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                @lang('shippinginsurance::app.shop.label')
+                            </p>
+                        @endif
+
                         @if ($order->giftcard_amount > 0)
                             <p class="text-gray-600 dark:text-gray-300 !leading-5 text-sm">
                                 @lang('gift::app.giftcard.giftcard_amount')
@@ -375,6 +381,12 @@
                                 {{ core()->formatBasePrice($invoice->base_discount_amount) }}
                             </p>
                         @endif
+                        <!-- Shipping Insurance Amount -->
+                        @if ($invoice->base_shipping_insurance_amount > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                + {{ core()->formatBasePrice($invoice->base_shipping_insurance_amount) }}
+                            </p>
+                        @endif
                         <!-- Giftcard Amount -->
                         @if ($order->giftcard_amount > 0)
                             <p class="text-gray-600 dark:text-gray-300 !leading-5">

+ 18 - 0
packages/Longyi/Member/src/DataGrids/Member/MemberDataGrid.php

@@ -20,6 +20,7 @@ class MemberDataGrid extends DataGrid
                 'member_log.id',
                 'member_log.customer_id',
                 'member_log.order_id',
+                'member_log.type',
                 'member_log.amount',
                 'customers.email as customer_email',
                 'customers.first_name',
@@ -31,6 +32,7 @@ class MemberDataGrid extends DataGrid
 
         $this->addFilter('id', 'member_log.id');
         $this->addFilter('order_id', 'member_log.order_id');
+        $this->addFilter('type', 'member_log.type');
         $this->addFilter('amount', 'member_log.amount');
         $this->addFilter('expirationdate', 'member_log.expirationdate');
         $this->addFilter('created_at', 'member_log.created_at');
@@ -77,6 +79,22 @@ class MemberDataGrid extends DataGrid
             'filterable' => false,
             'sortable'   => true
         ]);
+        $this->addColumn([
+            'index'      => 'type',
+            'label'      => '类型',
+            'type'       => 'integer',
+            'filterable' => true,
+            'sortable'   => true,
+            'closure'    => function ($row) {
+                $typeMap = [
+                    1 => '下单购买',
+                    2 => '退款',
+                    3 => '取消订单',
+                ];
+
+                return $typeMap[$row->type] ?? '未知';
+            },
+        ]);
         $this->addColumn([
             'index'      => 'amount',
             'label'      => 'vip amount',

+ 28 - 0
packages/Longyi/Member/src/Database/Migrations/2026_08_10_000000_add_previous_expire_date_to_member_log_table.php

@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('member_log', function (Blueprint $table) {
+            $table->dateTime('previous_expire_date')->nullable()->after('expirationdate')->comment('购买前的VIP过期时间');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('member_log', function (Blueprint $table) {
+            $table->dropColumn('previous_expire_date');
+        });
+    }
+};

+ 29 - 0
packages/Longyi/Member/src/Listeners/ApplepayBreakdownHandler.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Longyi\Pay\Applepay\Events\CollectingAmountBreakdown;
+
+class ApplepayBreakdownHandler
+{
+    /**
+     * Map member plus / vip discount amounts into the Apple Pay breakdown.
+     *
+     * @param  \Longyi\Pay\Applepay\Events\CollectingAmountBreakdown  $event
+     * @return void
+     */
+    public function collect(CollectingAmountBreakdown $event): void
+    {
+        $order = $event->order;
+
+        // 会员 plus 金额 -> handling(附加费)
+        if (! empty($order->vip_plus_amount)) {
+            $event->breakdown->addFee((float) $order->vip_plus_amount);
+        }
+
+        // VIP 折扣 -> discount(正数递减总额)
+        if (! empty($order->vip_discount_amount)) {
+            $event->breakdown->addDiscount((float) $order->vip_discount_amount);
+        }
+    }
+}

+ 41 - 26
packages/Longyi/Member/src/Listeners/OrderPlacedHandler.php

@@ -3,8 +3,6 @@
 namespace Longyi\Member\Listeners;
 
 use Longyi\Member\Models\MemberLog;
-use Webkul\Paypal\Payment\SmartButton;
-use Webkul\Sales\Repositories\OrderTransactionRepository;
 use Webkul\Customer\Models\Customer;
 use Carbon\Carbon;
 
@@ -12,52 +10,69 @@ class OrderPlacedHandler
 {
     protected const MEMBER_DISCOUNT_AMOUNT = 19.9;
 
-
     protected const VIP_EXTENSION_DAYS = 180;
-    /**
-     * Create a new listener instance.
-     *
-     * @return void
-     */
-    public function __construct(
-        protected SmartButton $smartButton,
-        protected OrderTransactionRepository $orderTransactionRepository
-    ) {}
 
     /**
-     * Apply member discount to cart.
+     * 订单支付完成后激活 VIP 会员
+     * 监听 sales.invoice.save.after 事件
      *
-     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @param  \Webkul\Sales\Models\Invoice  $invoice
      * @return void
      */
-    public function afterPlaceOrder($order)
+    public function afterInvoicePaid($invoice)
     {
-        if (!$order || !$order->customer_id) {
+        // 只处理已支付的发票
+        if ($invoice->state !== 'paid') {
             return;
         }
-        if (!$order->vip_plus_amount || $order->vip_plus_amount <= 0) {
+
+        $order = $invoice->order;
+
+        if (! $order || ! $order->customer_id) {
             return;
         }
+
+        if (! $order->vip_plus_amount || $order->vip_plus_amount <= 0) {
+            return;
+        }
+
+        // 防止重复激活:检查是否已为该订单激活过 VIP
+        $existingLog = MemberLog::where('order_id', $order->increment_id)
+            ->where('type', 1)
+            ->first();
+
+        if ($existingLog) {
+            return;
+        }
+
         $customer = Customer::find($order->customer_id);
-        if (!$customer) {
+
+        if (! $customer) {
             return;
         }
-        $currentExpireDate = $customer->vip_expire_date ? Carbon::parse($customer->vip_expire_date) : now();
+
+        $previousExpireDate = $customer->vip_expire_date;
+        $currentExpireDate = $customer->vip_expire_date
+            ? Carbon::parse($customer->vip_expire_date)
+            : now();
+
         if ($currentExpireDate->isPast()) {
             $newExpireDate = now()->addDays(self::VIP_EXTENSION_DAYS);
         } else {
             $newExpireDate = $currentExpireDate->addDays(self::VIP_EXTENSION_DAYS);
         }
+
         $customer->vip_expire_date = $newExpireDate;
         $customer->save();
-        // 记录
+
+        // 记录购买日志(含购买前的过期时间,用于退款时回退)
         MemberLog::create([
-            'customer_id' => $order->customer_id,
-            'order_id' => $order->increment_id,
-            'amount' => self::MEMBER_DISCOUNT_AMOUNT,
-            'type' => 1,
-            'expirationdate' => $newExpireDate
+            'customer_id'          => $order->customer_id,
+            'order_id'             => $order->increment_id,
+            'amount'               => self::MEMBER_DISCOUNT_AMOUNT,
+            'type'                 => 1,
+            'previous_expire_date' => $previousExpireDate,
+            'expirationdate'       => $newExpireDate,
         ]);
     }
-
 }

+ 107 - 0
packages/Longyi/Member/src/Listeners/VipRevokeHandler.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace Longyi\Member\Listeners;
+
+use Longyi\Member\Models\MemberLog;
+use Webkul\Customer\Models\Customer;
+use Carbon\Carbon;
+
+class VipRevokeHandler
+{
+    /**
+     * 订单取消时撤销 VIP 会员
+     * 监听 sales.order.cancel.after 事件
+     *
+     * @param  \Webkul\Sales\Models\Order  $order
+     * @return void
+     */
+    public function onOrderCancel($order)
+    {
+        $this->revokeVip($order, 2);
+    }
+
+    /**
+     * 订单退款时撤销 VIP 会员
+     * 监听 sales.refund.save.after 事件
+     *
+     * @param  \Webkul\Sales\Models\Refund  $refund
+     * @return void
+     */
+    public function onRefund($refund)
+    {
+        $order = $refund->order;
+
+        if (! $order || ! $order->customer_id) {
+            return;
+        }
+
+        // 只有包含 VIP Plus 费用的订单才处理
+        if (! $order->vip_plus_amount || $order->vip_plus_amount <= 0) {
+            return;
+        }
+
+        // 检查是否为全额退款(退款金额 >= 订单总额),避免部分退款误撤销
+        if (bccomp((string) $refund->base_grand_total, (string) $order->base_grand_total, 2) < 0) {
+            return;
+        }
+
+        $this->revokeVip($order, 3);
+    }
+
+    /**
+     * 执行 VIP 撤销逻辑
+     *
+     * @param  \Webkul\Sales\Models\Order  $order
+     * @param  int  $revokeType  2=取消撤销, 3=退款撤销
+     * @return void
+     */
+    protected function revokeVip($order, int $revokeType)
+    {
+        if (! $order || ! $order->customer_id) {
+            return;
+        }
+
+        if (! $order->vip_plus_amount || $order->vip_plus_amount <= 0) {
+            return;
+        }
+
+        // 防止重复撤销
+        $existingRevokeLog = MemberLog::where('order_id', $order->increment_id)
+            ->whereIn('type', [2, 3])
+            ->first();
+
+        if ($existingRevokeLog) {
+            return;
+        }
+
+        // 查找该订单的激活记录
+        $activationLog = MemberLog::where('order_id', $order->increment_id)
+            ->where('type', 1)
+            ->first();
+
+        if (! $activationLog) {
+            // 没有激活记录,说明可能是在支付之前就取消了,无需处理
+            return;
+        }
+
+        $customer = Customer::find($order->customer_id);
+
+        if (! $customer) {
+            return;
+        }
+
+        // 恢复到购买前的 VIP 过期时间
+        $customer->vip_expire_date = $activationLog->previous_expire_date;
+        $customer->save();
+
+        // 记录撤销日志
+        MemberLog::create([
+            'customer_id'          => $order->customer_id,
+            'order_id'             => $order->increment_id,
+            'amount'               => $activationLog->amount,
+            'type'                 => $revokeType,
+            'previous_expire_date' => $activationLog->expirationdate,
+            'expirationdate'       => $activationLog->previous_expire_date,
+        ]);
+    }
+}

+ 2 - 0
packages/Longyi/Member/src/Models/MemberLog.php

@@ -21,6 +21,7 @@ class MemberLog extends Model implements MemberLogContract
         'order_id',
         'type',
         'amount',
+        'previous_expire_date',
         'expirationdate'
     ];
 
@@ -34,6 +35,7 @@ class MemberLog extends Model implements MemberLogContract
         'order_id' => 'integer',
         'type' => 'integer',
         'amount' => 'decimal:2',
+        'previous_expire_date' => 'date',
         'expirationdate' => 'date',
     ];
 }

+ 8 - 1
packages/Longyi/Member/src/Providers/EventServiceProvider.php

@@ -4,8 +4,10 @@ namespace Longyi\Member\Providers;
 
 use Illuminate\Support\Facades\Event;
 use Illuminate\Support\ServiceProvider;
+use Longyi\Member\Listeners\ApplepayBreakdownHandler;
 use Longyi\Member\Listeners\MemberHandler;
 use Longyi\Member\Listeners\VipDiscountHandler;
+use Longyi\Pay\Applepay\Events\CollectingAmountBreakdown;
 
 class EventServiceProvider extends ServiceProvider
 {
@@ -27,7 +29,12 @@ class EventServiceProvider extends ServiceProvider
         Event::listen('bagisto.admin.sales.order.view.giftcard.before', function($viewRenderEventManager) {
             $viewRenderEventManager->addTemplate('member::sales.orders.view');
         });
-        Event::listen('checkout.order.save.after', 'Longyi\Member\Listeners\OrderPlacedHandler@afterPlaceOrder');
+        Event::listen('sales.invoice.save.after', 'Longyi\Member\Listeners\OrderPlacedHandler@afterInvoicePaid');
+        Event::listen('sales.order.cancel.after', 'Longyi\Member\Listeners\VipRevokeHandler@onOrderCancel');
+        Event::listen('sales.refund.save.after', 'Longyi\Member\Listeners\VipRevokeHandler@onRefund');
         Event::listen('bagisto.shop.customers.account.orders.view.information.discount.after', 'Longyi\Member\Listeners\OrderViewHandler@renderMemberInfo');
+
+        // 将会员 plus / VIP 折扣金额映射进 Apple Pay breakdown
+        Event::listen(CollectingAmountBreakdown::class, ApplepayBreakdownHandler::class . '@collect');
     }
 }

+ 3 - 0
packages/Longyi/Pay/Afterpay/src/Payment/Afterpay.php

@@ -393,6 +393,9 @@ class Afterpay extends Payment
     {
         $description = $this->getConfigData('description');
         $info = $this->getOrder() ?: Cart::getCart();
+        if (! $info) {
+            return $description;
+        }
         $amount = round($info->grand_total / 4, 2);
         $symbol = core()->getCurrentCurrency()->symbol;
         return sprintf($description, $symbol.$amount);

+ 24 - 0
packages/Longyi/Pay/Applepay/src/Events/CollectingAmountBreakdown.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace Longyi\Pay\Applepay\Events;
+
+use Longyi\Pay\Applepay\Support\PayPalBreakdown;
+use Webkul\Sales\Models\Order;
+
+/**
+ * Dispatched while building the PayPal / Apple Pay order amount breakdown.
+ *
+ * Any feature that adds an extra checkout line item can listen to this event
+ * and map its order amount into the shared breakdown instance.
+ */
+class CollectingAmountBreakdown
+{
+    /**
+     * @param  Order  $order
+     * @param  PayPalBreakdown  $breakdown
+     */
+    public function __construct(
+        public Order $order,
+        public PayPalBreakdown $breakdown,
+    ) {}
+}

+ 23 - 22
packages/Longyi/Pay/Applepay/src/Payment/Applepay.php

@@ -4,6 +4,8 @@ namespace Longyi\Pay\Applepay\Payment;
 
 use GuzzleHttp\Client;
 use Illuminate\Support\Facades\Log;
+use Longyi\Pay\Applepay\Events\CollectingAmountBreakdown;
+use Longyi\Pay\Applepay\Support\PayPalBreakdown;
 use Webkul\Payment\Payment\Payment;
 use Webkul\Sales\Models\Order;
 use Webkul\Sales\Repositories\OrderRepository;
@@ -72,6 +74,25 @@ class Applepay extends Payment
     public function createOrder($order, $override)
     {
         $billingAddressLines = $this->getAddressLines($order->billing_address->address);
+
+        $breakdown = (new PayPalBreakdown())
+            ->setCurrencyCode($order->order_currency_code)
+            ->setItemTotal((float) $order->sub_total)
+            ->setShipping((float) ($order->selected_shipping_rate ? $order->selected_shipping_rate->price : 0))
+            ->setTaxTotal((float) $order->tax_total)
+            ->addDiscount((float) $order->discount_amount);
+
+        event(new CollectingAmountBreakdown($order, $breakdown));
+
+        if (abs($breakdown->total() - (float) $order->grand_total) > 0.011) {
+            Log::channel('payment')->warning('applepay breakdown mismatch with grand_total', [
+                'order_increment_id' => $order->increment_id ?? null,
+                'breakdown'          => $breakdown->toArray(),
+                'breakdown_total'    => $breakdown->total(),
+                'grand_total'        => (float) $order->grand_total,
+            ]);
+        }
+
         $data = [
             'intent' => 'CAPTURE',
             'purchase_units' => [
@@ -80,28 +101,8 @@ class Applepay extends Payment
                     'invoice_id' => $override['merchantOrderId'],
                     'amount' => [
                         'currency_code' => $order->order_currency_code,
-                        'value' => $this->formatCurrencyValue((float) $order->sub_total + $order->tax_total + ($order->selected_shipping_rate ? $order->selected_shipping_rate->price : 0) - $order->discount_amount),
-                        'breakdown' => [
-                            'item_total' => [
-                                'currency_code' => $order->order_currency_code,
-                                'value'         => $this->formatCurrencyValue((float) $order->sub_total),
-                            ],
-
-                            'shipping'   => [
-                                'currency_code' => $order->order_currency_code,
-                                'value'         => $this->formatCurrencyValue((float) ($order->selected_shipping_rate ? $order->selected_shipping_rate->price : 0)),
-                            ],
-
-                            'tax_total'  => [
-                                'currency_code' => $order->order_currency_code,
-                                'value'         => $this->formatCurrencyValue((float) $order->tax_total),
-                            ],
-
-                            'discount'   => [
-                                'currency_code' => $order->order_currency_code,
-                                'value'         => $this->formatCurrencyValue((float) $order->discount_amount),
-                            ],
-                        ],
+                        'value'         => $breakdown->value(),
+                        'breakdown'     => $breakdown->toArray(),
                     ],
                     'items' => $this->getLineItems($order),
                     'shipping' => [

+ 167 - 0
packages/Longyi/Pay/Applepay/src/Support/PayPalBreakdown.php

@@ -0,0 +1,167 @@
+<?php
+
+namespace Longyi\Pay\Applepay\Support;
+
+/**
+ * PayPal / Apple Pay order amount breakdown value object.
+ *
+ * Collects amount breakdown fields in a centralized, event-driven way so that
+ * any later-added checkout line item (gift card, member plus, shipping
+ * insurance, etc.) can register its own listener and map its amount into the
+ * breakdown without touching the payment core.
+ *
+ * Invariant:
+ *   value = item_total + shipping + tax_total + handling + insurance - discount
+ */
+class PayPalBreakdown
+{
+    /**
+     * Currency code for all amounts.
+     */
+    protected string $currencyCode;
+
+    /**
+     * Sum of items unit_amount * quantity.
+     */
+    protected float $itemTotal = 0.0;
+
+    /**
+     * Shipping cost.
+     */
+    protected float $shipping = 0.0;
+
+    /**
+     * Tax total.
+     */
+    protected float $taxTotal = 0.0;
+
+    /**
+     * Handling / additional fees (e.g. member plus).
+     */
+    protected float $handling = 0.0;
+
+    /**
+     * Insurance (e.g. shipping insurance).
+     */
+    protected float $insurance = 0.0;
+
+    /**
+     * Total discount (coupon, gift card, vip discount, etc.).
+     */
+    protected float $discount = 0.0;
+
+    public function setCurrencyCode(string $currencyCode): self
+    {
+        $this->currencyCode = $currencyCode;
+
+        return $this;
+    }
+
+    public function setItemTotal(float $amount): self
+    {
+        $this->itemTotal = $amount;
+
+        return $this;
+    }
+
+    public function setShipping(float $amount): self
+    {
+        $this->shipping = $amount;
+
+        return $this;
+    }
+
+    public function setTaxTotal(float $amount): self
+    {
+        $this->taxTotal = $amount;
+
+        return $this;
+    }
+
+    /**
+     * Add a fee (handling).
+     */
+    public function addFee(float $amount): self
+    {
+        $this->handling += $amount;
+
+        return $this;
+    }
+
+    /**
+     * Add insurance amount.
+     */
+    public function addInsurance(float $amount): self
+    {
+        $this->insurance += $amount;
+
+        return $this;
+    }
+
+    /**
+     * Add to the total discount (positive amount decreases the total).
+     */
+    public function addDiscount(float $amount): self
+    {
+        $this->discount += $amount;
+
+        return $this;
+    }
+
+    /**
+     * The computed total value.
+     *
+     * item_total + shipping + tax_total + handling + insurance - discount
+     */
+    public function total(): float
+    {
+        $value = $this->itemTotal
+            + $this->shipping
+            + $this->taxTotal
+            + $this->handling
+            + $this->insurance
+            - $this->discount;
+
+        return round($value, 2);
+    }
+
+    /**
+     * Alias for total() — used as the PayPal amount.value.
+     */
+    public function value(): float
+    {
+        return $this->total();
+    }
+
+    /**
+     * Convert to the PayPal "breakdown" array, omitting zero-value fields.
+     */
+    public function toArray(): array
+    {
+        $fields = [
+            'item_total' => $this->itemTotal,
+            'shipping'   => $this->shipping,
+            'tax_total'  => $this->taxTotal,
+            'handling'   => $this->handling,
+            'insurance'  => $this->insurance,
+            'discount'   => $this->discount,
+        ];
+
+        $breakdown = [];
+
+        foreach ($fields as $key => $amount) {
+            $rounded = round((float) $amount, 2);
+
+            if ($rounded == 0.0) {
+                continue;
+            }
+
+            $breakdown[$key] = [
+                'currency_code' => $this->currencyCode,
+                'value'         => $rounded,
+            ];
+        }
+
+        return $breakdown;
+    }
+}

+ 3 - 0
packages/Longyi/Pay/AwxAfterpay/src/Payment/AwxAfterpay.php

@@ -326,6 +326,9 @@ class AwxAfterpay extends Payment
     {
         $description = $this->getConfigData('description');
         $info = $this->getOrder() ?: Cart::getCart();
+        if (! $info) {
+            return $description;
+        }
         $amount = round($info->grand_total / 4, 2);
         $symbol = core()->getCurrentCurrency()->symbol;
         return sprintf($description, $symbol.$amount);

+ 3 - 0
packages/Longyi/Pay/AwxKlarna/src/Payment/AwxKlarna.php

@@ -336,6 +336,9 @@ class AwxKlarna extends Payment
     {
         $description = $this->getConfigData('description');
         $info = $this->getOrder() ?: Cart::getCart();
+        if (! $info) {
+            return $description;
+        }
         $amount = round($info->grand_total / 4, 2);
         $symbol = core()->getCurrentCurrency()->symbol;
         return sprintf($description, $symbol.$amount);

+ 3 - 0
packages/Longyi/Pay/Klarna/src/Payment/Klarna.php

@@ -347,6 +347,9 @@ class Klarna extends Payment
     {
         $description = $this->getConfigData('description');
         $info = $this->getOrder() ?: Cart::getCart();
+        if (! $info) {
+            return $description;
+        }
         $amount = round($info->grand_total / 4, 2);
         $symbol = core()->getCurrentCurrency()->symbol;
         return sprintf($description, $symbol.$amount);

+ 14 - 1
packages/Longyi/RewardPoints/src/Config/TransactionType.php

@@ -85,6 +85,11 @@ class TransactionType
      */
     const FOLLOW = 14;
 
+    /**
+     * 积分抵扣(结账时使用积分抵扣支付金额)
+     */
+    const POINTS_REDEEM = 15;
+
     /**
      * 获取所有类型配置
      *
@@ -198,6 +203,13 @@ class TransactionType
                 'icon' => 'icon-heart',
                 'color' => 'red',
             ],
+            self::POINTS_REDEEM => [
+                'code' => 'points_redeem',
+                'name' => '积分抵扣',
+                'description' => '结账时使用积分抵扣支付金额',
+                'icon' => 'icon-shopping-cart',
+                'color' => 'orange',
+            ],
         ];
     }
 
@@ -210,12 +222,13 @@ class TransactionType
     {
         $all = self::all();
 
-        // 排除特殊类型:管理员操作(99)、过期(10)、兑换礼品卡(11)、取消订单退回(12)
+        // 排除特殊类型:管理员操作(99)、过期(10)、兑换礼品卡(11)、取消订单退回(12)、积分抵扣(15)
         $excludedTypes = [
             self::ADMIN_ACTION,
             self::EXPIRED,
             self::GIFT_CARD_REDEEM,
             self::ORDER_CANCEL_REFUND,
+            self::POINTS_REDEEM,
         ];
 
         foreach ($excludedTypes as $typeId) {

+ 38 - 0
packages/Longyi/RewardPoints/src/Database/Migrations/2026_08_18_000001_add_reward_points_fields_to_cart_table.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        if (! Schema::hasColumn('cart', 'reward_points_used')) {
+            Schema::table('cart', function (Blueprint $table) {
+                $table->integer('reward_points_used')->default(0)->after('base_vip_discount_amount')->comment('结账使用的积分数');
+                $table->decimal('reward_points_amount', 12, 4)->default(0)->after('reward_points_used')->comment('积分抵扣金额');
+                $table->decimal('base_reward_points_amount', 12, 4)->default(0)->after('reward_points_amount')->comment('基础币种积分抵扣金额');
+            });
+        }
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        if (Schema::hasColumn('cart', 'reward_points_used')) {
+            Schema::table('cart', function (Blueprint $table) {
+                $table->dropColumn([
+                    'reward_points_used',
+                    'reward_points_amount',
+                    'base_reward_points_amount',
+                ]);
+            });
+        }
+    }
+};

+ 40 - 0
packages/Longyi/RewardPoints/src/Database/Migrations/2026_08_21_000001_add_reward_points_fields_to_orders_table.php

@@ -0,0 +1,40 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        if (! Schema::hasColumn('orders', 'reward_points_used')) {
+            Schema::table('orders', function (Blueprint $table) {
+                $table->integer('reward_points_used')->default(0)->after('base_vip_discount_amount')->comment('下单使用的积分数');
+                $table->decimal('reward_points_amount', 12, 4)->default(0)->after('reward_points_used')->comment('积分抵扣金额');
+                $table->decimal('base_reward_points_amount', 12, 4)->default(0)->after('reward_points_amount')->comment('基础币种积分抵扣金额');
+                $table->integer('reward_points_earned')->default(0)->after('base_reward_points_amount')->comment('下单赚取的积分数');
+            });
+        }
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        if (Schema::hasColumn('orders', 'reward_points_used')) {
+            Schema::table('orders', function (Blueprint $table) {
+                $table->dropColumn([
+                    'reward_points_used',
+                    'reward_points_amount',
+                    'base_reward_points_amount',
+                    'reward_points_earned',
+                ]);
+            });
+        }
+    }
+};

+ 54 - 0
packages/Longyi/RewardPoints/src/Database/Migrations/2026_08_21_000002_add_reward_points_invoiced_fields.php

@@ -0,0 +1,54 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        // 订单表:积分抵扣已开票金额
+        if (! Schema::hasColumn('orders', 'reward_points_invoiced')) {
+            Schema::table('orders', function (Blueprint $table) {
+                $table->decimal('reward_points_invoiced', 12, 4)->default(0)->after('base_reward_points_amount')->comment('积分抵扣已开票金额');
+                $table->decimal('base_reward_points_invoiced', 12, 4)->default(0)->after('reward_points_invoiced')->comment('基础币种积分抵扣已开票金额');
+            });
+        }
+
+        // 发票表:单张发票的积分抵扣金额
+        if (! Schema::hasColumn('invoices', 'reward_points_amount')) {
+            Schema::table('invoices', function (Blueprint $table) {
+                $table->decimal('reward_points_amount', 12, 4)->default(0)->after('base_shipping_insurance_amount')->comment('积分抵扣金额');
+                $table->decimal('base_reward_points_amount', 12, 4)->default(0)->after('reward_points_amount')->comment('基础币种积分抵扣金额');
+            });
+        }
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        if (Schema::hasColumn('orders', 'reward_points_invoiced')) {
+            Schema::table('orders', function (Blueprint $table) {
+                $table->dropColumn([
+                    'reward_points_invoiced',
+                    'base_reward_points_invoiced',
+                ]);
+            });
+        }
+
+        if (Schema::hasColumn('invoices', 'reward_points_amount')) {
+            Schema::table('invoices', function (Blueprint $table) {
+                $table->dropColumn([
+                    'reward_points_amount',
+                    'base_reward_points_amount',
+                ]);
+            });
+        }
+    }
+};

+ 39 - 0
packages/Longyi/RewardPoints/src/Listeners/OrderEvents.php

@@ -5,6 +5,7 @@ namespace Longyi\RewardPoints\Listeners;
 use Longyi\RewardPoints\Repositories\RewardPointRepository;
 use Longyi\RewardPoints\Models\RewardActiveRule;
 use Longyi\RewardPoints\Models\RewardPointHistory;
+use Longyi\RewardPoints\Config\TransactionType;
 use Longyi\RewardPoints\Services\GrowthValueService;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
@@ -32,6 +33,9 @@ class OrderEvents
             return;
         }
 
+        // 1. 处理积分抵扣:如果结账时使用了积分抵扣,下单时真正扣除积分
+        $this->deductRedeemedPoints($order);
+
         $rule = $this->getOrderRule();
         if (!$rule) {
             return;
@@ -68,6 +72,10 @@ class OrderEvents
                 $rule
             );
 
+            // 将赚取的积分写回订单表
+            $order->reward_points_earned = $points;
+            $order->save();
+
             Log::info('Order points added as PENDING', [
                 'order_id' => $order->id,
                 'customer_id' => $order->customer_id,
@@ -78,6 +86,37 @@ class OrderEvents
             ]);
         }
     }
+    /**
+     * Deduct the reward points that were redeemed against this order.
+     *
+     * The cart stores how many points the customer chose to redeem in the
+     * "reward_points_used" column (set by the GraphQL SaveCheckoutCart flow).
+     */
+    protected function deductRedeemedPoints(Order $order): void
+    {
+        $cart = $order->cart;
+
+        if (! $cart || ! ($pointsUsed = (int) ($cart->reward_points_used ?? 0))) {
+            return;
+        }
+
+        $discountAmount = (float) ($cart->base_reward_points_amount ?? 0);
+
+        $this->rewardPointRepository->deductPoints(
+            $order->customer_id,
+            $pointsUsed,
+            TransactionType::POINTS_REDEEM,
+            'Points redeemed for order #' . $order->increment_id . ' (Discount: ' . core()->formatPrice($discountAmount) . ')',
+            $order->id
+        );
+
+        // 清除购物车积分抵扣标记,避免重复扣减
+        $cart->reward_points_used = 0;
+        $cart->reward_points_amount = 0;
+        $cart->base_reward_points_amount = 0;
+        $cart->save();
+    }
+
     public function handleOrderCancellation(Order $order): void
     {
         if (!$order->customer_id) {

+ 49 - 0
packages/Longyi/RewardPoints/src/Listeners/RewardPointsHandler.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Longyi\RewardPoints\Listeners;
+
+use Webkul\Checkout\Models\Cart;
+
+class RewardPointsHandler
+{
+    /**
+     * Apply reward points discount to the cart grand total.
+     *
+     * Runs after "checkout.cart.collect.totals.after" (same pattern as gift
+     * cards). The cart's reward_points_used field is set by the GraphQL
+     * SaveCheckoutCartProcessor; here we compute the discount amount and deduct
+     * it from the grand total so the remaining balance is what the customer
+     * actually pays.
+     */
+    public function applyRewardPoints(Cart $cart): void
+    {
+        $pointsUsed = (int) ($cart->reward_points_used ?? 0);
+
+        if ($pointsUsed <= 0) {
+            return;
+        }
+
+        $pointValue = (float) config('rewardpoints.general.point_value', 0.01);
+        $maxDiscountPercentage = (float) config('rewardpoints.general.max_discount_percentage', 100);
+
+        // Discount amount in the cart's current currency.
+        $discountAmount = round($pointsUsed * $pointValue, 2);
+
+        // Cap the discount at the configured percentage of the grand total.
+        $maxDiscountAmount = round((float) $cart->base_grand_total * $maxDiscountPercentage / 100, 2);
+        $baseDiscountAmount = core()->convertToBasePrice($discountAmount);
+
+        if ($baseDiscountAmount > $maxDiscountAmount) {
+            $baseDiscountAmount = $maxDiscountAmount;
+            $discountAmount = core()->convertPrice($baseDiscountAmount);
+        }
+
+        $cart->reward_points_amount = $discountAmount;
+        $cart->base_reward_points_amount = $baseDiscountAmount;
+
+        $cart->grand_total = max(0, round((float) $cart->grand_total - $discountAmount, 2));
+        $cart->base_grand_total = max(0, round((float) $cart->base_grand_total - $baseDiscountAmount, 2));
+
+        $cart->save();
+    }
+}

+ 7 - 0
packages/Longyi/RewardPoints/src/Providers/RewardPointsServiceProvider.php

@@ -3,6 +3,7 @@
 namespace Longyi\RewardPoints\Providers;
 
 use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Event;
 use Illuminate\Routing\Router;
 use Longyi\RewardPoints\Providers\EventServiceProvider;
 use Longyi\RewardPoints\Services\CartRewardPoints;
@@ -32,6 +33,12 @@ class RewardPointsServiceProvider extends ServiceProvider
         $this->app->booted(function () {
             $this->loadDatabaseSettings();
         });
+
+        // 结账积分抵扣:在 cart totals 计算后扣除积分抵扣金额
+        Event::listen(
+            'checkout.cart.collect.totals.after',
+            \Longyi\RewardPoints\Listeners\RewardPointsHandler::class.'@applyRewardPoints'
+        );
     }
 
     public function register()

+ 2 - 0
packages/Longyi/RewardPoints/src/Resources/lang/en/rewardpoints.php

@@ -1,6 +1,8 @@
 <?php
 
 return [
+    'reward-points-amount' => 'Reward Points Amount',
+
     'customer' => [
         'my-reward-points' => 'My Reward Points',
         'available-points' => 'Available Points',

+ 2 - 0
packages/Longyi/RewardPoints/src/Resources/lang/zh_CN/rewardpoints.php

@@ -1,6 +1,8 @@
 <?php
 
 return [
+    'reward-points-amount' => '积分抵扣金额',
+
     'customer' => [
         'my-reward-points' => '我的积分',
         'available-points' => '可用积分',

+ 10 - 0
packages/Longyi/ShippingInsurance/src/Config/acl.php

@@ -0,0 +1,10 @@
+<?php
+
+return [
+    [
+        'key'   => 'shipping_insurance',
+        'name'  => 'shippinginsurance::app.admin.acl-title',
+        'route' => 'admin.shipping_insurance.index',
+        'sort'  => 102
+    ]
+];

+ 11 - 0
packages/Longyi/ShippingInsurance/src/Config/admin-menu.php

@@ -0,0 +1,11 @@
+<?php
+
+return [
+    [
+        'key'   => 'shipping_insurance',
+        'name'  => 'shippinginsurance::app.admin.menu-title',
+        'route' => 'admin.shipping_insurance.index',
+        'sort'  => 102,
+        'icon'  => 'icon-settings',
+    ]
+];

+ 38 - 0
packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000000_add_shipping_insurance_to_cart_table.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            if (! Schema::hasColumn('cart', 'shipping_insurance_amount')) {
+                $table->decimal('shipping_insurance_amount', 12, 4)->default(0)->after('vip_discount_amount');
+            }
+            if (! Schema::hasColumn('cart', 'base_shipping_insurance_amount')) {
+                $table->decimal('base_shipping_insurance_amount', 12, 4)->default(0)->after('shipping_insurance_amount');
+            }
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            if (Schema::hasColumn('cart', 'shipping_insurance_amount')) {
+                $table->dropColumn('shipping_insurance_amount');
+            }
+            if (Schema::hasColumn('cart', 'base_shipping_insurance_amount')) {
+                $table->dropColumn('base_shipping_insurance_amount');
+            }
+        });
+    }
+};

+ 32 - 0
packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000001_add_shipping_insurance_meta_to_cart_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            if (! Schema::hasColumn('cart', 'shipping_insurance_name')) {
+                $table->string('shipping_insurance_name', 255)->default('')->after('base_shipping_insurance_amount');
+            }
+            if (! Schema::hasColumn('cart', 'shipping_insurance_enabled')) {
+                $table->boolean('shipping_insurance_enabled')->default(false)->after('shipping_insurance_name');
+            }
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            if (Schema::hasColumn('cart', 'shipping_insurance_enabled')) {
+                $table->dropColumn('shipping_insurance_enabled');
+            }
+            if (Schema::hasColumn('cart', 'shipping_insurance_name')) {
+                $table->dropColumn('shipping_insurance_name');
+            }
+        });
+    }
+};

+ 26 - 0
packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000002_add_use_shipping_insurance_to_cart_table.php

@@ -0,0 +1,26 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            if (! Schema::hasColumn('cart', 'use_shipping_insurance')) {
+                $table->boolean('use_shipping_insurance')->default(false)->after('shipping_insurance_enabled');
+            }
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            if (Schema::hasColumn('cart', 'use_shipping_insurance')) {
+                $table->dropColumn('use_shipping_insurance');
+            }
+        });
+    }
+};

+ 32 - 0
packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000003_add_shipping_insurance_type_value_to_cart_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            if (! Schema::hasColumn('cart', 'shipping_insurance_type')) {
+                $table->string('shipping_insurance_type', 20)->default('fixed')->after('shipping_insurance_enabled');
+            }
+            if (! Schema::hasColumn('cart', 'shipping_insurance_value')) {
+                $table->decimal('shipping_insurance_value', 12, 4)->default(0)->after('shipping_insurance_type');
+            }
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('cart', function (Blueprint $table) {
+            if (Schema::hasColumn('cart', 'shipping_insurance_value')) {
+                $table->dropColumn('shipping_insurance_value');
+            }
+            if (Schema::hasColumn('cart', 'shipping_insurance_type')) {
+                $table->dropColumn('shipping_insurance_type');
+            }
+        });
+    }
+};

+ 29 - 0
packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000000_add_shipping_insurance_to_orders_table.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->decimal('shipping_insurance_amount', 12, 4)->default(0)->nullable()->after('base_vip_discount_amount');
+            $table->decimal('base_shipping_insurance_amount', 12, 4)->default(0)->nullable()->after('shipping_insurance_amount');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->dropColumn(['shipping_insurance_amount', 'base_shipping_insurance_amount']);
+        });
+    }
+};

+ 29 - 0
packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000001_add_shipping_insurance_to_invoices_table.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('invoices', function (Blueprint $table) {
+            $table->decimal('shipping_insurance_amount', 12, 4)->default(0)->nullable()->after('base_vip_discount_amount');
+            $table->decimal('base_shipping_insurance_amount', 12, 4)->default(0)->nullable()->after('shipping_insurance_amount');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('invoices', function (Blueprint $table) {
+            $table->dropColumn(['shipping_insurance_amount', 'base_shipping_insurance_amount']);
+        });
+    }
+};

+ 29 - 0
packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000002_add_shipping_insurance_invoiced_to_orders_table.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->decimal('shipping_insurance_invoiced', 12, 4)->default(0)->nullable()->after('base_shipping_insurance_amount');
+            $table->decimal('base_shipping_insurance_invoiced', 12, 4)->default(0)->nullable()->after('shipping_insurance_invoiced');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('orders', function (Blueprint $table) {
+            $table->dropColumn(['shipping_insurance_invoiced', 'base_shipping_insurance_invoiced']);
+        });
+    }
+};

+ 65 - 0
packages/Longyi/ShippingInsurance/src/Http/Controllers/Admin/ShippingInsuranceController.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Longyi\ShippingInsurance\Http\Controllers\Admin;
+
+use Illuminate\Http\RedirectResponse;
+use Illuminate\View\View;
+use Webkul\Admin\Http\Controllers\Controller;
+use Webkul\Core\Repositories\CoreConfigRepository;
+
+class ShippingInsuranceController extends Controller
+{
+    /**
+     * Create a new controller instance.
+     */
+    public function __construct(
+        protected CoreConfigRepository $coreConfigRepository,
+    ) {
+    }
+
+    /**
+     * Display the shipping insurance configuration.
+     */
+    public function index(): View
+    {
+        $config = [
+            'enable' => (bool) core()->getConfigData('sales.checkout.shipping_insurance.enable'),
+            'name'   => core()->getConfigData('sales.checkout.shipping_insurance.name') ?: '运费险',
+            'type'   => core()->getConfigData('sales.checkout.shipping_insurance.type') ?: 'fixed',
+            'value'  => core()->getConfigData('sales.checkout.shipping_insurance.value') ?: 0,
+        ];
+
+        return view('shippinginsurance::admin.index', compact('config'));
+    }
+
+    /**
+     * Store the shipping insurance configuration.
+     */
+    public function store(): RedirectResponse
+    {
+        $validated = request()->validate([
+            'enable' => 'nullable|boolean',
+            'name'   => 'nullable|string|max:50',
+            'type'   => 'required|in:fixed,percentage',
+            'value'  => 'required|numeric|min:0',
+        ]);
+
+        $values = [
+            'sales.checkout.shipping_insurance.enable' => ($validated['enable'] ?? false) ? 1 : 0,
+            'sales.checkout.shipping_insurance.name'   => $validated['name'] ?? '运费险',
+            'sales.checkout.shipping_insurance.type'   => $validated['type'],
+            'sales.checkout.shipping_insurance.value'  => $validated['value'],
+        ];
+
+        foreach ($values as $code => $value) {
+            $this->coreConfigRepository->updateOrCreate(
+                ['code' => $code],
+                ['value' => $value]
+            );
+        }
+
+        session()->flash('success', trans('shippinginsurance::app.admin.save-success'));
+
+        return redirect()->route('admin.shipping_insurance.index');
+    }
+}

+ 42 - 0
packages/Longyi/ShippingInsurance/src/Http/Controllers/Shop/ShippingInsuranceController.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace Longyi\ShippingInsurance\Http\Controllers\Shop;
+
+use Illuminate\Http\JsonResponse;
+use Webkul\Checkout\Facades\Cart;
+use Webkul\Shop\Http\Controllers\Controller;
+use Webkul\Shop\Http\Resources\CartResource;
+
+class ShippingInsuranceController extends Controller
+{
+    /**
+     * Toggle shipping insurance on/off for the current cart.
+     */
+    public function toggle(): JsonResponse
+    {
+        $cart = Cart::getCart();
+
+        if (! $cart) {
+            return response()->json([
+                'message' => '购物车为空',
+            ], 404);
+        }
+
+        $useInsurance = ! $cart->use_shipping_insurance;
+
+        $cart->update([
+            'use_shipping_insurance' => $useInsurance,
+        ]);
+
+        // Recalculate cart totals with/without insurance
+        Cart::collectTotals();
+
+        return response()->json([
+            'data'    => new CartResource(Cart::getCart()),
+            'message' => $useInsurance
+                ? trans('shippinginsurance::app.shop.enabled')
+                : trans('shippinginsurance::app.shop.disabled'),
+            'cartId'  => $cart->id,
+        ]);
+    }
+}

+ 24 - 0
packages/Longyi/ShippingInsurance/src/Listeners/ApplepayBreakdownHandler.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace Longyi\ShippingInsurance\Listeners;
+
+use Longyi\Pay\Applepay\Events\CollectingAmountBreakdown;
+
+class ApplepayBreakdownHandler
+{
+    /**
+     * Map shipping insurance amount into the Apple Pay breakdown.
+     *
+     * @param  \Longyi\Pay\Applepay\Events\CollectingAmountBreakdown  $event
+     * @return void
+     */
+    public function collect(CollectingAmountBreakdown $event): void
+    {
+        $order = $event->order;
+
+        // 运费险金额 -> insurance
+        if (! empty($order->shipping_insurance_amount)) {
+            $event->breakdown->addInsurance((float) $order->shipping_insurance_amount);
+        }
+    }
+}

+ 37 - 0
packages/Longyi/ShippingInsurance/src/Listeners/OrderViewHandler.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Longyi\ShippingInsurance\Listeners;
+
+use Webkul\Sales\Models\Order;
+
+class OrderViewHandler
+{
+    /**
+     * 订单详情:在 discount 之后、grand total 之前显示运费险金额。
+     *
+     * 参考 Longyi\Gift\Listeners\OrderViewHandler 的实现:
+     * 从当前路由参数获取订单 ID 并查询订单,通过 echo 渲染组件,
+     * 不依赖视图事件是否传递 $order 变量。
+     *
+     * @param  mixed  $viewRenderEventManager
+     * @return void
+     */
+    public function renderShippingInsurance($viewRenderEventManager)
+    {
+        // 从当前路由参数中获取订单ID
+        $orderId = request()->route('id');
+
+        if (! $orderId) {
+            return;
+        }
+
+        // 查询订单
+        $order = Order::find($orderId);
+
+        if (! $order || ! $order->shipping_insurance_amount || $order->shipping_insurance_amount <= 0) {
+            return;
+        }
+
+        echo view('shippinginsurance::components.shipping-insurance-ordersummary', compact('order'))->render();
+    }
+}

+ 78 - 0
packages/Longyi/ShippingInsurance/src/Listeners/ShippingInsuranceHandler.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace Longyi\ShippingInsurance\Listeners;
+
+class ShippingInsuranceHandler
+{
+    /**
+     * Apply shipping insurance to the cart.
+     *
+     * IMPORTANT: This handler modifies the $cart object passed by reference
+     * from the 'checkout.cart.collect.totals.after' event. It must operate on
+     * the same object instance (not re-fetch from DB) so that Cart::getCart()
+     * returns the updated values.
+     *
+     * @param  \Webkul\Checkout\Models\Cart  $cart
+     * @return void
+     */
+    public function applyShippingInsurance($cart): void
+    {
+        if (! $cart) {
+            return;
+        }
+
+        // Check if shipping insurance is enabled in admin configuration
+        if (! (bool) core()->getConfigData('sales.checkout.shipping_insurance.enable')) {
+            $cart->shipping_insurance_amount      = 0;
+            $cart->base_shipping_insurance_amount = 0;
+            $cart->shipping_insurance_name        = '';
+            $cart->shipping_insurance_enabled     = false;
+            $cart->shipping_insurance_type        = 'fixed';
+            $cart->shipping_insurance_value       = 0;
+            $cart->use_shipping_insurance         = false;
+            $cart->save();
+
+            return;
+        }
+
+        // Get insurance configuration
+        $name  = core()->getConfigData('sales.checkout.shipping_insurance.name') ?: '运费险';
+        $type  = core()->getConfigData('sales.checkout.shipping_insurance.type') ?: 'fixed';
+        $value = (float) core()->getConfigData('sales.checkout.shipping_insurance.value');
+
+        // Always write config metadata to the cart
+        $cart->shipping_insurance_name    = $name;
+        $cart->shipping_insurance_enabled = true;
+        $cart->shipping_insurance_type    = $type;
+        $cart->shipping_insurance_value   = $value;
+
+        // Only apply insurance if the customer has opted in AND value > 0
+        if (! $cart->use_shipping_insurance || $value <= 0) {
+            $cart->shipping_insurance_amount      = 0;
+            $cart->base_shipping_insurance_amount = 0;
+            $cart->save();
+
+            return;
+        }
+
+        // Calculate insurance premium
+        if ($type === 'percentage') {
+            $insuranceAmount = round($cart->sub_total * ($value / 100), 2);
+        } else {
+            // Fixed amount is defined in USD (base currency), convert to current currency
+            $insuranceAmount = round((float) core()->convertPrice($value), 2);
+        }
+
+        // Convert to base currency for base fields
+        $baseInsuranceAmount = core()->convertToBasePrice($insuranceAmount);
+
+        $cart->shipping_insurance_amount      = $insuranceAmount;
+        $cart->base_shipping_insurance_amount = $baseInsuranceAmount;
+
+        // Add insurance to grand total (respect currency distinction)
+        $cart->grand_total      = round($cart->grand_total + $insuranceAmount, 2);
+        $cart->base_grand_total = round($cart->base_grand_total + $baseInsuranceAmount, 2);
+
+        $cart->save();
+    }
+}

+ 39 - 0
packages/Longyi/ShippingInsurance/src/Providers/EventServiceProvider.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Longyi\ShippingInsurance\Providers;
+
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\ServiceProvider;
+use Longyi\Pay\Applepay\Events\CollectingAmountBreakdown;
+
+class EventServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap services.
+     */
+    public function boot(): void
+    {
+        // 运费险计算:监听购物车总计收集事件
+        Event::listen('checkout.cart.collect.totals.after', 'Longyi\ShippingInsurance\Listeners\ShippingInsuranceHandler@applyShippingInsurance');
+
+        // 购物车页面:在 tax 之后、grand total 之前显示运费险
+        Event::listen('bagisto.shop.checkout.cart.summary.tax.after', function ($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('shippinginsurance::components.shipping-insurance-cartsummary');
+        });
+
+        // 结账页面:在 tax 之后、grand total 之前显示运费险
+        Event::listen('bagisto.shop.checkout.onepage.summary.tax.after', function ($viewRenderEventManager) {
+            $viewRenderEventManager->addTemplate('shippinginsurance::components.shipping-insurance-cartsummary');
+        });
+
+        // 后台订单详情:在 discount 之后、grand total 之前显示运费险金额
+        // 参考礼品卡的 OrderViewHandler 方式:从路由获取订单后 echo 渲染组件
+        Event::listen('bagisto.admin.sales.order.view.discount.after', 'Longyi\ShippingInsurance\Listeners\OrderViewHandler@renderShippingInsurance');
+
+        // 前台会员中心订单详情:在 discount 之后、grand total 之前显示运费险金额
+        Event::listen('bagisto.shop.customers.account.orders.view.information.discount.after', 'Longyi\ShippingInsurance\Listeners\OrderViewHandler@renderShippingInsurance');
+
+        // 将运费险金额映射进 Apple Pay breakdown
+        Event::listen(CollectingAmountBreakdown::class, 'Longyi\ShippingInsurance\Listeners\ApplepayBreakdownHandler@collect');
+    }
+}

+ 49 - 0
packages/Longyi/ShippingInsurance/src/Providers/ShippingInsuranceServiceProvider.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Longyi\ShippingInsurance\Providers;
+
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\ServiceProvider;
+
+class ShippingInsuranceServiceProvider extends ServiceProvider
+{
+    /**
+     * Register services.
+     */
+    public function register(): void
+    {
+        $this->registerConfig();
+    }
+
+    /**
+     * Bootstrap services.
+     */
+    public function boot(): void
+    {
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/shop-routes.php');
+
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'shippinginsurance');
+
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'shippinginsurance');
+
+        $this->app->register(EventServiceProvider::class);
+    }
+
+    /**
+     * Register package config.
+     */
+    protected function registerConfig(): void
+    {
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/admin-menu.php', 'menu.admin'
+        );
+
+        $this->mergeConfigFrom(
+            dirname(__DIR__) . '/Config/acl.php', 'acl'
+        );
+    }
+}

+ 27 - 0
packages/Longyi/ShippingInsurance/src/Resources/lang/en/app.php

@@ -0,0 +1,27 @@
+<?php
+
+return [
+    'shop' => [
+        'label'         => 'Shipping Insurance',
+        'description'   => 'Protect your shipment with insurance coverage',
+        'opt-in'        => 'Add Insurance',
+        'opt-out'       => 'Remove Insurance',
+        'enabled'       => 'Shipping insurance added',
+        'disabled'      => 'Shipping insurance removed',
+    ],
+    'admin' => [
+        'title'        => 'Shipping Insurance',
+        'menu-title'   => 'Shipping Insurance',
+        'acl-title'    => 'Shipping Insurance',
+        'enable'       => 'Enable Shipping Insurance',
+        'enable-info'  => 'When enabled, the shipping insurance item will be displayed on the cart page',
+        'name'         => 'Insurance Name',
+        'type'         => 'Charge Type',
+        'fixed'        => 'Fixed Amount',
+        'percentage'   => 'Percentage',
+        'value'        => 'Amount / Rate',
+        'value-info'   => 'For fixed amount, enter the value (e.g. 5). For percentage, enter the rate (e.g. 1 for 1%)',
+        'save'         => 'Save Configuration',
+        'save-success' => 'Shipping insurance configuration saved successfully!',
+    ],
+];

+ 27 - 0
packages/Longyi/ShippingInsurance/src/Resources/lang/zh_CN/app.php

@@ -0,0 +1,27 @@
+<?php
+
+return [
+    'shop' => [
+        'label'         => '运费险',
+        'description'   => '为您提供运费险保障,退货时可获得赔付',
+        'opt-in'        => '投保运费险',
+        'opt-out'       => '取消投保',
+        'enabled'       => '已投保运费险',
+        'disabled'      => '已取消运费险',
+    ],
+    'admin' => [
+        'title'        => '运费险配置',
+        'menu-title'   => '运费险',
+        'acl-title'    => '运费险',
+        'enable'       => '启用运费险',
+        'enable-info'  => '开启后购物车页面将展示运费险项',
+        'name'         => '运费险名称',
+        'type'         => '计费类型',
+        'fixed'        => '固定金额',
+        'percentage'   => '百分比',
+        'value'        => '金额 / 比例',
+        'value-info'   => '固定金额模式请输入金额(如 5),百分比模式请输入比例(如 1 表示 1%)',
+        'save'         => '保存配置',
+        'save-success' => '运费险配置保存成功!',
+    ],
+];

Разлика између датотеке није приказан због своје велике величине
+ 102 - 0
packages/Longyi/ShippingInsurance/src/Resources/views/admin/index.blade.php


+ 89 - 0
packages/Longyi/ShippingInsurance/src/Resources/views/components/shipping-insurance-cartsummary.blade.php

@@ -0,0 +1,89 @@
+{!! view_render_event('bagisto.shop.checkout.cart.summary.shipping_insurance.before') !!}
+
+<!-- Shipping Insurance Component -->
+<v-shipping-insurance :cart="cart"></v-shipping-insurance>
+
+@push('scripts')
+    <script type="text/x-template" id="v-shipping-insurance-template">
+        <div>
+            <!-- Insurance option row - always shown if enabled -->
+            <div
+                class="flex justify-between items-center border-b border-dashed border-gray-300 pb-[15px]"
+                v-if="cart.shipping_insurance && cart.shipping_insurance.enabled"
+            >
+                <label class="flex items-center gap-2.5 cursor-pointer">
+                    <input
+                        type="checkbox"
+                        :checked="cart.shipping_insurance.use_insurance"
+                        @change="toggle"
+                        class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
+                    >
+                    <span class="text-base max-sm:text-xs text-[#6E6E6E]">
+                        @{{ label }}
+                    </span>
+                </label>
+
+                <p
+                    class="text-base max-sm:text-xs font-medium"
+                    v-if="cart.shipping_insurance.use_insurance && cart.shipping_insurance.amount > 0"
+                >
+                    +@{{ cart.shipping_insurance.formatted_amount }}
+                </p>
+            </div>
+        </div>
+    </script>
+
+    <script type="module">
+        app.component('v-shipping-insurance', {
+            template: '#v-shipping-insurance-template',
+
+            props: ['cart'],
+
+            computed: {
+                label() {
+                    const insurance = this.cart.shipping_insurance;
+                    const name = insurance.name || '{{ __('shippinginsurance::app.shop.label') }}';
+                    const type = insurance.type || 'fixed';
+                    const value = parseFloat(insurance.value || 0);
+
+                    if (type === 'percentage' && value > 0) {
+                        return `${name} ( ${value}% )`;
+                    }
+
+                    if (type === 'fixed' && value > 0) {
+                        return `${name} ( ${insurance.formatted_value || insurance.formatted_amount} )`;
+                    }
+
+                    return name;
+                }
+            },
+
+            methods: {
+                toggle() {
+                    this.$axios.post('{{ route('shop.api.checkout.cart.shipping_insurance.toggle') }}')
+                        .then((response) => {
+                            // Refresh cart data via parent's getCart method
+                            // Works for both cart page (shop.api.checkout.cart.index) 
+                            // and checkout page (shop.checkout.onepage.summary)
+                            if (typeof this.$parent.getCart === 'function') {
+                                this.$parent.getCart();
+                            }
+
+                            this.$emitter.emit('add-flash', {
+                                type: 'success',
+                                message: response.data.message,
+                            });
+                        })
+                        .catch((error) => {
+                            this.$emitter.emit('add-flash', {
+                                type: 'error',
+                                message: error.response?.data?.message || 'Error',
+                            });
+                        });
+                },
+            },
+        });
+    </script>
+@endpush
+
+{!! view_render_event('bagisto.shop.checkout.cart.summary.shipping_insurance.after') !!}

+ 16 - 0
packages/Longyi/ShippingInsurance/src/Resources/views/components/shipping-insurance-ordersummary.blade.php

@@ -0,0 +1,16 @@
+{!! view_render_event('bagisto.sales.order.view.shipping_insurance.before') !!}
+
+<!-- Shipping Insurance Amount -->
+@if (isset($order) && $order->shipping_insurance_amount && $order->shipping_insurance_amount > 0)
+    <div class="flex w-full justify-between gap-x-5">
+        <p class="!leading-5 text-gray-600 dark:text-gray-300">
+            @lang('shippinginsurance::app.shop.label')
+        </p>
+
+        <p class="!leading-5 text-gray-600 dark:text-gray-300">
+            + {{ core()->formatPrice($order->shipping_insurance_amount, $order->order_currency_code) }}
+        </p>
+    </div>
+@endif
+
+{!! view_render_event('bagisto.sales.order.view.shipping_insurance.after') !!}

+ 11 - 0
packages/Longyi/ShippingInsurance/src/Routes/admin-routes.php

@@ -0,0 +1,11 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\ShippingInsurance\Http\Controllers\Admin\ShippingInsuranceController;
+
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin/shipping-insurance'], function () {
+    Route::controller(ShippingInsuranceController::class)->group(function () {
+        Route::get('', 'index')->name('admin.shipping_insurance.index');
+        Route::post('', 'store')->name('admin.shipping_insurance.store');
+    });
+});

+ 9 - 0
packages/Longyi/ShippingInsurance/src/Routes/shop-routes.php

@@ -0,0 +1,9 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\ShippingInsurance\Http\Controllers\Shop\ShippingInsuranceController;
+
+Route::group(['prefix' => 'api', 'middleware' => ['api','web']], function () {
+    Route::post('cart/shipping-insurance/toggle', [ShippingInsuranceController::class, 'toggle'])
+        ->name('shop.api.checkout.cart.shipping_insurance.toggle');
+});

+ 44 - 0
packages/Longyi/ShippingInsurance/src/Services/ShippingInsuranceService.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Longyi\ShippingInsurance\Services;
+
+use Webkul\Checkout\Facades\Cart;
+
+class ShippingInsuranceService
+{
+    /**
+     * 启用运费险
+     */
+    public function activate(): void
+    {
+        $cart = Cart::getCart();
+
+        if (! $cart) {
+            throw new \Exception('Cart not found.');
+        }
+
+        $cart->update([
+            'use_shipping_insurance' => true,
+        ]);
+
+        Cart::collectTotals();
+    }
+
+    /**
+     * 移除运费险
+     */
+    public function remove(): void
+    {
+        $cart = Cart::getCart();
+
+        if (! $cart) {
+            throw new \Exception('Cart not found.');
+        }
+
+        $cart->update([
+            'use_shipping_insurance' => false,
+        ]);
+
+        Cart::collectTotals();
+    }
+}

+ 48 - 0
packages/Webkul/Admin/src/Resources/views/sales/refunds/create.blade.php

@@ -309,6 +309,30 @@
                                         @lang('admin::app.sales.refunds.create.tax-amount')
                                     </p>
 
+                                    <template v-if="totals && totals.shipping_insurance && totals.shipping_insurance.price > 0">
+                                        <p class="text-gray-600 dark:text-gray-300">
+                                            @lang('shippinginsurance::app.shop.label')
+                                        </p>
+                                    </template>
+
+                                    <template v-if="totals && totals.giftcard && totals.giftcard.price > 0">
+                                        <p class="text-gray-600 dark:text-gray-300">
+                                            @lang('gift::app.giftcard.giftcard_amount')
+                                        </p>
+                                    </template>
+
+                                    <template v-if="totals && totals.vip_plus && totals.vip_plus.price > 0">
+                                        <p class="text-gray-600 dark:text-gray-300">
+                                            @lang('member::app.member.discount')
+                                        </p>
+                                    </template>
+
+                                    <template v-if="totals && totals.vip_discount && totals.vip_discount.price > 0">
+                                        <p class="text-gray-600 dark:text-gray-300">
+                                            @lang('member::app.member.vip_discount')
+                                        </p>
+                                    </template>
+
                                     <p class="font-semibold text-gray-600 dark:text-gray-300">
                                         @lang('admin::app.sales.refunds.create.grand-total')
                                     </p>
@@ -327,6 +351,30 @@
                                         @{{ totals.tax.formatted_price }}
                                     </p>
 
+                                    <template v-if="totals && totals.shipping_insurance && totals.shipping_insurance.price > 0">
+                                        <p class="text-gray-600 dark:text-gray-300">
+                                            + @{{ totals.shipping_insurance.formatted_price }}
+                                        </p>
+                                    </template>
+
+                                    <template v-if="totals && totals.giftcard && totals.giftcard.price > 0">
+                                        <p class="text-gray-600 dark:text-gray-300">
+                                            - @{{ totals.giftcard.formatted_price }}
+                                        </p>
+                                    </template>
+
+                                    <template v-if="totals && totals.vip_plus && totals.vip_plus.price > 0">
+                                        <p class="text-gray-600 dark:text-gray-300">
+                                            + @{{ totals.vip_plus.formatted_price }}
+                                        </p>
+                                    </template>
+
+                                    <template v-if="totals && totals.vip_discount && totals.vip_discount.price > 0">
+                                        <p class="text-gray-600 dark:text-gray-300">
+                                            - @{{ totals.vip_discount.formatted_price }}
+                                        </p>
+                                    </template>
+
                                     <p class="text-gray-600 dark:text-gray-300">
                                         @{{ totals.grand_total.formatted_price }}
                                     </p>

+ 64 - 1
packages/Webkul/Admin/src/Resources/views/sales/refunds/view.blade.php

@@ -1,4 +1,19 @@
-@php $order = $refund->order; @endphp
+@php
+$order = $refund->order;
+
+$subtotalRatio = $order->base_sub_total > 0
+    ? $refund->base_sub_total / $order->base_sub_total
+    : 0;
+
+$shippingRatio = $order->base_shipping_amount > 0
+    ? $refund->base_shipping_amount / $order->base_shipping_amount
+    : 0;
+
+$baseShippingInsurance = round($order->base_shipping_insurance_amount * $shippingRatio, 2);
+$baseGiftcard = round($order->base_giftcard_amount * $subtotalRatio, 2);
+$baseVipPlus = round($order->base_vip_plus_amount * $subtotalRatio, 2);
+$baseVipDiscount = round($order->base_vip_discount_amount * $subtotalRatio, 2);
+@endphp
 
 <x-admin::layouts>
     <!-- Page Title -->
@@ -212,6 +227,30 @@
                             </p>
                         @endif
 
+                        @if ($baseShippingInsurance > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                @lang('shippinginsurance::app.shop.label')
+                            </p>
+                        @endif
+
+                        @if ($baseGiftcard > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                @lang('gift::app.giftcard.giftcard_amount')
+                            </p>
+                        @endif
+
+                        @if ($baseVipPlus > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                @lang('member::app.member.discount')
+                            </p>
+                        @endif
+
+                        @if ($baseVipDiscount > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                @lang('member::app.member.vip_discount')
+                            </p>
+                        @endif
+
                         <p class="!leading-5 text-gray-600 dark:text-gray-300">
                             @lang('admin::app.sales.refunds.view.adjustment-refund')
                         </p>
@@ -280,6 +319,30 @@
                             </p>
                         @endif
 
+                        @if ($baseShippingInsurance > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                + {{ core()->formatBasePrice($baseShippingInsurance) }}
+                            </p>
+                        @endif
+
+                        @if ($baseGiftcard > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                - {{ core()->formatBasePrice($baseGiftcard) }}
+                            </p>
+                        @endif
+
+                        @if ($baseVipPlus > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                + {{ core()->formatBasePrice($baseVipPlus) }}
+                            </p>
+                        @endif
+
+                        @if ($baseVipDiscount > 0)
+                            <p class="!leading-5 text-gray-600 dark:text-gray-300">
+                                - {{ core()->formatBasePrice($baseVipDiscount) }}
+                            </p>
+                        @endif
+
                         <!-- Base Adjustment Refund -->
                         <p class="!leading-5 text-gray-600 dark:text-gray-300">
                             {{ core()->formatBasePrice($refund->base_adjustment_refund) }}

+ 26 - 0
packages/Webkul/BagistoApi/src/Database/Migrations/2026_08_15_000000_add_like_count_to_product_reviews_table.php

@@ -0,0 +1,26 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('product_reviews', function (Blueprint $table) {
+            if (! Schema::hasColumn('product_reviews', 'like_count')) {
+                $table->unsignedInteger('like_count')->default(0)->after('rating');
+            }
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('product_reviews', function (Blueprint $table) {
+            if (Schema::hasColumn('product_reviews', 'like_count')) {
+                $table->dropColumn('like_count');
+            }
+        });
+    }
+};

+ 105 - 0
packages/Webkul/BagistoApi/src/Dto/CartData.php

@@ -150,6 +150,38 @@ class CartData
     #[ApiProperty(description: 'VIP Plus expiration date')]
     public ?string $vipExpireDate = null;
 
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Whether shipping insurance is used')]
+    public ?bool $useShippingInsurance = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Whether shipping insurance is enabled in admin')]
+    public ?bool $shippingInsuranceEnabled = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Shipping insurance name')]
+    public ?string $shippingInsuranceName = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Shipping insurance type (fixed|percentage)')]
+    public ?string $shippingInsuranceType = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Shipping insurance value (percentage or fixed amount)')]
+    public ?float $shippingInsuranceValue = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Shipping insurance amount')]
+    public ?float $shippingInsuranceAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Base shipping insurance amount')]
+    public ?float $baseShippingInsuranceAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted shipping insurance amount')]
+    public ?string $formattedShippingInsuranceAmount = null;
+
     #[Groups(['query', 'mutation'])]
     public ?bool $success = null;
 
@@ -171,6 +203,30 @@ class CartData
     #[ApiProperty(description: 'Total quantity of all items')]
     public ?int $itemsQty = null;
 
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: "Customer's current reward points balance")]
+    public int $rewardPoints = 0;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Reward points that can be earned from the current cart')]
+    public int $earnableRewardPoints = 0;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Reward points redeemed against the current cart')]
+    public int $rewardPointsUsed = 0;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Reward points discount amount')]
+    public ?float $rewardPointsAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Base reward points discount amount')]
+    public ?float $baseRewardPointsAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted reward points discount amount')]
+    public ?string $formattedRewardPointsAmount = null;
+
     #[Groups(['query', 'mutation'])]
     #[ApiProperty(description: 'Subtotal including tax')]
     public ?float $subTotalInclTax = null;
@@ -348,6 +404,16 @@ class CartData
             }
         }
 
+        // Shipping insurance
+        $data->useShippingInsurance = (bool) ($cart->use_shipping_insurance ?? false);
+        $data->shippingInsuranceEnabled = (bool) ($cart->shipping_insurance_enabled ?? false);
+        $data->shippingInsuranceName = $cart->shipping_insurance_name ?? null;
+        $data->shippingInsuranceType = $cart->shipping_insurance_type ?? null;
+        $data->shippingInsuranceValue = (float) ($cart->shipping_insurance_value ?? 0);
+        $data->shippingInsuranceAmount = (float) ($cart->shipping_insurance_amount ?? 0);
+        $data->baseShippingInsuranceAmount = (float) ($cart->base_shipping_insurance_amount ?? 0);
+        $data->formattedShippingInsuranceAmount = core()->currency($cart->base_shipping_insurance_amount ?? 0);
+
         if ($cart->billing_address) {
             $data->billingAddress = [
                 'id'        => $cart->billing_address->id,
@@ -402,6 +468,45 @@ class CartData
             $data->selectedShippingRateTitle = null;
         }
 
+        // 积分信息:用户当前积分 + 当前购物车可获得积分
+        $data->rewardPoints = 0;
+        $data->earnableRewardPoints = 0;
+
+        // 积分抵扣信息
+        $data->rewardPointsUsed = (int) ($cart->reward_points_used ?? 0);
+        $data->rewardPointsAmount = (float) core()->convertPrice($cart->base_reward_points_amount ?? 0);
+        $data->baseRewardPointsAmount = (float) ($cart->base_reward_points_amount ?? 0);
+        $data->formattedRewardPointsAmount = core()->currency($cart->base_reward_points_amount ?? 0);
+
+        if ($cart->customer_id) {
+            try {
+                $rewardPointRepository = app(\Longyi\RewardPoints\Repositories\RewardPointRepository::class);
+
+                // 用户当前积分
+                $data->rewardPoints = (int) $rewardPointRepository->getCustomerPoints($cart->customer_id);
+
+                // 当前购物车可获得积分(复用订单积分计算逻辑)
+                $rule = \Longyi\RewardPoints\Models\RewardActiveRule::active()
+                    ->ofType(\Longyi\RewardPoints\Models\RewardActiveRule::TYPE_ORDER)
+                    ->first();
+
+                if ($rule) {
+                    $customer = \Webkul\Customer\Models\Customer::find($cart->customer_id);
+
+                    if ($customer && $rule->isApplicableToCustomer($customer)) {
+                        $pointsPerCurrency = $rule->getPointsForCustomer($customer);
+
+                        if ($pointsPerCurrency > 0) {
+                            $data->earnableRewardPoints = (int) floor(($cart->base_grand_total ?? 0) * $pointsPerCurrency);
+                        }
+                    }
+                }
+            } catch (\Throwable $e) {
+                // 积分计算失败不阻塞购物车接口
+                report($e);
+            }
+        }
+
         return $data;
     }
 

+ 12 - 0
packages/Webkul/BagistoApi/src/Dto/CustomerOrder/CustomerOrderDetailDto.php

@@ -148,6 +148,18 @@ class CustomerOrderDetailDto
 
     public ?string $applied_cart_rule_ids = null;
 
+    public ?int $reward_points_used = null;
+
+    public ?float $reward_points_amount = null;
+
+    public ?float $base_reward_points_amount = null;
+
+    public ?int $reward_points_earned = null;
+
+    public ?float $reward_points_invoiced = null;
+
+    public ?float $base_reward_points_invoiced = null;
+
     public ?string $created_at = null;
 
     public ?string $updated_at = null;

+ 8 - 0
packages/Webkul/BagistoApi/src/Dto/CustomerOrder/CustomerOrderListDto.php

@@ -66,6 +66,14 @@ class CustomerOrderListDto
 
     public ?string $order_currency_code = null;
 
+    public ?int $reward_points_used = null;
+
+    public ?float $reward_points_amount = null;
+
+    public ?float $base_reward_points_amount = null;
+
+    public ?int $reward_points_earned = null;
+
     public ?string $created_at = null;
 
     public ?string $updated_at = null;

+ 0 - 0
packages/Webkul/BagistoApi/src/Dto/ProductDetail/ProductAttributeDto.php


+ 15 - 0
packages/Webkul/BagistoApi/src/Dto/ProductDetail/ProductDetailDto.php

@@ -25,6 +25,21 @@ class ProductDetailDto
 
     public ?string $short_description = null;
 
+    /**
+     * Video attribute value (text).
+     */
+    public ?string $video = null;
+
+    /**
+     * Alone attribute value (boolean).
+     */
+    public ?bool $alone = null;
+
+    /**
+     * Customer Features HTML (textarea attribute with code "feature").
+     */
+    public ?string $feature = null;
+
     public ?float $price = null;
 
     public ?float $special_price = null;

+ 11 - 0
packages/Webkul/BagistoApi/src/Dto/SaveCheckoutCartInput.php

@@ -40,4 +40,15 @@ class SaveCheckoutCartInput
     #[ApiProperty(description: 'Member discount toggle. Pass "-1" to leave unchanged, "1" to apply, empty string to remove.')]
     #[SerializedName('memberDiscount')]
     public ?string $memberDiscount = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping insurance toggle. Pass "-1" to leave unchanged, "1" to apply, empty string to remove.')]
+    #[SerializedName('useShippingInsurance')]
+    public ?string $useShippingInsurance = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Reward points to redeem against the order total. Pass "-1" to leave unchanged, "0" to remove.')]
+    #[SerializedName('rewardPoints')]
+    public ?int $rewardPoints = null;
+
 }

+ 16 - 0
packages/Webkul/BagistoApi/src/Models/AttributeOption.php

@@ -44,6 +44,22 @@ use Illuminate\Database\Eloquent\Model as EloquentModel;
 )]
 class AttributeOption extends \Webkul\Attribute\Models\AttributeOption
 {
+    /**
+     * Number of visible products associated with this option within the
+     * current category context. Populated by FilterableAttributesProvider.
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public int $product_count = 0;
+
+    /**
+     * Laravel accessor for the product_count virtual attribute. The accessor
+     * naming convention makes API Platform discover it as a virtual attribute,
+     * while the public property above lets PropertyInfo infer the int type.
+     */
+    public function getProductCountAttribute(): int
+    {
+        return (int) ($this->product_count ?? 0);
+    }
 
     #[ApiProperty(identifier: true, writable: false)]
     public function getId(): ?int

+ 6 - 0
packages/Webkul/BagistoApi/src/Models/CustomerOrder.php

@@ -130,6 +130,12 @@ class CustomerOrder extends Model
         'customer_id'                   => 'int',
         'channel_id'                    => 'int',
         'applied_cart_rule_ids'         => 'string',
+        'reward_points_used'            => 'int',
+        'reward_points_amount'          => 'float',
+        'base_reward_points_amount'     => 'float',
+        'reward_points_earned'          => 'int',
+        'reward_points_invoiced'        => 'float',
+        'base_reward_points_invoiced'   => 'float',
         'created_at'                    => 'datetime',
         'updated_at'                    => 'datetime',
     ];

+ 66 - 0
packages/Webkul/BagistoApi/src/Models/Product.php

@@ -445,6 +445,9 @@ class Product extends BaseProduct
         'color'                => ['id' => 23],
         'size'                 => ['id' => 24],
         'brand'                => ['id' => 25],
+        'video'                => ['id' => 49],
+        'alone'                => ['id' => 36],
+        'feature'              => ['id' => 37],
     ];
 
     #[ApiProperty(identifier: true, writable: false)]
@@ -503,6 +506,69 @@ class Product extends BaseProduct
         return $this->hasMany(AttributeValue::class, 'product_id');
     }
 
+    /**
+     * Get the "video" attribute value (text).
+     */
+    public function getVideoAttribute(): ?string
+    {
+        $value = $this->getSystemAttributeValue('video');
+
+        return $value === '' ? null : (string) $value;
+    }
+
+    #[ApiProperty(writable: true, readable: true)]
+    public function getVideo(): ?string
+    {
+        return $this->getVideoAttribute();
+    }
+
+    public function setVideo(?string $value): void
+    {
+        $this->setSystemAttributeValue('video', $value);
+    }
+
+    /**
+     * Get the "alone" attribute value (boolean).
+     */
+    public function getAloneAttribute(): ?bool
+    {
+        $value = $this->getSystemAttributeValue('alone');
+
+        return $value === '' || $value === null ? null : (bool) $value;
+    }
+
+    #[ApiProperty(writable: true, readable: true)]
+    public function getAlone(): ?bool
+    {
+        return $this->getAloneAttribute();
+    }
+
+    public function setAlone(?bool $value): void
+    {
+        $this->setSystemAttributeValue('alone', $value);
+    }
+
+    /**
+     * Get the "feature" attribute value (textarea / HTML).
+     */
+    public function getFeatureAttribute(): ?string
+    {
+        $value = $this->getSystemAttributeValue('feature');
+
+        return $value === '' ? null : (string) $value;
+    }
+
+    #[ApiProperty(writable: true, readable: true)]
+    public function getFeature(): ?string
+    {
+        return $this->getFeatureAttribute();
+    }
+
+    public function setFeature(?string $value): void
+    {
+        $this->setSystemAttributeValue('feature', $value);
+    }
+
     /**
      * Get locale context.
      */

+ 70 - 3
packages/Webkul/BagistoApi/src/Models/ProductReview.php

@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\Link;
 use Webkul\BagistoApi\Dto\CreateProductReviewInput;
 use Webkul\BagistoApi\Dto\UpdateProductReviewInput;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\ProductReviewLikeProcessor;
 use Webkul\BagistoApi\State\ProductReviewProcessor;
 use Webkul\BagistoApi\State\ProductReviewProvider;
 use Webkul\BagistoApi\State\ProductReviewUpdateProvider;
@@ -214,6 +215,51 @@ use ApiPlatform\OpenApi\Model\Operation;
                 ],
             ),
         ),
+        new \ApiPlatform\Metadata\Post(
+            uriTemplate: '/reviews/{id}/like',
+            processor: ProductReviewLikeProcessor::class,
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+            ],
+            openapi: new Operation(
+                tags: ['Customer Review'],
+                summary: 'Like a product review',
+                description: 'Adds a like to a review. Uses `client_id` (device identifier) to track whether the current visitor has already liked. If already liked, returns an error. Guests can like; `client_id` is a UUID the client generates and stores locally.',
+                requestBody: new \ApiPlatform\OpenApi\Model\RequestBody(
+                    required: true,
+                    content: new \ArrayObject([
+                        'application/json' => [
+                            'schema' => [
+                                'type'       => 'object',
+                                'required'   => ['client_id'],
+                                'properties' => [
+                                    'client_id' => ['type' => 'string', 'example' => '550e8400-e29b-41d4-a716-446655440000', 'description' => 'Device/visitor identifier (UUID generated by client, stored locally)'],
+                                ],
+                            ],
+                            'example' => [
+                                'client_id' => '550e8400-e29b-41d4-a716-446655440000',
+                            ],
+                        ],
+                    ]),
+                ),
+                responses: [
+                    '200' => new \ApiPlatform\OpenApi\Model\Response(
+                        description: 'Like added.',
+                        content: new \ArrayObject([
+                            'application/json' => [
+                                'example' => [
+                                    'id'        => 2,
+                                    'liked'     => true,
+                                    'likeCount' => 42,
+                                ],
+                            ],
+                        ]),
+                    ),
+                    '400' => new \ApiPlatform\OpenApi\Model\Response(description: 'Already liked this review.'),
+                    '404' => new \ApiPlatform\OpenApi\Model\Response(description: 'Review not found.'),
+                ],
+            ),
+        ),
     ],
     graphQlOperations: [
         new QueryCollection(
@@ -312,6 +358,7 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
         'product_id',
         'customer_id',
         'name',
+        'like_count',
     ];
 
     protected $casts = [
@@ -322,6 +369,7 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
         'comment'     => 'string',
         'name'        => 'string',
         'rating'      => 'int',
+        'like_count'  => 'int',
         'status'      => 'string',
         'created_at'  => 'datetime',
         'updated_at'  => 'datetime',
@@ -346,6 +394,22 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
         return $this->getAttribute('id');
     }
 
+    /**
+     * 点赞总数(Eloquen 访问器,snake_case 输出 like_count)
+     */
+    public function getLikeCountAttribute(): int
+    {
+        return (int) ($this->attributes['like_count'] ?? 0);
+    }
+
+    /**
+     * 当前访客是否点赞(Eloquen 访问器,snake_case 输出 liked)
+     */
+    public function getLikedAttribute(): bool
+    {
+        return (bool) ($this->attributes['liked'] ?? false);
+    }
+
     /**
      * Override __isset to ensure isset() works correctly with __get()
      * This is critical for Symfony PropertyAccessor which checks isset() before reading.
@@ -364,7 +428,7 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
      */
     public function __set($key, $value)
     {
-        if (in_array($key, ['id', 'product_id', 'customer_id', 'title', 'comment', 'rating', 'name', 'email', 'status', 'created_at', 'updated_at'])) {
+        if (in_array($key, ['id', 'product_id', 'customer_id', 'title', 'comment', 'rating', 'name', 'email', 'status', 'like_count', 'liked', 'created_at', 'updated_at'])) {
             $this->setAttribute($key, $value);
         } else {
             parent::__set($key, $value);
@@ -378,11 +442,14 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
 
     public function getAttachmentUrls()
     {
-        if ($this->images->isEmpty()) {
+        // 强制重新查询,避免 save() 后关系集合未刷新导致 attachments 为空
+        $images = $this->images()->get();
+
+        if ($images->isEmpty()) {
             return [];
         }
 
-        return $this->images->map(function ($item) {
+        return $images->map(function ($item) {
             return [
                 'type' => $item->type,
                 'url'  => $item->url(),

+ 6 - 1
packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php

@@ -41,6 +41,8 @@ use Webkul\BagistoApi\State\PaymentInitiateProcessor;
 use Webkul\BagistoApi\State\PaymentReplayProcessor;
 use Webkul\BagistoApi\State\SaveCheckoutCartProcessor;
 use Longyi\Member\Services\MemberDiscountService;
+use Longyi\Gift\Services\GiftCardService;
+use Longyi\ShippingInsurance\Services\ShippingInsuranceService;
 use Webkul\BagistoApi\State\BookingSlotProvider;
 use Webkul\BagistoApi\State\PageProvider;
 use Webkul\BagistoApi\State\AttributeCollectionProvider;
@@ -185,6 +187,7 @@ class BagistoApiServiceProvider extends ServiceProvider
         $this->app->tag(PaymentCallbackProcessor::class, ProcessorInterface::class);
         $this->app->tag(PaymentReplayProcessor::class, ProcessorInterface::class);
         $this->app->tag(ProductReviewProcessor::class, ProcessorInterface::class);
+        $this->app->tag(\Webkul\BagistoApi\State\ProductReviewLikeProcessor::class, ProcessorInterface::class);
         $this->app->tag(CompareItemProcessor::class, ProcessorInterface::class);
         $this->app->tag(DownloadableProductProcessor::class, ProcessorInterface::class);
         $this->app->tag(NewsletterSubscriptionProcessor::class, ProcessorInterface::class);
@@ -258,7 +261,9 @@ class BagistoApiServiceProvider extends ServiceProvider
             return new SaveCheckoutCartProcessor(
                 $app->make('Webkul\CartRule\Repositories\CartRuleCouponRepository'),
                 $app->make(\Longyi\Gift\Services\GiftCardService::class),
-                $app->make(MemberDiscountService::class)
+                $app->make(MemberDiscountService::class),
+                $app->make(ShippingInsuranceService::class),
+                $app->make(\Longyi\RewardPoints\Repositories\RewardPointRepository::class)
             );
         });
 

+ 96 - 2
packages/Webkul/BagistoApi/src/State/FilterableAttributesProvider.php

@@ -68,7 +68,10 @@ class FilterableAttributesProvider implements ProviderInterface
         $customerGroup = core()->getGuestCustomerGroup();
 
         $maxPriceQuery = Product::query()
-            ->leftJoin('product_price_indices', 'products.id', 'product_price_indices.product_id')
+            ->leftJoin('product_price_indices', function ($join) {
+                $join->on('products.id', '=', 'product_price_indices.priceable_id')
+                    ->where('product_price_indices.priceable_type', '=', \Webkul\Product\Models\Product::class);
+            })
             ->leftJoin('product_categories', 'products.id', 'product_categories.product_id')
             ->where('product_price_indices.customer_group_id', $customerGroup->id);
 
@@ -89,10 +92,12 @@ class FilterableAttributesProvider implements ProviderInterface
             ->limit($perPage)
             ->get();
 
-        $items = $items->map(function ($item) use ($maxPrice) {
+        $items = $items->map(function ($item) use ($maxPrice, $categoryId) {
             $item->maxPrice = (float) $maxPrice;
             $item->minPrice = 0.0;
 
+            $this->attachProductCounts($item, $categoryId);
+
             return $item;
         });
 
@@ -108,4 +113,93 @@ class FilterableAttributesProvider implements ProviderInterface
             )
         );
     }
+
+    /**
+     * Attach the number of visible products associated with each option of
+     * the given attribute within the requested category context.
+     *
+     * Visibility mirrors ProductGraphQLProvider: a product is counted only when
+     * it has status=1 (attribute id 8) and visible_individually=1 (attribute id 7).
+     */
+    private function attachProductCounts($item, ?int $categoryId): void
+    {
+        $options = $item->options ?? $item->options()->get();
+
+        if ($options->isEmpty()) {
+            return;
+        }
+
+        $attributeId = $item->id;
+        $column = $item->type === 'select' ? 'integer_value' : 'text_value';
+
+        // Table aliases are prefixed by Laravel (e.g. "pav" becomes "ly_pav"),
+        // so raw SQL fragments must use the prefixed alias explicitly.
+        $prefix = DB::getTablePrefix();
+        $alias = $prefix.'pav';
+
+        $countQuery = DB::table('product_attribute_values as pav')
+            ->join('product_attribute_values as pav_status', function ($join) {
+                $join->on('pav_status.product_id', '=', 'pav.product_id')
+                    ->where('pav_status.attribute_id', 8)
+                    ->where('pav_status.boolean_value', 1);
+            })
+            ->join('product_attribute_values as pav_visible', function ($join) {
+                $join->on('pav_visible.product_id', '=', 'pav.product_id')
+                    ->where('pav_visible.attribute_id', 7)
+                    ->where('pav_visible.boolean_value', 1);
+            })
+            ->where('pav.attribute_id', $attributeId);
+
+        if ($categoryId) {
+            $countQuery->whereIn('pav.product_id', function ($sub) use ($categoryId) {
+                $sub->select('product_id')
+                    ->from('product_categories')
+                    ->where('category_id', $categoryId);
+            });
+        }
+
+        $optionIds = $options->pluck('id')->map(fn ($id) => (string) $id)->all();
+
+        if ($column === 'integer_value') {
+            $counts = (clone $countQuery)
+                ->whereIn('pav.integer_value', $optionIds)
+                ->selectRaw($alias.'.integer_value as option_id, COUNT(DISTINCT '.$alias.'.product_id) as total')
+                ->groupBy('pav.integer_value')
+                ->pluck('total', 'option_id')
+                ->toArray();
+        } else {
+            $counts = $this->countMultiSelectOptions(clone $countQuery, $optionIds, $alias);
+        }
+
+        foreach ($options as $option) {
+            $option->product_count = (int) ($counts[(string) $option->id] ?? 0);
+        }
+    }
+
+    /**
+     * Count products for multiselect/checkbox options whose values are stored
+     * as comma-separated option ids in the text_value column.
+     */
+    private function countMultiSelectOptions($query, array $optionIds, string $alias): array
+    {
+        $rows = $query
+            ->whereNotNull('pav.text_value')
+            ->selectRaw($alias.'.text_value, COUNT(DISTINCT '.$alias.'.product_id) as total')
+            ->groupBy('pav.text_value')
+            ->get();
+
+        $counts = [];
+
+        foreach ($rows as $row) {
+            $values = array_filter(array_map('trim', explode(',', (string) $row->text_value)));
+
+            foreach ($values as $value) {
+                if (in_array($value, $optionIds, true)) {
+                    $counts[$value] = ($counts[$value] ?? 0) + (int) $row->total;
+                }
+            }
+        }
+
+        return $counts;
+    }
 }

+ 4 - 0
packages/Webkul/BagistoApi/src/State/ProductDetailProvider.php

@@ -38,6 +38,7 @@ class ProductDetailProvider implements ProviderInterface
         $product = Product::with([
             'attribute_family',
             'attribute_values',
+            'attribute_values.attribute',
             'categories',
             'channels',
             'images',
@@ -76,6 +77,9 @@ class ProductDetailProvider implements ProviderInterface
         $dto->status = (bool) $product->status;
         $dto->description = $product->description;
         $dto->short_description = $product->short_description;
+        $dto->video = $product->video;
+        $dto->alone = $product->alone;
+        $dto->feature = $product->feature;
         $dto->price = $product->price !== null ? (float) $product->price : null;
         $dto->special_price = $product->special_price !== null ? (float) $product->special_price : null;
         $dto->new = (bool) $product->new;

+ 71 - 0
packages/Webkul/BagistoApi/src/State/ProductReviewLikeProcessor.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace Webkul\BagistoApi\State;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Redis;
+use Webkul\BagistoApi\Exception\InvalidInputException;
+use Webkul\BagistoApi\Exception\ResourceNotFoundException;
+use Webkul\BagistoApi\Models\ProductReview;
+
+/**
+ * 评论点赞处理(幂等)
+ *
+ * 点赞状态存 Redis(不记录用户,游客也可点赞),数量存 product_reviews.like_count。
+ * - /reviews/{id}/like :点赞(已点过则提示重复,不重复计数)
+ * Redis key: review_like:{review_id}:{client_id},有效期 3 天。
+ */
+class ProductReviewLikeProcessor implements ProcessorInterface
+{
+    /** 点赞状态缓存有效期(秒) */
+    private const TTL = 3 * 24 * 3600;
+
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
+    {
+        $reviewId = $uriVariables['id'] ?? $uriVariables['reviewId'] ?? null;
+        if (! $reviewId) {
+            throw new InvalidInputException('Review id is required');
+        }
+
+        $review = ProductReview::find($reviewId);
+        if (! $review) {
+            throw new ResourceNotFoundException('Review not found');
+        }
+
+        $request = $context['request'] ?? request();
+        $clientId = (string) ($request->input('client_id') ?? $request->input('clientId') ?? '');
+
+        if ($clientId === '') {
+            throw new InvalidInputException('client_id is required');
+        }
+
+        $key = $this->key($reviewId, $clientId);
+
+        // 已点过:返回提示,不重复计数
+        if (Redis::exists($key)) {
+            throw new InvalidInputException('You have already liked this review');
+        }
+
+        // 首次点赞
+        Redis::setex($key, self::TTL, 1);
+        DB::table('product_reviews')->where('id', $reviewId)->increment('like_count');
+
+        $review->refresh();
+
+        return (object) [
+            'id'        => (int) $review->id,
+            'liked'     => true,
+            'likeCount' => (int) $review->like_count,
+        ];
+    }
+
+    /**
+     * 生成 Redis key
+     */
+    private function key(int $reviewId, string $clientId): string
+    {
+        return "review_like:{$reviewId}:{$clientId}";
+    }
+}

+ 25 - 15
packages/Webkul/BagistoApi/src/State/ProductReviewProcessor.php

@@ -48,6 +48,9 @@ class ProductReviewProcessor implements ProcessorInterface
             if (method_exists($request, 'all')) {
                 $allData = $request->all();
 
+                // images / attachments 不是主表字段,先剔除,避免 fill 时污染模型属性
+                unset($allData['images'], $allData['attachments'], $allData['Images'], $allData['Attachments']);
+
                 $data->fill($allData);
 
                 // API Platform 使用 SnakeCaseToCamelCaseNameConverter,请求体字段会被
@@ -75,7 +78,25 @@ class ProductReviewProcessor implements ProcessorInterface
         }
         $this->validateReview($data);
 
-        return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
+        $saved = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
+
+        // REST 创建接口:处理图片上传(与 GraphQL handleCreate 保持一致)
+        if ($operation instanceof \ApiPlatform\Metadata\Post && $saved instanceof ProductReview) {
+            $request = $context['request'] ?? request();
+            $images = $request->input('images');
+            $attachmentsInput = $request->input('attachments');
+
+            if (! empty($images) && is_array($images)) {
+                $this->saveImagesToS3($images, $saved->id);
+            } elseif (! empty($attachmentsInput)) {
+                $this->saveAttachments(
+                    is_array($attachmentsInput) ? json_encode($attachmentsInput) : $attachmentsInput,
+                    $saved->id
+                );
+            }
+        }
+
+        return $saved;
     }
 
     /**
@@ -125,10 +146,6 @@ class ProductReviewProcessor implements ProcessorInterface
             $attachments = $this->saveAttachments($data->attachments, $review->id);
         }
 
-        if ($attachments) {
-            $review->setAttribute('attachments', json_encode($attachments));
-        }
-
         return $this->mapToOutput($review);
     }
 
@@ -170,21 +187,14 @@ class ProductReviewProcessor implements ProcessorInterface
         $review->save();
 
         // 处理图片上传(新方式:使用 images 字段)
-        $attachments = [];
-
         if (! empty($data->images) && is_array($data->images)) {
-            $attachments = $this->saveImagesToS3($data->images, $review->id);
+            $this->saveImagesToS3($data->images, $review->id);
         } elseif (! empty($data->attachments)) {
             // 向后兼容:使用旧的 attachments 字段(Base64格式)
-            $attachments = $this->saveAttachments($data->attachments, $review->id);
+            $this->saveAttachments($data->attachments, $review->id);
         }
 
-        if ($attachments) {
-            $review->setAttribute('attachments', json_encode($attachments));
-            $review->save();
-        }
-
-	    return $this->mapToOutput($review);
+        return $this->mapToOutput($review);
     }
 
     /**

+ 10 - 0
packages/Webkul/BagistoApi/src/State/ProductReviewProvider.php

@@ -7,6 +7,7 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
 use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Facades\Redis;
 use Webkul\BagistoApi\Models\ProductReview;
 
 /**
@@ -121,6 +122,15 @@ class ProductReviewProvider implements ProviderInterface
 
         $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
 
+        // 根据 client_id 从 Redis 标记当前访客是否点赞了每条评论
+        $clientId = (string) ($args['client_id'] ?? $args['clientId'] ?? '');
+        if ($clientId !== '') {
+            $items->each(function ($review) use ($clientId) {
+                $key = "review_like:{$review->id}:{$clientId}";
+                $review->setAttribute('liked', (bool) Redis::exists($key));
+            });
+        }
+
         return new Paginator(
             new LengthAwarePaginator(
                 $items,

+ 84 - 0
packages/Webkul/BagistoApi/src/State/SaveCheckoutCartProcessor.php

@@ -18,6 +18,8 @@ use Webkul\Checkout\Facades\Cart;
 use Webkul\Shipping\Facades\Shipping;
 use Longyi\Gift\Services\GiftCardService;
 use Longyi\Member\Services\MemberDiscountService;
+use Longyi\ShippingInsurance\Services\ShippingInsuranceService;
+use Longyi\RewardPoints\Repositories\RewardPointRepository;
 
 /**
  * GraphQL processor for the SaveCheckoutCart mutation.
@@ -37,6 +39,8 @@ class SaveCheckoutCartProcessor implements ProcessorInterface
         protected CartRuleCouponRepository $cartRuleCouponRepository,
         protected GiftCardService $giftCardService,
         protected MemberDiscountService $memberDiscountService,
+        protected ShippingInsuranceService $shippingInsuranceService,
+        protected RewardPointRepository $rewardPointRepository,
     ) {}
 
     /**
@@ -84,6 +88,14 @@ class SaveCheckoutCartProcessor implements ProcessorInterface
                 $this->applyMemberDiscount((string) $data->memberDiscount);
             }
 
+            if ($this->shouldUpdate($data->useShippingInsurance)) {
+                $this->applyShippingInsurance((string) $data->useShippingInsurance);
+            }
+
+            if ($data->rewardPoints !== null && $data->rewardPoints !== -1) {
+                $this->applyRewardPoints((int) $data->rewardPoints);
+            }
+
             Cart::collectTotals();
         } catch (OperationFailedException $e) {
             throw $e;
@@ -225,4 +237,76 @@ class SaveCheckoutCartProcessor implements ProcessorInterface
 
         $this->memberDiscountService->apply();
     }
+
+    /**
+     * Apply or remove shipping insurance.
+     * "1" applies, empty string or "0" removes.
+     */
+    private function applyShippingInsurance(string $value): void
+    {
+        $value = trim($value);
+
+        $cart = Cart::getCart();
+
+        if (! $cart) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.cart.invalid-token'));
+        }
+
+        if ($value === '') {
+            $this->shippingInsuranceService->remove();
+            return;
+        }
+
+        if ($cart->use_shipping_insurance) {
+            return;
+        }
+
+        $this->shippingInsuranceService->activate();
+    }
+
+    /**
+     * Apply or remove reward points redemption. "0" removes the redemption.
+     *
+     * The actual discount amount is computed and deducted from the grand total
+     * by the RewardPoints listener on the "checkout.cart.collect.totals.after"
+     * event (same pattern as gift cards), so here we only validate and persist
+     * how many points the customer wants to redeem.
+     */
+    private function applyRewardPoints(int $points): void
+    {
+        $cart = Cart::getCart();
+
+        if (! $cart) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-cart'));
+        }
+
+        $customerId = $cart->customer_id;
+
+        if (! $customerId) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.customer-not-found'));
+        }
+
+        // Remove redemption.
+        if ($points <= 0) {
+            $cart->reward_points_used = 0;
+            $cart->reward_points_amount = 0;
+            $cart->base_reward_points_amount = 0;
+            $cart->save();
+
+            Cart::collectTotals();
+
+            return;
+        }
+
+        $availablePoints = (int) $this->rewardPointRepository->getCustomerPoints($customerId);
+
+        if ($points > $availablePoints) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.insufficient-reward-points'));
+        }
+
+        $cart->reward_points_used = $points;
+        $cart->save();
+
+        Cart::collectTotals();
+    }
 }

+ 9 - 0
packages/Webkul/BagistoApi/src/Transformers/ExpressOrderResource.php

@@ -69,6 +69,15 @@ class ExpressOrderResource extends JsonResource
             'applied_cart_rule_ids'    => $this->applied_cart_rule_ids,
             'discount_amount'          => $discount,
             'base_discount_amount'     => $baseDiscount,
+            $this->mergeWhen($this->shipping_insurance_amount && $this->shipping_insurance_amount > 0, [
+                'shipping_insurance_amount'      => $this->shipping_insurance_amount,
+                'base_shipping_insurance_amount' => $this->base_shipping_insurance_amount,
+            ]),
+            $this->mergeWhen($this->reward_points_used && $this->reward_points_used > 0, [
+                'reward_points_used'             => $this->reward_points_used,
+                'reward_points_amount'           => $this->reward_points_amount,
+                'base_reward_points_amount'      => $this->base_reward_points_amount,
+            ]),
             'billing_address'          => $billing,
             'shipping_address'         => $shipping,
             'payment'                  => (new OrderPaymentResource($this->payment))->jsonSerialize(),

+ 4 - 1
packages/Webkul/Checkout/src/Models/Cart.php

@@ -38,7 +38,10 @@ class Cart extends Model implements CartContract
      * @var array
      */
     protected $casts = [
-        'additional' => 'json',
+        'additional'                => 'json',
+        'reward_points_used'        => 'integer',
+        'reward_points_amount'      => 'float',
+        'base_reward_points_amount' => 'float',
     ];
 
     /**

+ 92 - 3
packages/Webkul/Sales/src/Repositories/RefundRepository.php

@@ -209,8 +209,54 @@ class RefundRepository extends Repository
         $refund->tax_amount += $refund->shipping_tax_amount;
         $refund->base_tax_amount += $refund->base_shipping_tax_amount;
 
-        $refund->grand_total = $refund->sub_total + $refund->tax_amount + $refund->shipping_amount + $refund->adjustment_refund - $refund->adjustment_fee - $refund->discount_amount;
-        $refund->base_grand_total = $refund->base_sub_total + $refund->base_tax_amount + $refund->base_shipping_amount + $refund->base_adjustment_refund - $refund->base_adjustment_fee - $refund->base_discount_amount;
+        $order = $refund->order;
+
+        $subtotalRatio = $order->base_sub_total > 0
+            ? $refund->base_sub_total / $order->base_sub_total
+            : 0;
+
+        $shippingRatio = $order->base_shipping_amount > 0
+            ? $refund->base_shipping_amount / $order->base_shipping_amount
+            : 0;
+
+        $giftcardAmount = round($order->giftcard_amount * $subtotalRatio, 2);
+        $baseGiftcardAmount = round($order->base_giftcard_amount * $subtotalRatio, 2);
+
+        $shippingInsuranceAmount = round($order->shipping_insurance_amount * $shippingRatio, 2);
+        $baseShippingInsuranceAmount = round($order->base_shipping_insurance_amount * $shippingRatio, 2);
+
+        $vipPlusAmount = round($order->vip_plus_amount * $subtotalRatio, 2);
+        $baseVipPlusAmount = round($order->base_vip_plus_amount * $subtotalRatio, 2);
+
+        $vipDiscountAmount = round($order->vip_discount_amount * $subtotalRatio, 2);
+        $baseVipDiscountAmount = round($order->base_vip_discount_amount * $subtotalRatio, 2);
+
+        $rewardPointsAmount = round($order->reward_points_amount * $subtotalRatio, 2);
+        $baseRewardPointsAmount = round($order->base_reward_points_amount * $subtotalRatio, 2);
+
+        $refund->grand_total = $refund->sub_total
+            + $refund->tax_amount
+            + $refund->shipping_amount
+            + $shippingInsuranceAmount
+            - $giftcardAmount
+            + $vipPlusAmount
+            - $vipDiscountAmount
+            - $rewardPointsAmount
+            + $refund->adjustment_refund
+            - $refund->adjustment_fee
+            - $refund->discount_amount;
+
+        $refund->base_grand_total = $refund->base_sub_total
+            + $refund->base_tax_amount
+            + $refund->base_shipping_amount
+            + $baseShippingInsuranceAmount
+            - $baseGiftcardAmount
+            + $baseVipPlusAmount
+            - $baseVipDiscountAmount
+            - $baseRewardPointsAmount
+            + $refund->base_adjustment_refund
+            - $refund->base_adjustment_fee
+            - $refund->base_discount_amount;
 
         $refund->save();
 
@@ -260,7 +306,50 @@ class RefundRepository extends Repository
 
         $totals['shipping']['price'] += $data['shipping'];
 
-        $totals['grand_total']['price'] += $totals['subtotal']['price'] + $totals['tax']['price'] + $totals['shipping']['price'] + $data['adjustment_refund'] - $data['adjustment_fee'] - $totals['discount']['price'];
+        $subtotalRatio = $order->base_sub_total > 0
+            ? $totals['subtotal']['price'] / $order->base_sub_total
+            : 0;
+
+        $shippingRatio = $order->base_shipping_amount > 0
+            ? $data['shipping'] / $order->base_shipping_amount
+            : 0;
+
+        $totals['giftcard'] = [
+            'price'      => round($order->base_giftcard_amount * $subtotalRatio, 2),
+            'label'      => trans('gift::app.giftcard.giftcard_amount'),
+        ];
+
+        $totals['shipping_insurance'] = [
+            'price'      => round($order->base_shipping_insurance_amount * $shippingRatio, 2),
+            'label'      => trans('shippinginsurance::app.shop.label'),
+        ];
+
+        $totals['vip_plus'] = [
+            'price'      => round($order->base_vip_plus_amount * $subtotalRatio, 2),
+            'label'      => trans('member::app.member.discount'),
+        ];
+
+        $totals['vip_discount'] = [
+            'price'      => round($order->base_vip_discount_amount * $subtotalRatio, 2),
+            'label'      => trans('member::app.member.vip_discount'),
+        ];
+
+        $totals['reward_points'] = [
+            'price'      => round($order->base_reward_points_amount * $subtotalRatio, 2),
+            'label'      => trans('rewardpoints::rewardpoints.reward-points-amount'),
+        ];
+
+        $totals['grand_total']['price'] += $totals['subtotal']['price']
+            + $totals['tax']['price']
+            + $totals['shipping']['price']
+            + $totals['shipping_insurance']['price']
+            - $totals['giftcard']['price']
+            + $totals['vip_plus']['price']
+            - $totals['vip_discount']['price']
+            - $totals['reward_points']['price']
+            + $data['adjustment_refund']
+            - $data['adjustment_fee']
+            - $totals['discount']['price'];
 
         $totals = array_map(function ($item) {
             $item['formatted_price'] = core()->formatBasePrice($item['price']);

+ 9 - 0
packages/Webkul/Sales/src/Transformers/OrderResource.php

@@ -86,6 +86,15 @@ class OrderResource extends JsonResource
                 'base_vip_discount_amount'      => $this->base_vip_discount_amount,
                 'formatted_vip_discount_amount' => core()->formatPrice($this->vip_discount_amount),
             ]),
+            $this->mergeWhen($this->shipping_insurance_amount && $this->shipping_insurance_amount > 0, [
+                'shipping_insurance_amount'      => $this->shipping_insurance_amount,
+                'base_shipping_insurance_amount' => $this->base_shipping_insurance_amount,
+            ]),
+            $this->mergeWhen($this->reward_points_used && $this->reward_points_used > 0, [
+                'reward_points_used'             => $this->reward_points_used,
+                'reward_points_amount'           => $this->reward_points_amount,
+                'base_reward_points_amount'      => $this->base_reward_points_amount,
+            ]),
         ];
     }
 }

+ 15 - 0
packages/Webkul/Shop/src/Http/Resources/CartResource.php

@@ -68,6 +68,21 @@ class CartResource extends JsonResource
                 'base_vip_discount_amount'      => $this->base_vip_discount_amount,
                 'formatted_vip_discount_amount' => core()->formatPrice($this->vip_discount_amount),
             ]),
+            $this->mergeWhen($this->shipping_insurance_amount && $this->shipping_insurance_amount > 0, [
+                'shipping_insurance_amount'           => $this->shipping_insurance_amount,
+                'base_shipping_insurance_amount'      => $this->base_shipping_insurance_amount,
+                'formatted_shipping_insurance_amount' => core()->formatPrice($this->shipping_insurance_amount),
+            ]),
+            'shipping_insurance'                     => [
+                'enabled'           => (bool) ($this->shipping_insurance_enabled ?? false),
+                'name'              => $this->shipping_insurance_name ?: '运费险',
+                'type'              => $this->shipping_insurance_type ?: 'fixed',
+                'value'             => (float) ($this->shipping_insurance_value ?? 0),
+                'formatted_value'   => core()->formatPrice($this->shipping_insurance_value ?? 0),
+                'amount'            => $this->shipping_insurance_amount ?? 0,
+                'formatted_amount'  => core()->formatPrice($this->shipping_insurance_amount ?? 0),
+                'use_insurance'     => (bool) ($this->use_shipping_insurance ?? false),
+            ],
         ];
 
     }

+ 40 - 0
packages/Webkul/Shop/src/Resources/views/customers/account/orders/pdf.blade.php

@@ -623,6 +623,46 @@
                                 <td>{!! core()->formatPrice($invoice->discount_amount, $orderCurrencyCode) !!}</td>
                             </tr>
 
+                            @if ($invoice->shipping_insurance_amount > 0)
+                                <tr>
+                                    <td>@lang('shippinginsurance::app.shop.label')</td>
+                                    <td>-</td>
+                                    <td>+ {!! core()->formatPrice($invoice->shipping_insurance_amount, $orderCurrencyCode) !!}</td>
+                                </tr>
+                            @endif
+
+                            @if ($invoice->giftcard_amount > 0)
+                                <tr>
+                                    <td>@lang('gift::app.giftcard.giftcard_amount')</td>
+                                    <td>-</td>
+                                    <td>- {!! core()->formatPrice($invoice->giftcard_amount, $orderCurrencyCode) !!}</td>
+                                </tr>
+                            @endif
+
+                            @if (! empty($invoice->giftcard_number))
+                                <tr>
+                                    <td>@lang('gift::app.giftcard.giftcard_number')</td>
+                                    <td>-</td>
+                                    <td>{{ $invoice->giftcard_number }}</td>
+                                </tr>
+                            @endif
+
+                            @if ($invoice->vip_plus_amount > 0)
+                                <tr>
+                                    <td>@lang('member::app.member.discount')</td>
+                                    <td>-</td>
+                                    <td>+ {!! core()->formatPrice($invoice->vip_plus_amount, $orderCurrencyCode) !!}</td>
+                                </tr>
+                            @endif
+
+                            @if ($invoice->vip_discount_amount > 0)
+                                <tr>
+                                    <td>@lang('member::app.member.vip_discount')</td>
+                                    <td>-</td>
+                                    <td>- {!! core()->formatPrice($invoice->vip_discount_amount, $orderCurrencyCode) !!}</td>
+                                </tr>
+                            @endif
+
                             <tr>
                                 <td style="border-top: 1px solid #FFFFFF;">
                                     <b>@lang('shop::app.customers.account.orders.invoice-pdf.grand-total')</b>