spree_delhivery 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +175 -0
  3. data/Rakefile +21 -0
  4. data/app/assets/config/spree_delhivery_manifest.js +4 -0
  5. data/app/assets/images/integration_icons/delhivery.png +0 -0
  6. data/app/assets/images/payment_icons/delhivery.svg +12 -0
  7. data/app/assets/images/payment_icons/delhivery_cod.svg +12 -0
  8. data/app/controllers/spree/admin/delhivery_controller.rb +190 -0
  9. data/app/controllers/spree/admin/delhivery_returns_controller.rb +82 -0
  10. data/app/controllers/spree/admin/fulfillments_controller.rb +117 -0
  11. data/app/controllers/spree/admin/shipments_controller_decorator.rb +198 -0
  12. data/app/controllers/spree/admin/stock_locations_controller_decorator.rb +38 -0
  13. data/app/controllers/spree/api/v3/store/delhivery_controller.rb +126 -0
  14. data/app/jobs/spree_delhivery/base_job.rb +5 -0
  15. data/app/models/spree/calculator/shipping/delhivery.rb +97 -0
  16. data/app/models/spree/integrations/delhivery.rb +48 -0
  17. data/app/models/spree/order_decorator.rb +63 -0
  18. data/app/models/spree/page_blocks/products/delhivery_edd.rb +42 -0
  19. data/app/models/spree/page_sections/product_details_decorator.rb +26 -0
  20. data/app/models/spree/payment_method/delhivery_cod.rb +57 -0
  21. data/app/services/spree_delhivery/client.rb +281 -0
  22. data/app/services/spree_delhivery/pickup_service.rb +49 -0
  23. data/app/services/spree_delhivery/shipment_canceler.rb +59 -0
  24. data/app/services/spree_delhivery/shipment_sender.rb +210 -0
  25. data/app/services/spree_delhivery/shipment_tracker.rb +50 -0
  26. data/app/views/spree/admin/fulfillments/new.html.erb +118 -0
  27. data/app/views/spree/admin/integrations/forms/_delhivery.html.erb +51 -0
  28. data/app/views/spree/admin/orders/_shipment.html.erb +180 -0
  29. data/app/views/spree/admin/orders/return_authorizations/_return_authorization.html.erb +157 -0
  30. data/app/views/spree/admin/page_blocks/forms/_delhivery_edd.html.erb +157 -0
  31. data/app/views/spree/admin/payment_methods/configuration_guides/_delhivery_cod.html.erb +71 -0
  32. data/app/views/spree/admin/payment_methods/descriptions/_delhivery_cod.html.erb +7 -0
  33. data/app/views/spree/admin/return_authorizations/index.html.erb +143 -0
  34. data/app/views/spree/admin/shipments/edit.html.erb +40 -0
  35. data/app/views/spree/admin/stock_locations/_delhivery_fields.html.erb +19 -0
  36. data/app/views/spree/admin/stock_locations/_form.html.erb +184 -0
  37. data/app/views/spree/checkout/payment/_delhivery_cod.html.erb +9 -0
  38. data/app/views/spree/page_blocks/products/delhivery_edd/_delhivery_edd.html.erb +239 -0
  39. data/app/views/spree_delhivery/_head.html.erb +0 -0
  40. data/config/importmap.rb +6 -0
  41. data/config/initializers/spree.rb +15 -0
  42. data/config/initializers/spree_permitted_attributes.rb +4 -0
  43. data/config/locales/en.yml +36 -0
  44. data/config/routes.rb +42 -0
  45. data/db/migrate/20250101000001_add_delhivery_fields_to_shipments.rb +10 -0
  46. data/db/migrate/20250101000002_add_tracking_status_to_shipments.rb +13 -0
  47. data/db/migrate/20251227110851_add_delhivery_fields_to_spree_stock_locations.rb +5 -0
  48. data/db/migrate/20251227112401_add_geolocation_to_stock_locations.rb +9 -0
  49. data/db/migrate/20251227123158_add_missing_coordinates_to_stock_locations.rb +18 -0
  50. data/db/migrate/20251228081459_add_delhivery_to_return_authorizations.rb +8 -0
  51. data/lib/generators/spree_delhivery/install/install_generator.rb +139 -0
  52. data/lib/spree_delhivery/configuration.rb +13 -0
  53. data/lib/spree_delhivery/engine.rb +39 -0
  54. data/lib/spree_delhivery/factories.rb +6 -0
  55. data/lib/spree_delhivery/version.rb +7 -0
  56. data/lib/spree_delhivery.rb +13 -0
  57. data/lib/tasks/delhivery.rake +60 -0
  58. metadata +151 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '05580fdecdbb22dca647a7f073e15bae3ccc34a15b563fbced26b05f384d3b82'
4
+ data.tar.gz: a4dee0340ac7ac8587ec7df82ed266999369f080ae658216a42307f45d1753fe
5
+ SHA512:
6
+ metadata.gz: 1563cee16ea41246edf09ea656a80fb1420796c69e6758c6b7b4a51d8a49715787f9fa9bfb16e3df933d298b56ad1250c9b87fa57d4af10f1902fecd10d6fc2b
7
+ data.tar.gz: 2a0cd1fd6fc233f5301903934b4e56ab67c094e63874c407b984385e18fdc7916a92afa0fe35ca3e9fc8d1863c9bb50925348f14f4d67578da5b523276069052
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # Spree Delhivery Integration
2
+
3
+ <img width="300" height="auto" alt="delhivery Header" src="https://github.com/user-attachments/assets/c3fb2919-a732-4719-905a-54d202380703" /><br>
4
+
5
+ This extension provides a comprehensive integration between **Spree Commerce** and **Delhivery Logistics**. It streamlines your shipping workflow by allowing you to generate waybills, schedule pickups, print labels, and track shipments directly from the Spree Admin panel. It also enhances the customer experience with a storefront delivery availability widget.
6
+
7
+ ## 🚀 Key Features
8
+
9
+ ### 📦 Admin Logistics & Fulfillment
10
+ * **Live Shipping Rates:** Automatically calculates shipping costs based on product weight (volumetric vs actual) and distance.
11
+ * **One-Click Manifesting:** Generate Delhivery Waybills and tracking numbers directly from the Shipment card.
12
+ * **Label Printing:** Download and print official PDF shipping labels (AWB).
13
+ * **Pickup Scheduling:** Schedule carrier pickups for specific dates and times directly from the Admin UI.
14
+ * **Tracking Sync:** Real-time status updates (e.g., *In Transit, Out for Delivery, Delivered, RTO*) displayed with color-coded badges.
15
+ * **Cancellation:** Void/Cancel waybills before pickup directly from Spree.
16
+
17
+ ### 📍 Warehouse Management (Geolocation)
18
+ * **Interactive Map:** Upgraded Stock Location form with a **Leaflet.js** map.
19
+ * **Pinpoint Accuracy:** Search for cities or drag the pin to set precise Latitude/Longitude for accurate pickup calculations.
20
+ * **Auto-Fill:** Automatically captures coordinates to ensure accurate logistics routing.
21
+
22
+ ### 🛍️ Storefront Experience (PDP Widget)
23
+ * **Delivery Checker:** Users can enter their Pincode to check serviceability.
24
+ * **Smart Location Detection:** improved logic to detect and display **"City, District, State"** (e.g., *Bardoli, Surat, Gujarat*) instead of just the post office name.
25
+ * **Estimated Delivery Date (EDD):** Shows dynamic delivery dates based on Delhivery TAT API.
26
+ * **Countdown Timer:** "Order within 2 hrs 30 mins for delivery by Tuesday" logic.
27
+ * **Customizable UI:** Admin controls for widget colors, headings, and placeholder text.
28
+
29
+ ---
30
+
31
+ ## 🛠 Installation
32
+
33
+ 1. Add this line to your application's `Gemfile`:
34
+
35
+ ```ruby
36
+ gem 'spree_delhivery', github: 'umeshravani/spree_delhivery'
37
+ ```
38
+ 2. Install the Gem:
39
+
40
+ ```ruby
41
+ bundle install
42
+ ```
43
+ 3. Run the installation generator:
44
+
45
+ • This installs migrations.<br>
46
+ • Runs migrations (optional).<br>
47
+ • Seeds Shipping Methods: Automatically creates "Delhivery Surface" and "Delhivery Express" shipping methods with correct preferences.
48
+
49
+ ```ruby
50
+ bundle exec rails g spree_delhivery:install
51
+ ```
52
+ <br>
53
+
54
+ ## ⚙️ Configuration
55
+
56
+ ### 1. General Settings
57
+
58
+ Go to Admin Panel -> Integrations -> Delhivery.
59
+
60
+ <img width="279" height="332" alt="Integrations Page" src="https://github.com/user-attachments/assets/3da6c1be-92b3-410c-ab00-f0bb27e79099" /><br>
61
+
62
+
63
+ • API Token: Enter your Delhivery API Token (masked for security).
64
+
65
+ • Production Mode: Check this box for live shipments. Uncheck for Sandbox/Testing.
66
+
67
+ • Pickup Location: Crucial. This must match the exact warehouse name registered in your Delhivery Dashboard.
68
+
69
+ • Unit Mapping: Select how your store stores Weight (kg/lbs) and Dimensions (cm/in) so the calculator converts them correctly for the API.<br>
70
+
71
+ <img width="500" height="auto" alt="Integration Settings Delhivery" src="https://github.com/user-attachments/assets/d74ff3ca-13c2-4ff4-9465-2f7e483a9955" /><br>
72
+
73
+
74
+ ### 2. Shipping Methods
75
+
76
+ If you didn't seed them during install, create a Shipping Method in Admin -> Shipping -> Shipping Methods:
77
+
78
+ • Calculator: Select Delhivery Live Rate.
79
+
80
+ • Service Mode: Enter Surface or Express.
81
+
82
+ • Tracking URL: https://www.delhivery.com/track/package/:tracking <br>
83
+
84
+ <img width="800" height="auto" alt="Shipping Methods (Auto Added)" src="https://github.com/user-attachments/assets/017a6fda-3059-4bfc-97aa-29f21674e1b4" /><br>
85
+
86
+
87
+ ### 3. Widget Configuration
88
+
89
+ Go to Admin -> Content -> Page Blocks -> Delhivery EDD.
90
+
91
+ • Customize the Heading, Button Text, and Colors.
92
+
93
+ • Set the Cutoff Time (e.g., 2:00 PM) to control the "Order within..." countdown timer.<br>
94
+
95
+ <img width="800" height="auto" alt="Delhivery EDD Widget" src="https://github.com/user-attachments/assets/8802836d-ad0c-4ccb-9bad-629a508322ae" /><br>
96
+
97
+
98
+ ## 🖥️ Usage Guide
99
+
100
+ ### Fulfillment Workflow (Admin)
101
+
102
+ 1. Navigate to Orders -> Order # -> Shipments.
103
+
104
+ 2. You will see the unified Delhivery Toolbar on the shipment card.
105
+
106
+ 3. Ship: Click "Ship with Delhivery". This generates the AWB.
107
+
108
+ 4. Print: Click the Printer icon to get the PDF label.
109
+
110
+ 5. Pickup: Click the Truck icon to open the Schedule Pickup Modal. Select date/time and confirm.
111
+
112
+ 6. Track: Click the Refresh icon to pull the latest status from Delhivery.<br>
113
+
114
+
115
+ <img width="800" height="auto" alt="Orders Page (Unshipped Order)" src="https://github.com/user-attachments/assets/1c1527b3-4096-4b26-a594-5ebe3f528677" /><br>
116
+
117
+ <img width="800" height="560" alt="Shipped Order (Orders Page)" src="https://github.com/user-attachments/assets/9dc19710-e5b9-4e69-a862-ea7d02d1cb13" /><br>
118
+
119
+
120
+ ### Storefront Widget
121
+
122
+ To display the Pincode checker on your product page, add this helper to your products/show view file:
123
+
124
+ 1. Find your Partial file Example:
125
+ ```
126
+ 'app/views/themes/default/spree/page_sections/_product_details.html.erb'
127
+ ```
128
+ Note: If you dont find this file inside your spree's directory, You can [Download](https://github.com/spree/spree/blob/df400d3557c244ec3829f175a27f3990cdeb2452/storefront/app/views/themes/default/spree/page_sections/_product_details.html.erb#L4) this directly from Spree's Github and place it exactly inside your Spree's directory
129
+
130
+ 2. Place this Rendering Code:
131
+
132
+ ```ruby
133
+ <% when 'Spree::PageBlocks::Products::DelhiveryEdd' %>
134
+ <%= block.render(self, product: product) %>
135
+ ```
136
+ 3. Exactly below this part:
137
+ ```
138
+ <% when 'Spree::PageBlocks::Products::Description' %>
139
+ ```
140
+
141
+ ## 🧩 Technical Details
142
+
143
+ • Maps: Uses OpenStreetMap + Leaflet.js (No Google Maps API key required).
144
+
145
+
146
+ • Turbo Support: Fully compatible with Turbo Drive; map re-initializes correctly on page transitions.
147
+
148
+
149
+ • Styling: Uses Tailwind CSS utility classes matching Spree's default admin theme.
150
+
151
+
152
+ • Calculations: Handles volumetric weight calculation (L x W x H) / 5000 automatically based on your unit settings.<br>
153
+
154
+ <img width="800" height="auto" alt="Stock Locations Page (Delhivery Settings)" src="https://github.com/user-attachments/assets/729f5abf-afe7-430f-87ef-93a52997e0c4" /><br>
155
+
156
+
157
+ ## 🛒 Checkout Page Auto-Calculation
158
+
159
+ Eliminate guesswork and undercharging for shipping. This extension integrates directly into the Spree Checkout flow (Delivery Step) to provide accurate costs instantly.
160
+
161
+ • Live API Calls: As soon as a customer enters their shipping address, the calculator queries the Delhivery API for real-time rates based on the specific source and destination pincodes.
162
+
163
+ • Volumetric Weight Logic: Automatically calculates (Length x Width x Height) / 5000 and compares it against the actual weight. The API requests the rate based on whichever is higher, ensuring you never lose money on bulky, lightweight items.
164
+
165
+ • Performance Caching: Rate responses are cached for 15 minutes to ensure fast page loads and prevent hitting API rate limits during high traffic.
166
+
167
+ • Handling Fee Support: Easily add a fixed handling/packing fee on top of the live carrier rate via the Shipping Method preferences.<br>
168
+
169
+ <br><img width="800" height="auto" alt="Checkout Page (Auto Calculate Shipping Costs)" src="https://github.com/user-attachments/assets/aa983ad0-638a-4140-971f-f1f56dead652" />
170
+
171
+
172
+
173
+ ## 🤝 Contributing
174
+
175
+ Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/extension_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir['spec/dummy'].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir('../../')
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'spree_delhivery'
20
+ Rake::Task['extension:test_app'].invoke
21
+ end
@@ -0,0 +1,4 @@
1
+ //= link_tree ../images
2
+ //= link_tree ../../javascript/spree_delhivery/controllers .js
3
+ //= link_tree ../../../vendor/javascript .js
4
+ //= link_tree ../../../vendor/stylesheets .css
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 38 24" width="38" height="24" aria-labelledby="pi-payu">
3
+ <path opacity=".07" d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3"/>
4
+ <path fill="#fff" d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2z"/>
5
+ <style>.st0{opacity:7.000000e-02;enable-background:new}.st1{fill:#fff}</style>
6
+ <path class="st0" d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3"/>
7
+ <path class="st1" d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2z"/>
8
+ <path class="st0" d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3"/>
9
+ <path class="st1" d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2z"/>
10
+ <path d="M 10.73 6.106 C 10.73 5.449 11.262 4.917 11.917 4.917 L 26.172 4.917 C 26.827 4.917 27.359 5.449 27.359 6.106 L 10.73 6.106 Z M 19.044 15.609 C 20.873 15.609 22.016 13.628 21.102 12.045 C 20.678 11.31 19.894 10.856 19.044 10.856 C 17.216 10.856 16.073 12.837 16.987 14.421 C 17.411 15.155 18.195 15.609 19.044 15.609 Z" style="stroke-width: 50; fill: rgb(228, 77, 75);"/>
11
+ <path d="M 9.542 8.48 C 9.542 7.824 10.073 7.293 10.73 7.293 L 27.359 7.293 C 28.016 7.293 28.547 7.824 28.547 8.48 L 28.547 17.983 C 28.547 18.64 28.016 19.172 27.359 19.172 L 10.73 19.172 C 10.073 19.172 9.542 18.64 9.542 17.983 L 9.542 8.48 Z M 13.106 8.48 C 13.106 9.794 12.041 10.856 10.73 10.856 L 10.73 15.609 C 12.041 15.609 13.106 16.672 13.106 17.983 L 24.983 17.983 C 24.983 16.672 26.048 15.609 27.359 15.609 L 27.359 10.856 C 26.048 10.856 24.983 9.794 24.983 8.48 L 13.106 8.48 Z" style="stroke-width: 50; fill: rgb(35, 31, 32);"/>
12
+ </svg>
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 38 24" width="38" height="24" aria-labelledby="pi-payu">
3
+ <path opacity=".07" d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3"/>
4
+ <path fill="#fff" d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2z"/>
5
+ <style>.st0{opacity:7.000000e-02;enable-background:new}.st1{fill:#fff}</style>
6
+ <path class="st0" d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3"/>
7
+ <path class="st1" d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2z"/>
8
+ <path class="st0" d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3"/>
9
+ <path class="st1" d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2z"/>
10
+ <path d="M 10.73 6.106 C 10.73 5.449 11.262 4.917 11.917 4.917 L 26.172 4.917 C 26.827 4.917 27.359 5.449 27.359 6.106 L 10.73 6.106 Z M 19.044 15.609 C 20.873 15.609 22.016 13.628 21.102 12.045 C 20.678 11.31 19.894 10.856 19.044 10.856 C 17.216 10.856 16.073 12.837 16.987 14.421 C 17.411 15.155 18.195 15.609 19.044 15.609 Z" style="stroke-width: 50; fill: rgb(228, 77, 75);"/>
11
+ <path d="M 9.542 8.48 C 9.542 7.824 10.073 7.293 10.73 7.293 L 27.359 7.293 C 28.016 7.293 28.547 7.824 28.547 8.48 L 28.547 17.983 C 28.547 18.64 28.016 19.172 27.359 19.172 L 10.73 19.172 C 10.073 19.172 9.542 18.64 9.542 17.983 L 9.542 8.48 Z M 13.106 8.48 C 13.106 9.794 12.041 10.856 10.73 10.856 L 10.73 15.609 C 12.041 15.609 13.106 16.672 13.106 17.983 L 24.983 17.983 C 24.983 16.672 26.048 15.609 27.359 15.609 L 27.359 10.856 C 26.048 10.856 24.983 9.794 24.983 8.48 L 13.106 8.48 Z" style="stroke-width: 50; fill: rgb(35, 31, 32);"/>
12
+ </svg>
@@ -0,0 +1,190 @@
1
+ module Spree
2
+ module Admin
3
+ class DelhiveryController < Spree::Admin::BaseController
4
+ # Skip load_shipment for create_pickup because that action uses a StockLocation ID
5
+ before_action :load_shipment, except: [:create_pickup]
6
+
7
+ def create_pickup
8
+ stock_id = params[:id]
9
+ @stock_location = if stock_id.to_s.start_with?('stl_') && Spree::StockLocation.respond_to?(:find_by_prefix_id)
10
+ Spree::StockLocation.find_by_prefix_id(stock_id)
11
+ else
12
+ Spree::StockLocation.find(stock_id)
13
+ end
14
+
15
+ service = SpreeDelhivery::PickupService.new(@stock_location, count: 5)
16
+ result = service.call
17
+
18
+ if result.success?
19
+ flash[:success] = result.message
20
+ else
21
+ flash[:error] = "Pickup Failed: #{result.message}"
22
+ end
23
+
24
+ # Turbo Fix for seamless UI updates
25
+ redirect_to spree.edit_admin_stock_location_path(@stock_location), status: :see_other
26
+ end
27
+
28
+ def create_manifest
29
+ sender = SpreeDelhivery::ShipmentSender.new(@shipment)
30
+ result = sender.call
31
+
32
+ if result.success?
33
+ # Force the shipment from 'pending' directly to 'shipped' on a successful API response
34
+ ActiveRecord::Base.transaction do
35
+ @shipment.update_columns(
36
+ state: 'shipped',
37
+ shipped_at: @shipment.shipped_at || Time.current
38
+ )
39
+ # Log all inventory units out of warehouse stock management
40
+ @shipment.inventory_units.where.not(state: 'shipped').update_all(state: 'shipped')
41
+ # Trigger downstream order pipeline status calculations
42
+ @shipment.order.updater.update
43
+ end
44
+
45
+ flash[:success] = "Shipment Manifested! Waybill: #{@shipment.delhivery_waybill}"
46
+ else
47
+ flash[:error] = "Delhivery Error: #{result.error}"
48
+ end
49
+
50
+ # Turbo Fix
51
+ redirect_to spree.edit_admin_order_path(@shipment.order), status: :see_other
52
+ end
53
+
54
+ def delhivery_cancel
55
+ result = SpreeDelhivery::ShipmentCanceler.new(@shipment).call
56
+
57
+ if result.success?
58
+ flash[:success] = "Shipment Waybill Voided Successfully."
59
+ else
60
+ flash[:error] = "Delhivery Error: #{result.error}"
61
+ end
62
+
63
+ # Turbo Fix
64
+ redirect_to spree.edit_admin_order_path(@shipment.order), status: :see_other
65
+ end
66
+
67
+ def download_label
68
+ if @shipment.delhivery_label_url.present?
69
+ redirect_to @shipment.delhivery_label_url, allow_other_host: true
70
+ else
71
+ client = SpreeDelhivery::Client.new
72
+ label_res = client.fetch_label(@shipment.delhivery_waybill)
73
+
74
+ if label_res['packages'].present? && label_res['packages'][0]['pdf_download_link'].present?
75
+ url = label_res['packages'][0]['pdf_download_link']
76
+ @shipment.update(delhivery_label_url: url)
77
+ redirect_to url, allow_other_host: true
78
+ else
79
+ flash[:error] = "Label not generated yet. Please try again later."
80
+ redirect_to spree.edit_admin_order_path(@shipment.order), status: :see_other
81
+ end
82
+ end
83
+ end
84
+
85
+ def sync_tracking
86
+ result = SpreeDelhivery::ShipmentTracker.new(@shipment).call
87
+ current_status = result.status.to_s.upcase
88
+
89
+ raw_data_string = result.data.inspect.upcase if result.data
90
+
91
+ is_cancelled = current_status.include?("CANCEL") ||
92
+ current_status.include?("VOID") ||
93
+ (raw_data_string && (raw_data_string.include?("CANCEL") || raw_data_string.include?("VOID")))
94
+
95
+ if is_cancelled
96
+ ActiveRecord::Base.transaction do
97
+ @shipment.update_columns(
98
+ delhivery_waybill: nil, tracking: nil, tracking_status: "CANCELLED",
99
+ state: "ready", shipped_at: nil
100
+ )
101
+ @shipment.inventory_units.update_all(state: "on_hand")
102
+ @shipment.order.updater.update
103
+ end
104
+
105
+ flash[:success] = "Remote cancellation detected. Waybill cleared and shipment reset to Ready."
106
+ redirect_to spree.edit_admin_order_path(@shipment.order), status: :see_other
107
+ return
108
+ end
109
+
110
+ # THE COD AUTO-CAPTURE ENGINE
111
+ is_delivered = current_status.include?("DELIVERED") ||
112
+ (raw_data_string && raw_data_string.include?("DELIVERED"))
113
+
114
+ if is_delivered
115
+ ActiveRecord::Base.transaction do
116
+ # Find any pending COD payments on this order and capture them
117
+ @shipment.order.payments.valid.where(state: ['pending', 'checkout']).each do |payment|
118
+ if payment.payment_method&.type == 'Spree::PaymentMethod::DelhiveryCod'
119
+ payment.capture!
120
+ Rails.logger.info "[Delhivery] Auto-captured COD Payment #{payment.number} for Order #{@shipment.order.number}"
121
+ end
122
+ end
123
+
124
+ # Crash Prevention: Spree doesn't natively support a 'delivered' state-machine path.
125
+ # We explicitly update columns to avoid NoMethodError on deliver!
126
+ @shipment.update_columns(
127
+ state: 'shipped',
128
+ tracking_status: 'DELIVERED',
129
+ shipped_at: @shipment.shipped_at || Time.current
130
+ )
131
+
132
+ # Cleanly verify inventory states match deployment metrics
133
+ @shipment.inventory_units.where.not(state: 'shipped').update_all(state: 'shipped')
134
+
135
+ # Force downstream state engine recalculations (Clears "Balance Due" badge to green Paid)
136
+ @shipment.order.updater.update
137
+ end
138
+
139
+ flash[:success] = "Shipment Delivered! COD Payment automatically captured and reconciled."
140
+ redirect_to spree.edit_admin_order_path(@shipment.order), status: :see_other
141
+ return
142
+ end
143
+
144
+ # STANDARD TRACKING UPDATE
145
+ if result.success?
146
+ @shipment.update_column(:tracking_status, current_status)
147
+ flash[:success] = "Status Updated: #{current_status}"
148
+ else
149
+ flash[:error] = "Tracking Error: #{result.error || current_status}"
150
+ end
151
+
152
+ # Turbo Fix
153
+ redirect_to spree.edit_admin_order_path(@shipment.order), status: :see_other
154
+ end
155
+
156
+ private
157
+
158
+ def load_shipment
159
+ shipment_param = params[:shipment_id] || params[:id]
160
+ Rails.logger.info "--- [DELHIVERY DEBUG] Processing ID: #{shipment_param} ---"
161
+
162
+ @shipment = nil
163
+
164
+ if shipment_param.to_s.start_with?('ful_')
165
+ if Spree::Shipment.respond_to?(:find_by_prefix_id)
166
+ @shipment = Spree::Shipment.find_by_prefix_id(shipment_param)
167
+ else
168
+ Rails.logger.error "--- [DELHIVERY WARNING] 'find_by_prefix_id' method is missing! ---"
169
+ end
170
+ end
171
+
172
+ @shipment ||= Spree::Shipment.find_by(number: shipment_param)
173
+
174
+ if @shipment.nil? && shipment_param.to_s.match?(/\A\d+\z/)
175
+ @shipment = Spree::Shipment.find_by(id: shipment_param)
176
+ end
177
+
178
+ if @shipment.nil?
179
+ Rails.logger.error "--- [DELHIVERY ERROR] No Shipment found for: #{shipment_param} ---"
180
+ flash[:error] = "Shipment not found for ID: #{shipment_param}"
181
+ redirect_to spree.admin_orders_path, status: :see_other
182
+ end
183
+ rescue => e
184
+ Rails.logger.error "--- [DELHIVERY FATAL] #{e.class}: #{e.message} ---"
185
+ flash[:error] = "An unexpected error occurred: #{e.message}"
186
+ redirect_to spree.admin_orders_path, status: :see_other
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,82 @@
1
+ module Spree
2
+ module Admin
3
+ class DelhiveryReturnsController < Spree::Admin::BaseController
4
+
5
+ def create_pickup
6
+ @return_auth = Spree::ReturnAuthorization.find(params[:id])
7
+
8
+ # 1. Validation: Warehouse Name
9
+ if @return_auth.stock_location.delhivery_warehouse_name.blank?
10
+ flash[:error] = "Delhivery Warehouse Name is missing in Stock Location settings."
11
+ redirect_back(fallback_location: admin_order_return_authorizations_path(@return_auth.order))
12
+ return
13
+ end
14
+
15
+ # 2. Capture Options from Modal
16
+ extra_options = {
17
+ brand: params[:brand],
18
+ category: params[:category]
19
+ }
20
+
21
+ begin
22
+ client = SpreeDelhivery::Client.new
23
+
24
+ # 3. Call Service
25
+ # Note: Ensure your client.rb's create_return_request accepts the second argument (options)
26
+ response = client.create_return_request(@return_auth, extra_options)
27
+
28
+ # 4. Normalize Response
29
+ resp = response.is_a?(HTTParty::Response) ? response.parsed_response : response
30
+ Rails.logger.info "Delhivery Return Response: #{resp.inspect}"
31
+
32
+ # 5. Success Check
33
+ is_success = false
34
+
35
+ # Check 'packages' array (Standard Delhivery Response)
36
+ if resp['packages'].present? && resp['packages'].is_a?(Array)
37
+ first_pkg = resp['packages'].first
38
+ # Success if status is 'Success' OR if we got a valid waybill number
39
+ if first_pkg['status'] == 'Success' || first_pkg['waybill'].to_s.length > 5
40
+ is_success = true
41
+ end
42
+ end
43
+
44
+ if is_success
45
+ waybill = resp['packages'].first['waybill']
46
+ ref_id = resp['packages'].first['ref_id']
47
+
48
+ # Update Database
49
+ @return_auth.update_columns(
50
+ delhivery_waybill: waybill,
51
+ delhivery_ref_id: ref_id
52
+ )
53
+
54
+ flash[:success] = "Reverse Pickup Scheduled! Waybill: #{waybill}"
55
+ else
56
+ # 6. Granular Error Handling
57
+ error_text = "Unknown Error"
58
+
59
+ if resp['detail'].present?
60
+ error_text = "Auth/Permission Error: #{resp['detail']}"
61
+ elsif resp['rmk'].present?
62
+ error_text = resp['rmk']
63
+ elsif resp['packages'].present? && resp['packages'].first
64
+ pkg = resp['packages'].first
65
+ error_text = pkg['remarks'] || pkg['status'] || "Package Error"
66
+ elsif resp['error'].present?
67
+ error_text = resp['error'].to_s
68
+ end
69
+
70
+ flash[:error] = "Delhivery Error: #{error_text}"
71
+ end
72
+
73
+ rescue StandardError => e
74
+ flash[:error] = "Connection Exception: #{e.message}"
75
+ end
76
+
77
+ redirect_back(fallback_location: admin_order_return_authorizations_path(@return_auth.order))
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,117 @@
1
+ module Spree
2
+ module Admin
3
+ class FulfillmentsController < Spree::Admin::BaseController
4
+ before_action :load_order_and_shipment
5
+
6
+ def new
7
+ # 1. Fetch available shipping rates for the dropdown
8
+ @available_methods = Spree::ShippingMethod.all.filter_map do |method|
9
+ begin
10
+ rate = @shipment.shipping_rates.find_by(shipping_method_id: method.id)
11
+ cost = rate&.cost || method.calculator.compute(@shipment) || 0
12
+ display_cost = Spree::Money.new(cost, currency: @order.currency).to_s
13
+
14
+ { id: method.id, name: method.name, cost: cost, display_cost: display_cost }
15
+ rescue NotImplementedError, StandardError
16
+ nil
17
+ end
18
+ end
19
+
20
+ # 2. Pre-calculate Live Delhivery Payload Diagnostics for the UI Audit Box
21
+ if defined?(Spree::Integrations::Delhivery) && Spree::Integrations::Delhivery.active.exists?
22
+ @integration = Spree::Integrations::Delhivery.active.first
23
+
24
+ # Compute Weight
25
+ raw_weight = @shipment.line_items.sum { |li| (li.variant.weight || 0) * li.quantity }
26
+ raw_weight = 0.5 if raw_weight.zero?
27
+ @weight_unit = @integration.preferred_store_weight_unit || 'kg'
28
+ @weight_in_grams = case @weight_unit
29
+ when 'kg' then raw_weight * 1000
30
+ when 'lbs' then raw_weight * 453.592
31
+ when 'g' then raw_weight
32
+ else raw_weight * 1000
33
+ end.to_i
34
+
35
+ # Compute Volumetric Dimensions (Stacking Rule)
36
+ max_l = 0; max_w = 0; total_h = 0
37
+ @shipment.line_items.each do |line_item|
38
+ v = line_item.variant
39
+ q = line_item.quantity
40
+ max_l = [max_l, (v.depth || 10).to_f].max
41
+ max_w = [max_w, (v.width || 10).to_f].max
42
+ total_h += ((v.height || 10).to_f * q)
43
+ end
44
+
45
+ @dim_unit = @integration.preferred_store_dimension_unit || 'cm'
46
+ @dims_cm = [max_l, max_w, total_h].map do |val|
47
+ case @dim_unit
48
+ when 'cm' then val
49
+ when 'in' then val * 2.54
50
+ when 'm' then val * 100
51
+ when 'mm' then val / 10.0
52
+ else val
53
+ end.round(1)
54
+ end
55
+
56
+ # Auto-assign packaging recommendation baseline
57
+ # Delhivery Flyers are restricted to small sizes and weights under 2kg (2000g)
58
+ @recommended_packaging = (@weight_in_grams > 2000 || @dims_cm[0] > 30 || @dims_cm[1] > 30) ? "Carton Box" : "Flyer"
59
+ end
60
+
61
+ render layout: false
62
+ end
63
+
64
+ def create
65
+ if params[:shipping_method_id].present?
66
+ new_method = Spree::ShippingMethod.find_by(id: params[:shipping_method_id])
67
+ if new_method && new_method.id != @shipment.selected_shipping_rate&.shipping_method_id
68
+ new_rate = @shipment.shipping_rates.find_or_initialize_by(shipping_method_id: new_method.id)
69
+ if new_rate.new_record?
70
+ new_rate.cost = new_method.calculator.compute(@shipment) || 0
71
+ new_rate.save!
72
+ end
73
+ @shipment.shipping_rates.update_all(selected: false)
74
+ new_rate.update_column(:selected, true)
75
+ @shipment.update_column(:cost, new_rate.cost)
76
+ @order.update_with_updater!
77
+ end
78
+ end
79
+
80
+ if params[:fulfillment_type] == 'manual'
81
+ ActiveRecord::Base.transaction do
82
+ @shipment.update_columns(
83
+ tracking: params[:tracking_number],
84
+ state: 'shipped',
85
+ shipped_at: @shipment.shipped_at || Time.current
86
+ )
87
+ @shipment.inventory_units.where.not(state: 'shipped').update_all(state: 'shipped')
88
+ @order.updater.update
89
+ end
90
+ Spree::ShipmentMailer.shipped_email(@shipment.id).deliver_later rescue nil
91
+ carrier_name = @shipment.reload.selected_shipping_rate&.name || "Manual Carrier"
92
+ flash[:success] = "Tracking updated successfully via #{carrier_name}."
93
+ redirect_to spree.edit_admin_order_path(@order), status: :see_other
94
+
95
+ elsif params[:fulfillment_type] == 'delhivery'
96
+ # Production Feature: Track layout type form input if selected manually
97
+ if params[:delhivery_packaging_type].present?
98
+ # Store package selection if custom meta columns exist on your shipment
99
+ @shipment.update_column(:delhivery_response_data, (@shipment.delhivery_response_data || {}).merge(selected_packaging: params[:delhivery_packaging_type]))
100
+ end
101
+ redirect_to spree.delhivery_manifest_admin_shipment_path(@shipment), status: :temporary_redirect
102
+ else
103
+ flash[:error] = "Invalid fulfillment type selected."
104
+ redirect_to spree.edit_admin_order_path(@order), status: :see_other
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def load_order_and_shipment
111
+ @order = Spree::Order.find_by!(number: params[:order_id])
112
+ # Accommodate direct collection scoping lookups
113
+ @shipment = @order.shipments.find_by(number: params[:shipment_id]) || Spree::Shipment.find_by!(number: params[:shipment_id])
114
+ end
115
+ end
116
+ end
117
+ end