Five files under docs/, the names the family agreed: README, SETUP,
ARCHITECTURE, CONTRACT, RELEASE. Plus ci/check-docs.php, ported from
woo-security-manager 52dd99d, wired into the static job.
CONTRACT.md is generated from the source rather than written from memory, and
that was the right instruction: my first sweep read only single-quoted hook
names and missed two rows, one of them update_plugins_{$hostname} — the hook
behind the live-shop collision, written with interpolation and therefore
invisible to a pattern built from the concept of the line. Nine hooks
documented: six wc_edm_* filters this plugin offers, three wpmgr_* it both
offers and answers through its copy of the frozen shared-menu.php.
Two hooks the checker structurally cannot see are listed by hand with a note
saying so: wc_edm_prune_audit_log (add_action on a class constant) and
update_plugins_{$hostname} (interpolated). A generated list beats a remembered
one and is still a list of what the generator can see.
The checker needed two fixes before it was safe to run here.
🔴 The published version reads HOOK_PREFIX from ci/plugin.conf for the
documented side, and refuses without it — then tests the source side against a
hardcoded 'nlsec_'. Ported verbatim that is not a green over nothing, which
would be easy to spot. Measured in this tree with CONTRACT.md documenting no
hook at all:
3 hook(s) in the shipped source and not in docs/CONTRACT.md:
wpmgr_dashboard_cards, wpmgr_menu_glyph, wpmgr_menu_title
Red, specific, and blind to all six wc_edm_* hooks — because 'wpmgr_' is the
same string in every tree and 'nlsec_' is the one that is not. Fix those three
and it goes green over six undocumented contract hooks, with a red run behind
you as evidence that it works. A partially blind check that fails is more
convincing than one that passes.
Note which step disarmed the guard. The floor at 20 shipped PHP files refused
to run in this 14-file tree, and that refusal was the only thing standing in
the way; the porting instructions were to add HOOK_PREFIX and adjust the floor.
Doing as told is what removed the protection — two wrongs cancelling into a
pass, in the order CLAUDE.md predicts, because the floor is the error you can
see and the prefix is not.
Both numbers now come from the conf (HOOK_PREFIX, MIN_SHIPPED_PHP_FILES) and
both refuse rather than defaulting. The inherited literal 20 is gone: a second
hardcoded number per plugin is a second thing to re-measure on every port.
Ten selftest assertions, 58 total. Four failure modes, the two refusals, the
counts on the pass, and the row that matters most:
🔴 A hook name in a COMMENT must not read as a hook call. CONTRACT.md documents
a filter by showing the apply_filters() a consumer makes, and the same example
sits in a docblock — so a checker that reads comments reports this plugin
consuming a filter it answers, in the file explaining why it must not. That
assertion is a control asserting a PASS, which is the kind that can quietly
become unable to fail, so it names the specific success line rather than
reading an exit code, and it is proven both ways: with token_get_all's
T_COMMENT branch made inert, exactly one of 58 assertions fails and it is that
one.
docs/TESTING-AND-RELEASES.md is folded into RELEASE.md and deleted rather than
left beside it — two copies of a rule is how the copies start to disagree.
Folding surfaced that it still documented the pre-rename Update URI
(kw/woocommerce-email-discount-manager) and still recommended, as future work,
the split into a releases-only repository that has already happened. Both
corrected in place, with the correction dated.
docs/ is not in ci/dist-files.sh — measured, 0 docs files in the zip.
static=0 selftest=0 site=0; site still 69 assertions on WordPress 7.1.2 with
WooCommerce 11.1.2. ci/ and docs/ ship nothing, so no version bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.1 KiB
Nolimit Discounts
Version 1.6.3 · a WooCommerce plugin · slug woocommerce-email-discount-manager
Gives a customer a discount, and decides which shipping methods they may choose, based on the email address they check out with.
What it does
A shop puts in email patterns and says what each one is worth. At checkout the plugin looks at the customer's email, finds the most specific pattern that matches, and applies it.
Three kinds of assignment, one per email pattern:
| type | effect |
|---|---|
| Group | the customer gets a discount group's percent, on the product categories attached to that group |
| Coupon | an existing WooCommerce coupon is applied automatically |
| Custom | a flat percent off the whole cart |
Patterns can be a full address (john@example.com), a domain
(@company.com — everyone there), or a fragment (john — any address
containing it). When more than one matches, the most specific wins: exact
address, then domain, then fragment.
Shipping
A discount group can be given shipping rules, which is the half worth reading twice because it decides what a stranger sees.
A method a group is told to show is reserved for that group. Customers who match no group do not see it. Before 1.6.0 a method set up for approved company addresses stayed on the checkout for everybody, so free delivery meant for one customer could be chosen by anyone who found it — the bug that release fixed.
The plugin never leaves a customer with no shipping method at all. If applying the rules would empty the list, every method comes back.
What it is not
Facts that take a while to establish by reading the source, so they are written down here:
- Not a coupon engine. The Coupon assignment type applies a coupon WooCommerce already has. This plugin does not create, price or validate one.
- No public REST surface. There is a REST API, and every route requires
manage_woocommerce. Nothing is readable by an anonymous visitor. - No telemetry. Nothing is sent anywhere. The only outbound request is the update check against the release feed, and it asks for a version number.
- No block editor or shortcode. The customer-facing side is the cart and checkout calculation and nothing else.
- Data survives uninstall. There is no
uninstall.phpand no uninstall hook, so deleting the plugin leaves its five tables and its options in the database. That is deliberate — deleting a plugin by accident should not destroy a shop's discount configuration — but it means a clean removal is a manual job. See ARCHITECTURE.md.
Where the rest is
| document | what is in it |
|---|---|
| SETUP.md | installing it, and how to tell it is actually working |
| ARCHITECTURE.md | files, boot order, where data lives, what activation does |
| CONTRACT.md | every hook this plugin offers and every one it answers |
| RELEASE.md | how a release is cut, and what is checked before it is called done |
Requires WordPress 5.8+, WooCommerce 6.0+, PHP 7.4+. Tested against WordPress 7.1.2 and WooCommerce 11.1.2 on PHP 7.4, 8.3 and 8.4.