Browse Source

Merge branch 'dev' into dev-rewardPoints

bianjunhui 1 week ago
parent
commit
4d1346836a
58 changed files with 1872 additions and 144 deletions
  1. 82 78
      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. 24 0
      packages/Longyi/Gift/src/Listeners/ApplepayBreakdownHandler.php
  6. 4 0
      packages/Longyi/Gift/src/Providers/EventServiceProvider.php
  7. 6 2
      packages/Longyi/Gift/src/Repositories/CustomInvoiceRepository.php
  8. 15 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. 10 0
      packages/Longyi/ShippingInsurance/src/Config/acl.php
  25. 11 0
      packages/Longyi/ShippingInsurance/src/Config/admin-menu.php
  26. 38 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000000_add_shipping_insurance_to_cart_table.php
  27. 32 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000001_add_shipping_insurance_meta_to_cart_table.php
  28. 26 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000002_add_use_shipping_insurance_to_cart_table.php
  29. 32 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_08_000003_add_shipping_insurance_type_value_to_cart_table.php
  30. 29 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000000_add_shipping_insurance_to_orders_table.php
  31. 29 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000001_add_shipping_insurance_to_invoices_table.php
  32. 29 0
      packages/Longyi/ShippingInsurance/src/Database/Migrations/2026_08_11_000002_add_shipping_insurance_invoiced_to_orders_table.php
  33. 65 0
      packages/Longyi/ShippingInsurance/src/Http/Controllers/Admin/ShippingInsuranceController.php
  34. 42 0
      packages/Longyi/ShippingInsurance/src/Http/Controllers/Shop/ShippingInsuranceController.php
  35. 24 0
      packages/Longyi/ShippingInsurance/src/Listeners/ApplepayBreakdownHandler.php
  36. 37 0
      packages/Longyi/ShippingInsurance/src/Listeners/OrderViewHandler.php
  37. 78 0
      packages/Longyi/ShippingInsurance/src/Listeners/ShippingInsuranceHandler.php
  38. 39 0
      packages/Longyi/ShippingInsurance/src/Providers/EventServiceProvider.php
  39. 49 0
      packages/Longyi/ShippingInsurance/src/Providers/ShippingInsuranceServiceProvider.php
  40. 27 0
      packages/Longyi/ShippingInsurance/src/Resources/lang/en/app.php
  41. 27 0
      packages/Longyi/ShippingInsurance/src/Resources/lang/zh_CN/app.php
  42. 102 0
      packages/Longyi/ShippingInsurance/src/Resources/views/admin/index.blade.php
  43. 89 0
      packages/Longyi/ShippingInsurance/src/Resources/views/components/shipping-insurance-cartsummary.blade.php
  44. 16 0
      packages/Longyi/ShippingInsurance/src/Resources/views/components/shipping-insurance-ordersummary.blade.php
  45. 11 0
      packages/Longyi/ShippingInsurance/src/Routes/admin-routes.php
  46. 9 0
      packages/Longyi/ShippingInsurance/src/Routes/shop-routes.php
  47. 44 0
      packages/Longyi/ShippingInsurance/src/Services/ShippingInsuranceService.php
  48. 48 0
      packages/Webkul/Admin/src/Resources/views/sales/refunds/create.blade.php
  49. 64 1
      packages/Webkul/Admin/src/Resources/views/sales/refunds/view.blade.php
  50. 42 0
      packages/Webkul/BagistoApi/src/Dto/CartData.php
  51. 4 0
      packages/Webkul/BagistoApi/src/Dto/SaveCheckoutCartInput.php
  52. 5 0
      packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php
  53. 29 0
      packages/Webkul/BagistoApi/src/State/SaveCheckoutCartProcessor.php
  54. 4 0
      packages/Webkul/BagistoApi/src/Transformers/ExpressOrderResource.php
  55. 81 3
      packages/Webkul/Sales/src/Repositories/RefundRepository.php
  56. 4 0
      packages/Webkul/Sales/src/Transformers/OrderResource.php
  57. 15 0
      packages/Webkul/Shop/src/Http/Resources/CartResource.php
  58. 40 0
      packages/Webkul/Shop/src/Resources/views/customers/account/orders/pdf.blade.php

+ 82 - 78
bootstrap/cache/services.php

@@ -60,45 +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 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
-    65 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
-    66 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
-    67 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
-    68 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
-    69 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
-    70 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
-    71 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
-    72 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
-    73 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
-    74 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
-    75 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
-    76 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
-    77 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
-    78 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
-    79 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
-    80 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
-    81 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
-    82 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
-    83 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
-    84 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
-    85 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
-    86 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
-    87 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
-    88 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
-    89 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
-    90 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
-    91 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
-    92 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
-    93 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
-    94 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
-    95 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
-    96 => 'Webkul\\User\\Providers\\UserServiceProvider',
-    97 => '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 (
@@ -144,45 +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 => 'Webkul\\Attribute\\Providers\\AttributeServiceProvider',
-    48 => 'Webkul\\BookingProduct\\Providers\\BookingProductServiceProvider',
-    49 => 'Webkul\\CMS\\Providers\\CMSServiceProvider',
-    50 => 'Webkul\\CartRule\\Providers\\CartRuleServiceProvider',
-    51 => 'Webkul\\CatalogRule\\Providers\\CatalogRuleServiceProvider',
-    52 => 'Webkul\\Category\\Providers\\CategoryServiceProvider',
-    53 => 'Webkul\\Checkout\\Providers\\CheckoutServiceProvider',
-    54 => 'Webkul\\Core\\Providers\\CoreServiceProvider',
-    55 => 'Webkul\\Core\\Providers\\EnvValidatorServiceProvider',
-    56 => 'Webkul\\Customer\\Providers\\CustomerServiceProvider',
-    57 => 'Webkul\\DataGrid\\Providers\\DataGridServiceProvider',
-    58 => 'Webkul\\DataTransfer\\Providers\\DataTransferServiceProvider',
-    59 => 'Webkul\\DebugBar\\Providers\\DebugBarServiceProvider',
-    60 => 'Webkul\\FPC\\Providers\\FPCServiceProvider',
-    61 => 'Webkul\\GDPR\\Providers\\GDPRServiceProvider',
-    62 => 'Webkul\\Installer\\Providers\\InstallerServiceProvider',
-    63 => 'Webkul\\Inventory\\Providers\\InventoryServiceProvider',
-    64 => 'Webkul\\MagicAI\\Providers\\MagicAIServiceProvider',
-    65 => 'Webkul\\Marketing\\Providers\\MarketingServiceProvider',
-    66 => 'Webkul\\Notification\\Providers\\NotificationServiceProvider',
-    67 => 'Webkul\\Payment\\Providers\\PaymentServiceProvider',
-    68 => 'Webkul\\Paypal\\Providers\\PaypalServiceProvider',
-    69 => 'Webkul\\Product\\Providers\\ProductServiceProvider',
-    70 => 'Webkul\\Rule\\Providers\\RuleServiceProvider',
-    71 => 'Webkul\\Sales\\Providers\\SalesServiceProvider',
-    72 => 'Webkul\\Shipping\\Providers\\ShippingServiceProvider',
-    73 => 'Webkul\\Shop\\Providers\\ShopServiceProvider',
-    74 => 'Webkul\\Sitemap\\Providers\\SitemapServiceProvider',
-    75 => 'Webkul\\SocialLogin\\Providers\\SocialLoginServiceProvider',
-    76 => 'Webkul\\SocialShare\\Providers\\SocialShareServiceProvider',
-    77 => 'Webkul\\Tax\\Providers\\TaxServiceProvider',
-    78 => 'Webkul\\Theme\\Providers\\ThemeServiceProvider',
-    79 => 'Webkul\\User\\Providers\\UserServiceProvider',
-    80 => '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',
+            ]);
+        });
+    }
+};

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

@@ -0,0 +1,24 @@
+<?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);
+        }
+    }
+}

+ 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');
     }
 }

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

@@ -18,6 +18,7 @@ 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;
 
         foreach ($invoice->items as $item) {
             $invoice->tax_amount += $item->tax_amount;
@@ -51,6 +52,9 @@ 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;
+
         if ($invoice->order->shipping_tax_amount) {
             $invoice->shipping_tax_amount = $invoice->order->shipping_tax_amount;
 
@@ -85,8 +89,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->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->save();
         return $invoice;

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

@@ -13,9 +13,10 @@ 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;
 
         foreach ($order->invoices as $invoice) {
             $order->sub_total_invoiced += $invoice->sub_total;
@@ -30,18 +31,21 @@ 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->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->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 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);

+ 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' => '运费险配置保存成功!',
+    ],
+];

File diff suppressed because it is too large
+ 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) }}

+ 42 - 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;
 
@@ -372,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,

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

@@ -42,7 +42,11 @@ class SaveCheckoutCartInput
     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;
     #[ApiProperty(description: 'Reward points to redeem against the order total. Pass "-1" to leave unchanged, "0" to remove.')]
     #[SerializedName('rewardPoints')]
     public ?int $rewardPoints = null;
+
 }

+ 5 - 0
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;
@@ -258,6 +260,9 @@ class BagistoApiServiceProvider extends ServiceProvider
         $this->app->singleton(SaveCheckoutCartProcessor::class, function ($app) {
             return new SaveCheckoutCartProcessor(
                 $app->make('Webkul\CartRule\Repositories\CartRuleCouponRepository'),
+                $app->make(GiftCardService::class),
+                $app->make(MemberDiscountService::class),
+                $app->make(ShippingInsuranceService::class)
                 $app->make(\Longyi\Gift\Services\GiftCardService::class),
                 $app->make(MemberDiscountService::class),
                 $app->make(\Longyi\RewardPoints\Repositories\RewardPointRepository::class)

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

@@ -18,6 +18,7 @@ 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;
 
 /**
@@ -38,6 +39,7 @@ class SaveCheckoutCartProcessor implements ProcessorInterface
         protected CartRuleCouponRepository $cartRuleCouponRepository,
         protected GiftCardService $giftCardService,
         protected MemberDiscountService $memberDiscountService,
+        protected ShippingInsuranceService $shippingInsuranceService,
         protected RewardPointRepository $rewardPointRepository,
     ) {}
 
@@ -86,6 +88,9 @@ 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);
             }
@@ -233,6 +238,30 @@ class SaveCheckoutCartProcessor implements ProcessorInterface
     }
 
     /**
+     * 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

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

@@ -69,6 +69,10 @@ 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,
+            ]),
             'billing_address'          => $billing,
             'shipping_address'         => $shipping,
             'payment'                  => (new OrderPaymentResource($this->payment))->jsonSerialize(),

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

@@ -209,8 +209,49 @@ 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);
+
+        $refund->grand_total = $refund->sub_total
+            + $refund->tax_amount
+            + $refund->shipping_amount
+            + $shippingInsuranceAmount
+            - $giftcardAmount
+            + $vipPlusAmount
+            - $vipDiscountAmount
+            + $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
+            + $refund->base_adjustment_refund
+            - $refund->base_adjustment_fee
+            - $refund->base_discount_amount;
 
         $refund->save();
 
@@ -260,7 +301,44 @@ 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['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']
+            + $data['adjustment_refund']
+            - $data['adjustment_fee']
+            - $totals['discount']['price'];
 
         $totals = array_map(function ($item) {
             $item['formatted_price'] = core()->formatBasePrice($item['price']);

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

@@ -86,6 +86,10 @@ 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,
+            ]),
         ];
     }
 }

+ 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>