Files
nolimit-discounts/docs/CONTRACT.md
T
kristianwindandClaude Code 0793a1ee82 1.7.0: shipping rule "Only" reserves a method, "Show" no longer does
Since 1.6.0 "Show" both whitelisted a method for its group and hid it from
everybody else. A group shown ordinary methods next to a free one therefore
took the ordinary ones away from every stranger, who was left with only the
free method. "Only" now carries the reservation; "Show" is just the group's
whitelist. Existing show rules are converted to only once on update so the
checkout does not change until the shop chooses.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-02 12:28:45 +02:00

6.2 KiB

Contract

Every hook this plugin offers, and every family hook it answers. Version 1.7.0.

A contract is a name somebody else has to know. Core hooks this plugin merely listens to — init, admin_menu, wp_enqueue_scripts — are not here; WordPress documents those, and listening to init is not a promise to anyone. The exception is at the bottom, and it is there because it is shared with every sibling plugin on the same update host.

The tables and the source are compared in both directions by ci/check-docs.php, which runs in CI. A hook added without a row here fails the build, and so does a row whose name no longer exists in the source.

Hooks this plugin provides

All six are filters, all read a setting that has a working default, and all are answered from apply_filters() in shipped code. A shop that adds no code gets the default and never needs this table.

hook signature default if you are absent
wc_edm_audit_log_enabled bool $enabled true the audit log records admin actions
wc_edm_audit_log_retention_days int $days 30 rows older than 30 days are pruned daily; the value is floored at 1, so 0 means 1 day, not "keep forever"
wc_edm_client_ip string $ip the resolved address the plugin's own resolution is used — override this behind a proxy
wc_edm_rate_limit_enabled bool $enabled true REST routes are rate limited
wc_edm_rate_limit_max_requests int $default, string $method per method the built-in allowance applies; floored at 1
wc_edm_rate_limit_window int $seconds, string $method MINUTE_IN_SECONDS a 60-second window; floored at 1

What a consumer must not do. Do not return a non-scalar: each of these is cast ((bool), (int), (string)) and a bad type becomes a wrong value rather than an error. Do not assume wc_edm_rate_limit_* can switch limiting off by returning 0 — they are floored at 1; use wc_edm_rate_limit_enabled. Returning false from wc_edm_audit_log_enabled on a shop that has a compliance reason to keep the log is a decision, not a performance tweak.

The family's shared hooks

These cross a repository boundary, which is the class of value nobody's checks cover, so both directions are written out.

hook this plugin what that means
wpmgr_menu_title answers it (main file) and provides it (our copy of shared-menu.php) returns Nolimit, so the shared parent menu is labelled the same whichever sibling registered it
wpmgr_dashboard_cards answers it (class-admin.php) and provides it (our copy of shared-menu.php) adds this plugin's card — colour #1a7a52, two counts, cached 5 minutes
wpmgr_menu_glyph provides it only, via our copy of shared-menu.php this plugin does not override the glyph; it answers with the shared default ∞

Why "provides" and "answers" are both true. includes/shared-menu.php is byte-identical in all eight family plugins and frozen at a SHA in ci/plugin.conf. Whichever copy loads first is the one that runs, so this plugin is simultaneously one of the eight possible providers of these three hooks and a consumer of whichever copy won. That is why the label is set with a filter and not by editing the shared file: editing it would make a site with three of eight plugins updated show a different menu title depending on activation order, and change its mind when a plugin is deactivated.

What a consumer must not do. Do not edit shared-menu.php to change any of these — it reaches every site in activation order and the frozen SHA will fail the build in all eight trees. Answer the filter from your own plugin. wpmgr_menu_title must be idempotent: every sibling adds a callback and the order is not defined.

One core hook, because it is shared

hook this plugin why it is in a contract document
update_plugins_{$hostname} answers it, add_filter( …, 10, 3 ) 🔴 the filter name is built from the hostname, not the repository, so every plugin updating from gitea.nolimit.dk lands on the same hook and WordPress runs every callback for every one of them

Answer it unconditionally and you offer your release to your neighbour. On a live shop in September 2026 the subscriptions plugin was told version 1.4.4 was an update for itself; 1.4.4 was this plugin's. Load order decides who wins, so a rename is enough to flip which plugin gets corrupted — and flipped toward one with auto-updates on, it installs a stranger's zip over itself unattended.

The guard is two halves and both are required:

add_filter( "update_plugins_{$host}", array( $this, 'offer_update' ), 10, 3 );

// and inside offer_update():
if ( '' !== $plugin_file && plugin_basename( WC_EDM_PLUGIN_FILE ) !== $plugin_file ) {
    return $update;
}

Leave the argument count at 2 and $plugin_file never arrives, so the guard silently does nothing — the same shape of bug it fixes. No integration test can catch this, because a test site installs one plugin from that host and the collision cannot arise; the assertion is the defence.

plugins_api is also answered and is safe, because it guards on $args->slug.

What the checker cannot see, and is therefore listed by hand

ci/check-docs.php reads hook names out of the shipped source as string literals. Two names in this tree are not literals at the call site:

hook how it is written why it is missed
wc_edm_prune_audit_log add_action( self::CRON_HOOK, … ) a class constant, resolved at runtime
update_plugins_{$hostname} add_filter( "update_plugins_{$host}", … ) interpolated from a variable

Both rows above are real and both would survive a rename of the constant or the variable without the checker noticing. wc_edm_prune_audit_log is the daily audit-log prune event; it is internal wiring rather than an extension point, but it is a name that appears in wp_next_scheduled() output and in any cron viewer, so it is written down.

This section exists because the instrument's blind spot is worth stating next to its results. A generated list is better than a remembered one, and it is still a list of what the generator can see.