This commit is contained in:
@@ -1,54 +1,36 @@
|
|||||||
# OpenCart 3 Module Template
|
# Prevent PO Box Address for OpenCart 3
|
||||||
|
|
||||||
A comprehensive template for developing custom modules and extensions for OpenCart 3.x e-commerce platform.
|
An OpenCart 3 extension template that blocks PO Box addresses during checkout and improves shipping address validation.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🚀 OpenCart 3 Compatibility
|
- Prevents PO Box addresses in shipping and billing forms
|
||||||
- Fully compatible with OpenCart 3.x versions
|
- Works with OpenCart 3.x and the OCMOD modification system
|
||||||
- Uses OCMOD (OpenCart Modification) system for safe modifications
|
- No core file edits required
|
||||||
- No core file changes required
|
- Easy to install as a `.ocmod.zip` extension package
|
||||||
|
- Includes a ready-to-use `install.xml` definition and upload package structure
|
||||||
### 📦 Module Structure
|
|
||||||
- **install.xml**: OCMOD installation file with modification definitions
|
|
||||||
- **upload/**: Directory for module files to be uploaded to OpenCart
|
|
||||||
- **install.php**: Optional PHP installation script
|
|
||||||
- **install.sql**: Optional database installation script
|
|
||||||
|
|
||||||
### 🔄 Automated Releases
|
|
||||||
- Integrated Gitea Actions workflow for automatic builds
|
|
||||||
- Generates `.ocmod.zip` packages on push to main branch
|
|
||||||
- Extracts metadata from `install.xml` for versioning
|
|
||||||
- Ready-to-install extension packages
|
|
||||||
|
|
||||||
### 🛠️ Development Tools
|
|
||||||
- XML-based modification system
|
|
||||||
- Template structure for rapid module development
|
|
||||||
- CI/CD pipeline for quality assurance
|
|
||||||
- Version management through install.xml
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Clone this repository
|
1. Download or clone this repository.
|
||||||
2. Customize the `install.xml` with your module details:
|
2. Open the OpenCart admin panel and go to `Extensions > Installer`.
|
||||||
- Set `<name>`, `<code>`, and `<version>`
|
3. Upload the generated `.ocmod.zip` package.
|
||||||
- Add your modifications in the `<file>` sections
|
4. Go to `Extensions > Modifications` and click `Refresh`.
|
||||||
3. Place your module files in the `upload/` directory
|
5. Open `Extensions > Extensions`, choose `Shipping` or the appropriate extension type, and install the module if needed.
|
||||||
4. Add installation scripts if needed (`install.php`, `install.sql`)
|
|
||||||
5. Push to your repository to trigger automatic release build
|
## Setup
|
||||||
|
|
||||||
|
1. Customize `install.xml` if required:
|
||||||
|
- Update `<name>`, `<code>`, and `<version>`
|
||||||
|
- Adjust the address validation rules in the modification definitions
|
||||||
|
2. Place any additional files in the `upload/` directory for packaging.
|
||||||
|
3. If you add install scripts, include `install.php` or `install.sql`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### For Module Developers
|
- After installation, checkout address fields are validated for PO Box patterns.
|
||||||
1. Use this template as a starting point for new OpenCart modules
|
- Users attempting to enter a PO Box address will be prompted to use a street address instead.
|
||||||
2. Define your modifications in `install.xml`
|
- Test on a development store before deploying to production.
|
||||||
3. Add custom files to `upload/` directory
|
|
||||||
4. Test locally before deploying
|
|
||||||
|
|
||||||
### For Store Owners
|
|
||||||
1. Download the generated `.ocmod.zip` from releases
|
|
||||||
2. Upload via OpenCart's Extension Installer
|
|
||||||
3. Install and configure the module through admin panel
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -61,8 +43,7 @@ A comprehensive template for developing custom modules and extensions for OpenCa
|
|||||||
|
|
||||||
**Mohamed Youssef**
|
**Mohamed Youssef**
|
||||||
Website: [my-dev.pro](https://my-dev.pro)
|
Website: [my-dev.pro](https://my-dev.pro)
|
||||||
Email: Contact through website
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This template is provided as-is for OpenCart module development. Please check individual module licenses for usage terms.
|
This extension template is provided as-is for OpenCart module development. Review and adapt it to your own store policies and licensing needs.
|
||||||
|
|||||||
+233
-2
@@ -1,11 +1,242 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<modification>
|
<modification>
|
||||||
<name></name>
|
<name>Prevent PO Box Addresses</name>
|
||||||
<code></code>
|
<code>mydev_prevent_po_box_addresses</code>
|
||||||
<author><![CDATA[<b style="color: green">MY-Dev | </b><b>Mohamed Youssef</b>]]></author>
|
<author><![CDATA[<b style="color: green">MY-Dev | </b><b>Mohamed Youssef</b>]]></author>
|
||||||
<link>https://my-dev.pro</link>
|
<link>https://my-dev.pro</link>
|
||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
|
<file path="catalog/model/account/address.php">
|
||||||
|
<operation error="log">
|
||||||
|
<search><![CDATA[public function getTotalAddresses() {]]></search>
|
||||||
|
<add position="before"><![CDATA[
|
||||||
|
private function getRequest(string $url): ?array
|
||||||
|
{
|
||||||
|
$connection = curl_init();
|
||||||
|
|
||||||
|
curl_setopt_array($connection, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($connection);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
curl_close($connection);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = curl_getinfo($connection, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($connection);
|
||||||
|
|
||||||
|
if ($status !== 200) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode($response, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch longitude & latitude based on ZIP Code
|
||||||
|
private function getCustomerLocation(string $zipcode, string $countryCode = 'US'): array
|
||||||
|
{
|
||||||
|
$url = sprintf(
|
||||||
|
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates?SingleLine=%s&f=json&outFields=*&countryCode=%s',
|
||||||
|
urlencode((string)$zipcode),
|
||||||
|
urlencode($countryCode)
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->getRequest($url);
|
||||||
|
if (empty($response['candidates'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return $response['candidates'][0]['location'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAvailableBoxes(string $zipcode, string $countryCode = 'US'): array
|
||||||
|
{
|
||||||
|
$location = $this->getCustomerLocation($zipcode, $countryCode);
|
||||||
|
|
||||||
|
if (empty($location)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$longitude = $location['x'];
|
||||||
|
$latitude = $location['y'];
|
||||||
|
|
||||||
|
$url = sprintf(
|
||||||
|
'https://poboxes.usps.com/pobol/app/poLocations/search?longitude=%s&latitude=%s',
|
||||||
|
$location['x'],
|
||||||
|
$location['y']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->getRequest($url);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeAddress(string $address): string
|
||||||
|
{
|
||||||
|
$address = strtoupper(trim($address));
|
||||||
|
|
||||||
|
// Remove punctuation
|
||||||
|
$address = preg_replace('/[.,#]/', ' ', $address);
|
||||||
|
|
||||||
|
// Normalize whitespace
|
||||||
|
$address = preg_replace('/\s+/', ' ', $address);
|
||||||
|
|
||||||
|
$replacements = [
|
||||||
|
// Directions
|
||||||
|
'NORTHWEST' => 'NW',
|
||||||
|
'NORTHEAST' => 'NE',
|
||||||
|
'SOUTHWEST' => 'SW',
|
||||||
|
'SOUTHEAST' => 'SE',
|
||||||
|
'NORTH' => 'N',
|
||||||
|
'SOUTH' => 'S',
|
||||||
|
'EAST' => 'E',
|
||||||
|
'WEST' => 'W',
|
||||||
|
|
||||||
|
// Street suffixes
|
||||||
|
'STREET' => 'ST',
|
||||||
|
'AVENUE' => 'AVE',
|
||||||
|
'ROAD' => 'RD',
|
||||||
|
'DRIVE' => 'DR',
|
||||||
|
'BOULEVARD' => 'BLVD',
|
||||||
|
'LANE' => 'LN',
|
||||||
|
'COURT' => 'CT',
|
||||||
|
'CIRCLE' => 'CIR',
|
||||||
|
'PLACE' => 'PL',
|
||||||
|
'PARKWAY' => 'PKWY',
|
||||||
|
'TERRACE' => 'TER',
|
||||||
|
'HIGHWAY' => 'HWY',
|
||||||
|
'SQUARE' => 'SQ',
|
||||||
|
'TRAIL' => 'TRL',
|
||||||
|
'WAY' => 'WAY',
|
||||||
|
'LOOP' => 'LOOP',
|
||||||
|
'CENTER' => 'CTR',
|
||||||
|
'MOUNT' => 'MT',
|
||||||
|
'JUNCTION' => 'JCT',
|
||||||
|
|
||||||
|
// Common unit designators
|
||||||
|
'APARTMENT' => 'APT',
|
||||||
|
'SUITE' => 'STE',
|
||||||
|
'BUILDING' => 'BLDG',
|
||||||
|
'FLOOR' => 'FL',
|
||||||
|
'ROOM' => 'RM',
|
||||||
|
|
||||||
|
// PO Box
|
||||||
|
'POST OFFICE BOX' => 'PO BOX',
|
||||||
|
'POST OFFICE' => 'PO',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($replacements as $search => $replace) {
|
||||||
|
$address = preg_replace(
|
||||||
|
'/\b' . preg_quote($search, '/') . '\b/',
|
||||||
|
$replace,
|
||||||
|
$address
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize whitespace again
|
||||||
|
$address = preg_replace('/\s+/', ' ', trim($address));
|
||||||
|
|
||||||
|
return $address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPOBoxFacility(string $zipCode, string $address): bool
|
||||||
|
{
|
||||||
|
|
||||||
|
$status = false;
|
||||||
|
$facilities = $this->getAvailableBoxes((string)$zipCode);
|
||||||
|
|
||||||
|
if (empty($facilities)) {
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
$searchAddress = $this->normalizeAddress($address);
|
||||||
|
$address_zipcode = explode('-', $zipCode); // Use only the first part of the ZIP code
|
||||||
|
$zipcode = $address_zipcode[0];
|
||||||
|
$zip4code = $address_zipcode[1] ?? null;
|
||||||
|
|
||||||
|
foreach ($facilities as $facility) {
|
||||||
|
if (($facility['address']['zipCode'] ?? '') !== $zipcode ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$facilityAddress = $this->normalizeAddress($facility['address']['addressLine1']);
|
||||||
|
|
||||||
|
if (strpos($searchAddress, $facilityAddress) !== false || strpos($facilityAddress, $searchAddress) !== false) {
|
||||||
|
$status = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($facility['address']['zip4Code'] ?? '') !== $zip4code) {
|
||||||
|
$status = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
]]></add>
|
||||||
|
</operation>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="catalog/controller/checkout/{payment_address,shipping_address}.php">
|
||||||
|
<operation error="log">
|
||||||
|
<search><![CDATA[if ($this->request->post['country_id'] == '') {]]></search>
|
||||||
|
<add position="before"><![CDATA[
|
||||||
|
if ($this->model_account_address->hasPOBoxFacility($this->request->post['postcode'], $this->request->post['address_1'])) {
|
||||||
|
$json['error']['address_1'] = $this->language->get('error_po_box');
|
||||||
|
}
|
||||||
|
]]></add>
|
||||||
|
</operation>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="catalog/controller/account/address.php">
|
||||||
|
<operation error="log">
|
||||||
|
<search><![CDATA[$country_info = $this->model_localisation_country->getCountry($this->request->post['country_id']);]]></search>
|
||||||
|
<add position="before"><![CDATA[
|
||||||
|
if ($this->model_account_address->hasPOBoxFacility($this->request->post['postcode'], $this->request->post['address_1'])) {
|
||||||
|
$json['error']['address_1'] = $this->language->get('error_po_box');
|
||||||
|
}
|
||||||
|
]]></add>
|
||||||
|
</operation>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="catalog/controller/checkout/guest.php">
|
||||||
|
<operation error="log">
|
||||||
|
<search><![CDATA[if ($this->request->post['country_id'] == '') {]]></search>
|
||||||
|
<add position="before"><![CDATA[
|
||||||
|
$this->load->model('account/address');
|
||||||
|
if ($this->model_account_address->hasPOBoxFacility($this->request->post['postcode'], $this->request->post['address_1'])) {
|
||||||
|
$json['error']['address_1'] = $this->language->get('error_po_box');
|
||||||
|
}
|
||||||
|
]]></add>
|
||||||
|
</operation>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="catalog/language/*/checkout/checkout.php">
|
||||||
|
<operation error="skip">
|
||||||
|
<search><![CDATA[$_['error_address_1'] = 'Address 1 must be between 3 and 128 characters!';]]></search>
|
||||||
|
<add position="after"><![CDATA[
|
||||||
|
$_['error_po_box'] = 'PO Box addresses are not allowed.';
|
||||||
|
]]></add>
|
||||||
|
</operation>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="catalog/language/*/account/address.php">
|
||||||
|
<operation error="skip">
|
||||||
|
<search><![CDATA[$_['error_address_1'] = 'Address must be between 3 and 128 characters!';]]></search>
|
||||||
|
<add position="after"><![CDATA[
|
||||||
|
$_['error_po_box'] = 'PO Box addresses are not allowed.';
|
||||||
|
]]></add>
|
||||||
|
</operation>
|
||||||
|
</file>
|
||||||
|
|
||||||
<file path="">
|
<file path="">
|
||||||
<operation error="skip">
|
<operation error="skip">
|
||||||
<search><![CDATA[]]></search>
|
<search><![CDATA[]]></search>
|
||||||
|
|||||||
Reference in New Issue
Block a user