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.
- checksums.yaml +7 -0
- data/README.md +175 -0
- data/Rakefile +21 -0
- data/app/assets/config/spree_delhivery_manifest.js +4 -0
- data/app/assets/images/integration_icons/delhivery.png +0 -0
- data/app/assets/images/payment_icons/delhivery.svg +12 -0
- data/app/assets/images/payment_icons/delhivery_cod.svg +12 -0
- data/app/controllers/spree/admin/delhivery_controller.rb +190 -0
- data/app/controllers/spree/admin/delhivery_returns_controller.rb +82 -0
- data/app/controllers/spree/admin/fulfillments_controller.rb +117 -0
- data/app/controllers/spree/admin/shipments_controller_decorator.rb +198 -0
- data/app/controllers/spree/admin/stock_locations_controller_decorator.rb +38 -0
- data/app/controllers/spree/api/v3/store/delhivery_controller.rb +126 -0
- data/app/jobs/spree_delhivery/base_job.rb +5 -0
- data/app/models/spree/calculator/shipping/delhivery.rb +97 -0
- data/app/models/spree/integrations/delhivery.rb +48 -0
- data/app/models/spree/order_decorator.rb +63 -0
- data/app/models/spree/page_blocks/products/delhivery_edd.rb +42 -0
- data/app/models/spree/page_sections/product_details_decorator.rb +26 -0
- data/app/models/spree/payment_method/delhivery_cod.rb +57 -0
- data/app/services/spree_delhivery/client.rb +281 -0
- data/app/services/spree_delhivery/pickup_service.rb +49 -0
- data/app/services/spree_delhivery/shipment_canceler.rb +59 -0
- data/app/services/spree_delhivery/shipment_sender.rb +210 -0
- data/app/services/spree_delhivery/shipment_tracker.rb +50 -0
- data/app/views/spree/admin/fulfillments/new.html.erb +118 -0
- data/app/views/spree/admin/integrations/forms/_delhivery.html.erb +51 -0
- data/app/views/spree/admin/orders/_shipment.html.erb +180 -0
- data/app/views/spree/admin/orders/return_authorizations/_return_authorization.html.erb +157 -0
- data/app/views/spree/admin/page_blocks/forms/_delhivery_edd.html.erb +157 -0
- data/app/views/spree/admin/payment_methods/configuration_guides/_delhivery_cod.html.erb +71 -0
- data/app/views/spree/admin/payment_methods/descriptions/_delhivery_cod.html.erb +7 -0
- data/app/views/spree/admin/return_authorizations/index.html.erb +143 -0
- data/app/views/spree/admin/shipments/edit.html.erb +40 -0
- data/app/views/spree/admin/stock_locations/_delhivery_fields.html.erb +19 -0
- data/app/views/spree/admin/stock_locations/_form.html.erb +184 -0
- data/app/views/spree/checkout/payment/_delhivery_cod.html.erb +9 -0
- data/app/views/spree/page_blocks/products/delhivery_edd/_delhivery_edd.html.erb +239 -0
- data/app/views/spree_delhivery/_head.html.erb +0 -0
- data/config/importmap.rb +6 -0
- data/config/initializers/spree.rb +15 -0
- data/config/initializers/spree_permitted_attributes.rb +4 -0
- data/config/locales/en.yml +36 -0
- data/config/routes.rb +42 -0
- data/db/migrate/20250101000001_add_delhivery_fields_to_shipments.rb +10 -0
- data/db/migrate/20250101000002_add_tracking_status_to_shipments.rb +13 -0
- data/db/migrate/20251227110851_add_delhivery_fields_to_spree_stock_locations.rb +5 -0
- data/db/migrate/20251227112401_add_geolocation_to_stock_locations.rb +9 -0
- data/db/migrate/20251227123158_add_missing_coordinates_to_stock_locations.rb +18 -0
- data/db/migrate/20251228081459_add_delhivery_to_return_authorizations.rb +8 -0
- data/lib/generators/spree_delhivery/install/install_generator.rb +139 -0
- data/lib/spree_delhivery/configuration.rb +13 -0
- data/lib/spree_delhivery/engine.rb +39 -0
- data/lib/spree_delhivery/factories.rb +6 -0
- data/lib/spree_delhivery/version.rb +7 -0
- data/lib/spree_delhivery.rb +13 -0
- data/lib/tasks/delhivery.rake +60 -0
- 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
|
|
Binary file
|
|
@@ -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
|