The system includes a full shipping/logistics integration with Zipnova as the primary carrier, built on an extensible multi-carrier architecture for future integrations (Andreani, Correo Argentino, etc.).
Backend:
- Configuration:
app/config/shipping.json- Multi-carrier settings - Core Logic:
app/includes/carriers.php(931 lines) - Universal carrier integration - Admin Panel:
app/pages/admin/config-shipping.php- Carrier configuration - Shipment Management:
app/pages/admin/envios-pendientes.php- Pending shipmentsapp/pages/admin/envios-archivo.php- Archived shipments
- API Endpoints:
app/pages/api/shipping.php- Quotes, create, track - Data Storage:
app/data/shipments/- Per-order shipping data
Frontend:
- New Checkout:
app/pages/frontend/checkout-new.php(2800+ lines) - Vertical layout with shipping - JavaScript Module:
public_html/assets/js/shipping.js(500+ lines) - Frontend shipping logic
Logs:
/logs/zipnova/- Daily event logs/logs/zipnova-responses/- Debug JSON responses
Carrier Identification:
- Carriers identified by 4-letter tags (ZNVA for Zipnova, etc.)
- Extensible for future carriers (ANDR, OCAS, etc.)
Universal Base Status:
pendiente → Shipment created, not yet dispatched
en_transito → In transit to destination
en_reparto → Out for delivery
entregada → Successfully delivered
cancelada → Cancelled by seller/customer
rechazada → Rejected by recipient
devuelta → Returned to sender
fallida → Delivery failed
Per-Carrier Configuration:
{
"carriers": {
"ZNVA": {
"tag": "ZNVA",
"name": "Zipnova",
"type": "zipnova",
"enabled": false,
"mode": "sandbox",
"credentials": {
"account_id": "...",
"client_id": "...",
"client_secret": "..."
},
"origin": {
"origin_id": "...",
"name": "...",
"address": "...",
"city": "...",
"province": "...",
"postal_code": "...",
"country": "AR",
"phone": "...",
"email": "..."
},
"default_package": {
"weight": 500,
"length": 20,
"width": 15,
"height": 10
},
"options": {
"webhook_secret": "...",
"auto_create_shipment": false,
"shipping_cost_margin": 0,
"cache_quotes_minutes": 30,
"timeout_seconds": 30,
"max_retries": 3
},
"enabled_services": {
"standard": true,
"express": true,
"same_day": false
}
}
}
}New shipping object in orders:
{
"shipping": {
"method": "standard",
"service_name": "Envío Estándar",
"cost": 2500,
"carrier": "ZNVA",
"carrier_shipment_id": "123456",
"carrier_status": "in_transit",
"tracking_id": "TRACK123",
"status": "en_transito",
"address": {
"name": "Juan Pérez",
"street": "Av. Corrientes 1234",
"city": "Buenos Aires",
"province": "Buenos Aires",
"postal_code": "C1043AAZ",
"country": "AR",
"phone": "+54 11 1234-5678"
},
"estimated_delivery": "3-5",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T14:20:00Z",
"history": [
{
"status": "pendiente",
"timestamp": "2024-01-15T10:30:00Z",
"notes": "Shipment created"
},
{
"status": "en_transito",
"timestamp": "2024-01-15T14:20:00Z",
"notes": "Picked up by carrier"
}
]
}
}Carrier Configuration:
get_carrier_config($carrier_tag) // Get config for a specific carrier
get_all_carriers() // List all configured carriersZipnova API:
// Get shipping quotes
zipnova_get_quotes($destination, $items, $value)
// Create shipment
zipnova_create_shipment($data)
// Get shipment status
zipnova_get_shipment($shipment_id)
// Cancel shipment
zipnova_cancel_shipment($shipment_id)
// Test API connection
zipnova_test_connection()Helper Functions:
// Calculate delivery time from ISO 8601 duration
calculate_delivery_days($delivery_time) // e.g., "P3DT2H" → "3-5 días"
// Parse ISO 8601 duration to days
parse_iso8601_duration_to_days($duration)
// Build packages from cart items
zipnova_build_packages_from_cart($cart_items)
// Calculate cart metrics
zipnova_calculate_cart_weight($cart_items)
zipnova_calculate_cart_dimensions($cart_items)
zipnova_calculate_cart_value($cart_items, $currency)
// Status mapping
map_carrier_status_to_base($type, $status) // Map carrier status → base status
get_status_label($status) // Get human-readable label
// Render status HTML
render_shipping_status($status)Logging:
zipnova_log($message, $level, $context) // Log events
zipnova_save_response_json($response, $endpoint) // Save debug JSONFeatures:
- Vertical responsive layout (2-column on desktop, stacked on mobile)
- Step-by-step validation (blocked until previous steps complete)
- Delivery method selection (pickup vs shipping)
- Real-time shipping quotes from Zipnova
- Automatic weight/dimension calculation from cart
- Shipping cost integration in total
- Session timeout (1 hour)
- Multi-currency support (ARS/USD)
- MercadoPago integration with shipping cost included
Shipping Calculation:
- Weight from product data or defaults (500g per item if missing)
- Dimensions from product data or defaults (20×15×10 cm if missing)
- Declared value = total cart value in ARS
- Automatic package consolidation
Flow:
- Select delivery method (retiro/envío)
- If envío → Enter shipping address
- Click "Cotizar Envío" → Get real-time quotes from Zipnova
- Select shipping service → Cost added to total
- Complete customer info
- Proceed to MercadoPago payment (includes shipping cost)
Shipping API (/api/shipping):
// Get quotes
GET /api/shipping?action=quotes&postal_code=1234&city=...
POST /api/shipping (with full address + cart data)
// Create shipment
POST /api/shipping?action=create
// Track shipment
GET /api/shipping?action=track&id=SHIPMENT_ID
// Webhook (for carrier status updates)
POST /api/shipping (with webhook signature)Pending Shipments (envios-pendientes.php):
- List all pending shipments
- Filter by status, reference, date
- Create shipment in carrier system
- Cancel shipment
- View tracking details
- Export to CSV
Archived Shipments (envios-archivo.php):
- Historical record of completed/cancelled shipments
- Same filters and export options
Zipnova API:
- HTTP Basic Authentication (client_id:client_secret)
- Retry logic with exponential backoff (max 3 retries)
- Request timeout (30 seconds default)
- Rate limiting
- Webhook signature validation
- Detailed logging of all requests/responses
Data Validation:
- Address validation (required fields, postal code format)
- Weight/dimension validation
- Package value validation
- Service availability checks
Location: app/includes/google-places.php, public_html/assets/js/address-validator.js
The system includes address validation and normalization using Google Places API to ensure accurate deliveries by validating addresses before shipping quotes are generated.
- Address autocomplete with real-time suggestions
- Interactive map with marker showing exact location
- Component extraction (street, neighborhood, city, province, postal code)
- Geocoding with latitude/longitude coordinates
- Country restriction (Argentina by default)
- Optional or mandatory validation (configurable)
Admin Panel (/admin/?page=config-shipping):
// Google Places section in shipping config
$config['google_places'] = [
'enabled' => true, // Enable/disable address validation
'api_key' => 'AIzaSy...', // Google Places API Key
'country_code' => 'ar', // ISO 3166-1 Alpha-2 code
'require_confirmation' => true // Make validation mandatory
];Required Google APIs:
- Places API (New) - For autocomplete and place details
- Maps JavaScript API - For interactive map display
Get API Key:
- Go to Google Cloud Console
- Create new project or select existing
- Enable "Places API (New)" and "Maps JavaScript API"
- Create API Key under Credentials
- (Optional) Restrict API key to your domain for security
Configuration:
google_places_get_config() // Get Google Places config
google_places_is_enabled() // Check if enabled and configured
google_places_get_api_key() // Get API key
google_places_get_country_code() // Get country restriction
google_places_requires_confirmation() // Check if validation is mandatoryValidation:
// Validate address (server-side using Geocoding API)
google_places_validate_address($address_data)
// Returns:
[
'success' => true,
'address' => [
'formatted_address' => 'Av. Corrientes 1234, Balvanera, CABA, Argentina',
'latitude' => -34.6037,
'longitude' => -58.3816,
'place_id' => 'ChIJ...',
'components' => [
'address' => 'Av. Corrientes 1234',
'street_number' => '1234',
'route' => 'Av. Corrientes',
'neighborhood' => 'Balvanera',
'city' => 'Buenos Aires',
'province' => 'Buenos Aires',
'postal_code' => 'C1043',
'country' => 'Argentina'
]
]
]Frontend Config:
// Get safe config for JavaScript
$config = google_places_get_frontend_config();
// Returns:
[
'enabled' => true,
'api_key' => 'AIzaSy...',
'country_code' => 'ar',
'require_confirmation' => true,
'maps_js_url' => 'https://maps.googleapis.com/maps/api/js?key=...'
]Initialization:
// Initialize validator (automatic in checkout)
initAddressValidator(config);
// Load Google Maps API dynamically
loadGoogleMapsAPI(function(success) {
if (success) {
// API loaded successfully
}
});Show Validation Modal:
// Show address validation modal
showAddressValidationModal(
addressData, // Current form data
onConfirm, // Callback on confirmation
onCancel // Callback on cancel
);
// addressData format:
{
address: 'Av. Corrientes 1234',
city: 'CABA',
province: 'Buenos Aires',
postal_code: '1043',
country: 'AR'
}
// Normalized address returned to onConfirm:
{
formatted_address: 'Av. Corrientes 1234, Balvanera, CABA, Argentina',
place_id: 'ChIJ...',
latitude: -34.6037,
longitude: -58.3816,
components: {
address: 'Av. Corrientes 1234',
neighborhood: 'Balvanera',
city: 'Buenos Aires',
province: 'Buenos Aires',
postal_code: 'C1043',
country: 'Argentina'
}
}Close Modal:
closeAddressValidationModal();Flow:
- User fills shipping address fields
- Clicks "🌍 Validar Dirección" button
- Modal opens with:
- Search input with Google Places Autocomplete
- Interactive map (centered on Argentina by default)
- User searches and selects their address
- Map shows selected location with marker
- Displays normalized address components
- User confirms → Form fields auto-update
- "📦 Calcular Costo de Envío" button becomes enabled
- Shipping quote uses normalized address
Mandatory vs Optional:
-
Mandatory (
require_confirmation: true):- Quote button disabled until address validated
- User MUST validate before quoting
- Best for production to ensure accuracy
-
Optional (
require_confirmation: false):- Quote button always enabled
- Validation is suggested but not enforced
- User can skip validation
app/includes/google-places.php # Backend service
public_html/assets/js/address-validator.js # Frontend modal & map
public_html/assets/css/address-validator.css # Modal styles
Checkout Integration:
// In checkout-new.php
require_once APP_PATH . '/includes/google-places.php';
$google_places_config = google_places_get_frontend_config();
// Conditional loading of scripts/styles
<?php if ($google_places_config['enabled']): ?>
<link rel="stylesheet" href="<?php echo url('/assets/css/address-validator.css'); ?>">
<script src="<?php echo url('/assets/js/address-validator.js'); ?>"></script>
<?php endif; ?>Normalized address saved in order:
{
"shipping": {
"address": {
"name": "Juan Pérez",
"street": "Av. Corrientes 1234",
"city": "Buenos Aires",
"province": "Buenos Aires",
"postal_code": "C1043",
"country": "AR",
"phone": "+54 11 1234-5678"
},
"normalized_address": {
"formatted_address": "Av. Corrientes 1234, Balvanera, CABA, Argentina",
"place_id": "ChIJ...",
"latitude": -34.6037,
"longitude": -58.3816,
"components": {
"address": "Av. Corrientes 1234",
"neighborhood": "Balvanera",
"city": "Buenos Aires",
"province": "Buenos Aires",
"postal_code": "C1043"
}
}
}
}For Logistics:
- ✅ Reduces delivery errors from incorrect addresses
- ✅ Standardized address format for all carriers
- ✅ Geocoordinates for precise location
- ✅ Correct neighborhood/locality identification
For Users:
- ✅ Easy address selection with autocomplete
- ✅ Visual confirmation on map
- ✅ Auto-fill of city, province, postal code
- ✅ Confidence in delivery accuracy
API Key Issues:
Error: "Google Maps API not loading"
→ Check API key is correct
→ Verify Places API (New) is enabled
→ Verify Maps JavaScript API is enabled
→ Check API key restrictions (HTTP referrers)
Country Restriction:
No results for address
→ Check country_code matches actual country
→ Try removing country restriction (empty string)
→ Verify address format is valid for country
Modal Not Opening:
→ Check browser console for JavaScript errors
→ Verify address-validator.js is loaded
→ Ensure Google Maps API loaded successfully
→ Check CSP nonce for inline scripts
Google Places API Pricing (as of 2025):
- Autocomplete (per session): ~$0.017 USD per session
- Place Details: $0.017 USD per request
- Maps JavaScript API: $0.007 USD per map load
Monthly Free Tier: $200 USD credit = ~11,700 autocomplete sessions/month
Optimization Tips:
- Use autocomplete sessions (link autocomplete + details = 1 session price)
- Cache validated addresses in session
- Only validate when user clicks button (not automatic)
The architecture is ready for:
- Andreani (tag: ANDR)
- Correo Argentino (tag: OCAS)
- DHL (tag: DHLE)
- Custom carriers with adapter pattern
Adding a new carrier:
- Create carrier config in
shipping.json - Implement carrier-specific functions in
carriers.php - Map carrier statuses to base statuses
- Add to admin UI in
config-shipping.php