Skip to content

feat(plugin): add the WooCommerce plugin for Nigerian postcodes - #1

Merged
MobileMage merged 25 commits into
mainfrom
feat/plugin
Oct 4, 2026
Merged

MobileMage merged 25 commits into
mainfrom
feat/plugin

Conversation

@MobileMage

@MobileMage MobileMage commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

This adds the WooCommerce plugin for Nigeria's digital postcodes. It
puts a postcode field in the classic checkout and in the checkout
block for Nigerian addresses, checks the format on the server, stores
the postcode with the order, and shows it in the orders list.

With a secret key in the settings, the plugin looks each postcode up
once, after the order. A failed lookup never stops an order: the order
keeps the postcode that the customer typed, gets a status and a note
that names the true cause, and the log gets one line with the postcode
redacted. The settings page never prints the stored key. The plugin
answers export and erase requests for the postcodes it keeps.

The plugin bundles the PHP SDK under its own namespace, so another
plugin's copy cannot clash, and a check fails on any unprefixed name.
The floors are WordPress 6.7 and WooCommerce 10.0, because the checkout
block first skips a hidden address field on the server in 10.0.

Tests run on the floor versions and the newest ones, on SQLite and
MySQL. Browser tests run both checkouts on the zip that a store
installs, and Plugin Check runs on the same zip. The release workflow
makes a draft release only, for a tag that matches the version, after
CI passed for that commit. No test calls NIPOST.

This change is larger than the 400 lines that GIT-5 suggests, because
it starts the repo.

Signed-off-by: MobileMage aidelojejoshua@gmail.com

The WooCommerce plugin needs its own repo, because it follows the
WordPress rules of the PHP standard and ships as a zip. Composer bundles
the PHP SDK, and Strauss moves it under the plugin's own namespace, so
another plugin's copy cannot clash with it. The tests run WordPress's
own test suite with WooCommerce on SQLite, so they need no Docker and
no database server. The plugin file declares support for order tables
and the checkout blocks, and deleting the plugin removes its options.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A store needs to know two things about a Nigerian postcode: whether a
customer typed it in the right form, and whether NIPOST knows it. The
format check runs on the server with the bundled SDK and names the one
problem it finds. The lookup uses the store's own live key through the
WordPress HTTP API, waits at most three seconds and never retries, so a
failure gives a status and an error code in place of a slow checkout.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A store owner needs one place to choose whether the postcode is
required, what to do with old 6-digit codes and whether to look codes
up. The section sits in the Advanced tab, and WooCommerce's settings API
checks the nonce and the capability. The key setting keeps only a live
secret key, because a test key cannot reach the production gateway and
a publishable key must never sit on a server. The key field never
prints the saved key. An empty field keeps it, and a checkbox removes it.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
WooCommerce 9.9 still checks the hidden block field, so a British order
fails there. The plugin header now names 10.0 as the floor. The tests
load vendor/ as well as vendor-prefixed/, so a plugin file that named
an unprefixed bundled class passed every check and failed only on a real
site, where the zip has no vendor/. A new check in composer check fails
on any such name in the plugin code.

The licence check now also needs NOTICE to name each shipped package,
and tests run it on small fake lock files. The uninstall test proves
that an unrelated option stays. The install script finds a missing sum
before it downloads. Comments no longer name files that do not exist.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A reply with a malformed header made the PSR-7 messages throw
InvalidArgumentException, which the lookup did not catch, so the order
could stop. The transport now reports an unreadable reply as a failed
request, and the checker gives the status error for any exception. The
transport also leaves the Host header to WordPress, an empty list of
statuses reads as unchecked, and the fake gateway answers a path it does
not serve with the unknown-path error. Tests cover a timeout, a 5xx
status, an unreadable body and a bad header.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A new valid key now has a test that it replaces the saved key. A new
key wins over a ticked remove box in the same save, so the person does
not lose it. The remove box shows only when a key is saved. The key
note is one translatable string, and the input points to its help text.
Tests now cover the rendered page, the hook wiring and a save by a
person without the capability.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Both checkouts need the same record of a postcode once the order
exists. The plugin keeps the canonical code and the check status under
its own meta keys, and copies the code into the order address so that
shipping plugins see it. A postcode that needs a person gets an order
note with the cause and the fix, and the note and the log show the
code redacted, because an order holds personal data.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A failure on the plugin's own side told the store that the gateway
gave an unreadable answer, so staff would look at the wrong system.
The check now keeps the class of the exception, never its message,
because the message can quote the URL that holds the postcode. Each
error code has its own note, one lookup gives one log line, and
recording the postcodes never throws, so it cannot stop an order.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
WooCommerce hides the postcode field for Nigeria, and its own check
knows only the old 6-digit codes. The classic checkout now shows the
plugin's field for a Nigerian address only, keeps the native field
hidden, and refuses a code with the wrong form with a message that
names the problem. WooCommerce saves the field as meta, so the plugin
moves it to its own key and checks it after the order. A failure of
the plugin itself never stops the order.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Most new stores use the checkout block, which builds its address forms
from registered fields. The plugin registers one address field that a
rule hides unless the country is Nigeria, so it never asks a customer
abroad for a postcode. The Store API checks every field of the address
locale, so the classic field stays optional there, or a required
setting would block every order of the block checkout. A failure of
the plugin itself never stops an order. Only a wrong format does.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The classic checkout deleted WooCommerce's copy of the typed postcode
before the plugin stored its own, so a failure in between could drop it
with no copy left. The plugin now removes WooCommerce's copy only for an
address it stored. On any failure, in either checkout, the typed text
stays as the native postcode, the order gets the status unchecked, and
one warning names the class of the failure. A failed format check logs
a warning too.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A classic checkout request can look like a Store API request, and
WooCommerce then left the classic field optional, so a store that
requires the postcode did not get it. The classic validation now checks
the required setting itself, and keeps only one error when WooCommerce
asks as well. One helper now holds the policy for a failure of the
plugin, which logs the class name and keeps the typed postcode. The
address rules and the block validation log a failure and fail open. The
ADR for the floors now states the environment of the runs.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A store owner scans the orders list, not each order, to find the
postcodes that need a call. The new column shows the shipping postcode,
or else the billing one, with the check status in words. WooCommerce
has one list for order tables and another for orders stored as posts,
so the column joins both, and the tests cover both. A failure in the
column leaves the cell empty and logs one warning, so it cannot break
the list.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The PHPUnit tests reach the plugin through WordPress's hooks, but a
store meets it through two checkout pages, a settings screen and two
orders lists. These tests drive each of them in Chromium against a shop
that installs the release zip, with the gateway answered from the spec
fixtures. The shop refuses a call to any other host, and a test fails
when the browser or the shop makes one.

The tests also check that the field passes axe and works by keyboard,
that the settings keep, replace and remove the saved key, and that the
zip works with the bundled SDK alone. The zip needs its WordPress.org
readme, which names the one external service, what the plugin sends to
it and when.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
WordPress.org reviews each plugin zip with Plugin Check, so CI runs the
same tool on the same zip that a release attaches, and fails on any
error. A version tag, or a manual run for an existing tag, builds that
zip, attests it and attaches it to a draft release. Nothing publishes
the draft or sends the plugin to WordPress.org. CI also runs the browser
tests on each pull request. The readme now links NIPOST's privacy
policy, and the first change file records the plugin's first version.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The zip test now compares each file of the zip with the tracked plugin
files and the bundled SDK, so a stray backup file in src/ fails it.
The zip build copies only tracked files, which keeps such a file out.

The licence check exits with code 2 when the npm report is missing,
empty, not a JSON object or free of packages, and it counts Composer
and npm packages apart. It accepts a package with an either-or
licence when one alternative is permissive.

The readme says that the plugin never sends an old 6-digit postcode and
that NIPOST sees the IP address of the store's server. More browser
tests cover the order note, the address postcode, an old postcode in
the checkout block, a failed lookup in the classic checkout, a customer
outside Nigeria and a block order in the orders list.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The Plugin Check script ended its command with a fallback that hid every
failure, so a crash left an empty report and the job passed. The script
now fails on a stop it does not expect, on an empty or unknown report,
and on any error row.

A release tag must now be a plain version tag, and must equal the plugin
version and the stable tag of the readme, or the job stops before it
builds. The tag reaches the checks through an environment variable. The
draft release step checks that the tag still points to the built commit.
The CI Plugin Check job keeps the zip as an artifact again.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
WooCommerce reuses the order of a failed payment, so both checkout hooks
fired again for the same order. Each retry sent the postcodes to NIPOST
again and added a second note, which spends the rate limit of the key
and let a card-testing run multiply the requests. The order now holds
the result of a lookup: when every Nigerian address has the same stored
postcode and the status is valid or invalid, the plugin sends nothing
and only restores the address postcode. It looks up again after an
error or an unchecked status.

The classic checkout left two empty postcode rows on every order, also
on a shop with no Nigerian customer, and a foreign address could keep a
Nigerian postcode that a customer typed in the hidden field. The plugin
now removes WooCommerce's copy when it is empty, when the address is not
in Nigeria, or when it stored the postcode itself. The checkout block
copy goes in the first two cases. A retry that moves an address out of
Nigeria also clears the stored postcode and the status.

Every request read the secret key, which is not autoloaded, so each page
paid one query for a key that only an order needs. The block field and
the address rules now read the two settings they use. The checker, and
with it the key, exists only for an order with a Nigerian postcode.
Activation adds the three small options, so a missing option costs no
query either.

Each lookup also sent the WordPress version and the address of the shop
in the User-Agent header. The transport now sends the fixed text
gatepost-postcode-for-woocommerce, and the readme says what is sent,
when, and how often.

Smaller points in the same code. A hook that another plugin fires with
something other than an order no longer ends the request with a type
error. The classic required error names the billing or shipping address.
The log names an anonymous exception class without the path of its file.
A kept typed postcode no longer hides an invalid or error status. The
plugin file shows an admin notice instead of a fatal error when the
bundled SDK is missing, as in GitHub's source archive.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A postcode of the new format names one building, so it is personal data.
An erase request through WooCommerce left the full postcode in four meta
rows and in the orders list, and an export left it out. The plugin now
uses the privacy hooks that WooCommerce already pages through. The
export lists the billing and shipping postcode of an order and of a
customer's saved address. The erase removes every copy of the postcode
and keeps the check status, which is not personal data. The plugin also
adds a short text for the privacy policy page that names the lookup, what
it sends and to whom.

The postcode field of the classic checkout is also saved in the user
meta of a customer, and the form of My Account had no format check. The
form now refuses a wrong format for a Nigerian address. Uninstall keeps
the saved postcodes, as it keeps the order meta, because both belong to
the store's own records and the erase tools cover them.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
WordPress runs uninstall.php once, for the site that a person deletes
the plugin from. On a network, every other site kept its four options,
and one of them is a live secret key. The file now visits each site and
deletes the options there. It still leaves the order data alone.

The suite runs on one site, so the network test has its own composer
script, which composer check runs. WordPress's test library installs a
network when WP_MULTISITE is set, and that works on SQLite.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A tag on a commit with a red CI still built a zip and attached it to a
release. Only Plugin Check ran on that zip, and the PHP and browser
tests ran on another build of the same commit. The claim that a store
installs what CI tested rested on a person who checks by hand.

A gate job now reads the check runs of the tagged commit and stops
unless the check, e2e and plugin-check jobs and all three test legs
succeeded. It counts only runs of GitHub Actions, takes the newest run
of each job, and has no permission but read access to contents and
checks. The zip job then runs the browser tests on the zip in dist/
without building a second one, and compares the sum of the file before
and after, so the release attaches the file that the tests used. The
zip job also stops when the tag moved after the gate. Every action keeps
its pinned commit, and the new Node step turns off the package cache.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The orders list showed one postcode with the status of the order. When
the billing and shipping postcodes differ, the status can belong to
either, and staff asked the customer about the wrong one. The cell now
shows both postcodes with their address. When staff correct the postcode
of the order after the check, the cell shows the new postcode and says
that it changed after the check, because the stored status describes the
old one. The cell and its hooks also ignore a call that does not hold an
order.

The block checkout shows the postcode twice in an order, once in the
address and once as a field line. WooCommerce has no setting to hide one
additional field from these displays, so a new decision record explains
why the plugin accepts the repeat.

Other changes. The notice file names the Unicode data inside the bundled
SDK and its licence. The README links the contributing and security files
in the organisation repo and sends questions to Issues. The multi-sentence
texts say that a translation may join the sentences in another way. Every
class file starts with the ABSPATH guard.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The checkout block saves its postcode field in the user meta of a
customer with an account. The export left that postcode out and the
erase request left it in. Both customer hooks now handle the block keys
next to the classic ones.

One status for the whole order meant that a retry sent a known postcode
again next to a failed one, never sent a failed postcode next to an
unknown one, and sent an unchanged postcode again after a country change
away and back. Each address now remembers its own answer, valid or
invalid, with a keyed hash of its postcode. A retry sends only a postcode
with no answer yet or a changed postcode. The hash holds no postcode, it
survives the move of an address out of Nigeria, and an erase request
removes it. The order status stays as the summary that the orders list
reads. A retry that needs no lookup no longer reads the secret key.

The release gate names its jobs and its number of test legs. A test now
compares both with ci.yml, so a new job or leg fails the checks of the
branch and does not pass unseen at release time.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Follow the organisation's README look: a centred header block, the
notice, a link row and a screenshot of the checkout with a synthetic
postcode. Say plainly that no release exists and that the plugin is not
on WordPress.org. Add a section that repeats what the plugin sends to
NIPOST, in line with readme.txt. The image sits in assets/, which the
zip script does not copy, and REUSE.toml covers it.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The WordPress and WooCommerce floors are merged in the spec repo, so
the submodule now points at that commit on its main branch. The
glossary is copied from the spec again, as the check asks.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
@MobileMage
MobileMage marked this pull request as ready for review October 4, 2026 00:59
@MobileMage
MobileMage merged commit e634dd4 into main Oct 4, 2026
8 of 10 checks passed
@MobileMage
MobileMage deleted the feat/plugin branch October 4, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant