Files
nolimit-discounts/docs/ARCHITECTURE.md
T

139 lines
5.9 KiB
Markdown
Raw Normal View History

# Architecture
## The files that ship
18 files. `ci/`, `tests/` and `docs/` are not among them — `ci/dist-files.sh`
decides, and a static check asserts that every PHP file the code loads is in
that list.
| file | what it is |
| --- | --- |
| `woocommerce-email-discount-manager.php` | header, constants, boot order, activation and deactivation |
| `includes/class-db.php` | the five tables, `dbDelta()` schema, and every query |
| `includes/class-helpers.php` | matching an email to an assignment |
| `includes/class-frontend.php` | cart totals and shipping filtering |
| `includes/class-admin.php` | the menu, the three tabs, the dashboard card |
| `includes/class-admin-groups.php` | the Groups tab |
| `includes/class-admin-emails.php` | the Emails tab |
| `includes/class-admin-shipping.php` | the Shipping tab |
| `includes/class-admin-audit.php` | the audit log viewer |
| `includes/class-audit-log.php` | writing and pruning the audit log |
| `includes/class-rate-limiter.php` | per-method REST rate limiting |
| `includes/class-rest-api.php` | the `wc-edm/v1` routes |
| `includes/class-updater.php` | the release-feed update check |
| `includes/shared-menu.php` | the family's shared parent menu — byte-identical in all eight plugins |
| `assets/css/admin.css`, `assets/js/admin.js`, `assets/icon.svg` | admin UI and the icon WordPress draws on the Updates screen |
| `README.txt` | the readme WordPress parses |
## Boot order
The order matters and two of the steps are deliberately before the WooCommerce
check.
1. **File load.** Constants are defined: `WC_EDM_VERSION`,
`WC_EDM_PLUGIN_FILE`, `WC_EDM_PLUGIN_DIR`, `WC_EDM_PLUGIN_URL`.
2. **`plugins_loaded`** → `init()`.
3. **The updater loads first**, before the WooCommerce check. A shop with
WooCommerce switched off for an afternoon must not quietly stop receiving
fixes for this plugin.
4. **The shared menu title filter is added**, also before that check, and for a
related reason: the shared parent menu is registered by whichever family
plugin boots first, and its label must not depend on whether *this* plugin
got past a gate.
5. **WooCommerce check.** Missing → an admin notice, and `init()` returns. The
plugin is dormant, not broken.
6. Text domain, then `includes()`. A file that failed to load is collected and
reported as a notice rather than fataling.
7. **`WC_EDM_DB` and `WC_EDM_Helpers` are required to continue.** Without
either, `init()` returns and the notice stands — everything else builds on
them.
8. `init` priority 5 → `WC_EDM_DB::maybe_upgrade()`, so a schema change lands
after an update that skipped re-activation.
9. Admin, frontend, audit log and REST objects are constructed, each behind a
`class_exists()` check.
The shared menu is registered under `WPMGR_MENU_SLUG` with a
`function_exists( 'wpmgr_ensure_menu' )` fallback, so a site where an older
sibling's copy of `shared-menu.php` loads first still gets a menu.
## Where data lives
### Tables
Five, all prefixed `wp_wc_edm_` (whatever the site's `$wpdb->prefix` is),
created by `dbDelta()` with foreign keys added afterwards:
- `wp_wc_edm_groups` — discount groups
- `wp_wc_edm_group_categories` — group → product category
- `wp_wc_edm_emails` — email patterns and their assignment
- `wp_wc_edm_shipping_rules` — group → shipping method, show or hide
- `wp_wc_edm_audit_log` — admin actions
### Options
- `wc_edm_db_version` — schema version, the only thing `maybe_upgrade()` reads
That is the complete list. This plugin stores no settings in options; the three
tabs write to the tables.
### Transients
- `wc_edm_release_feed` — the update feed response
- `wc_edm_card_counts` — the two numbers on the dashboard card, 5 minutes
### Scheduled events
One: `wc_edm_prune_audit_log`, daily, scheduled on `init` if not already
present and unscheduled on deactivation. It deletes audit rows older than the
retention window.
## Lifecycle
**Activate** (`register_activation_hook`) — refuses with an error if WooCommerce
is not active, then creates the tables.
**Deactivate** (`register_deactivation_hook`) — unschedules the prune event.
Tables, options and transients stay.
**Uninstall** — 🔴 **nothing happens.** There is no `uninstall.php` and no
`register_uninstall_hook`, so deleting the plugin in wp-admin removes the files
and leaves all five tables, both options and the transients in the database.
Deliberate: deleting a plugin by accident should not destroy a shop's discount
configuration. The cost is that a genuine clean removal is manual — drop the
five `wp_wc_edm_*` tables and delete `wc_edm_db_version` by hand.
## The customer-facing path
`woocommerce_cart_calculate_fees` (priority 20) adds the discount as a negative
fee. `woocommerce_before_calculate_totals` (priority 20) applies a coupon for
the Coupon assignment type. `woocommerce_package_rates` (priority 20, 2 args)
filters shipping.
The shipping filter's shape is the part worth reading in full, because a subtle
version of it shipped free delivery to strangers for several releases:
```php
$match = $this->get_current_match();
if ( $match && 'group' === $match->assignment_type && $match->group_id ) {
$rates = $this->apply_group_shipping_rules( $rates, (int) $match->group_id );
} else {
$rates = $this->remove_restricted_rates( $rates );
}
```
The `else` is the fix. A customer who matches nothing must still have the
reserved methods taken away; before 1.6.0 that branch did not exist, so the
restriction only applied to people who matched a group — i.e. to nobody who
mattered. `apply_group_shipping_rules()` also falls back to
`remove_restricted_rates()` when a group has no rules at all, for the same
reason.
## Not present
- No drop-in, no mu-plugin, nothing that runs before WordPress boots.
- Nothing hooks `the_content` or buffers output.
- No cron beyond the one prune event.
- No custom post types, no taxonomies, no user meta.
- No `WP_CLI` commands.